diff --git a/.editorconfig b/.editorconfig index f4f741b5b9..a55734415f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,33 @@ indent_style = space insert_final_newline = true trim_trailing_whitespace = true +[*.cs] +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols +dotnet_naming_rule.private_instance_fields_rule.severity = warning +dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols +dotnet_naming_rule.private_static_fields_rule.severity = warning +dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field +dotnet_naming_symbols.private_constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly + [*.md] charset = utf-8 @@ -108,6 +135,8 @@ dotnet_diagnostic.IDE1003.severity=suggestion dotnet_diagnostic.IDE1004.severity=suggestion dotnet_diagnostic.IDE1007.severity=suggestion dotnet_diagnostic.IDE1008.severity=suggestion +dotnet_diagnostic.IDE0074.severity=suggestion +dotnet_diagnostic.IDE0240.severity=suggestion # Enforce file header dotnet_diagnostic.IDE0073.severity = error diff --git a/.github/actions/deployedge/action.yml b/.github/actions/deployedge/action.yml index 8c676c8fe8..72e0a50677 100644 --- a/.github/actions/deployedge/action.yml +++ b/.github/actions/deployedge/action.yml @@ -43,7 +43,8 @@ runs: - run: | $LBS_IMAGE_TAG=$(az acr repository show-tags --username ${{ env.CONTAINER_REGISTRY_USERNAME }} --password "${{ env.CONTAINER_REGISTRY_PASSWORD }}" --name ${{ env.CONTAINER_REGISTRY_ADDRESS }} --repository lorabasicsstation --orderby time_desc -o json | ConvertFrom-Json)[0] echo $LBS_IMAGE_TAG - $LBS_IMAGE_TAG=$LBS_IMAGE_TAG.replace("-${{ inputs.architecture }}",'') + $LBS_IMAGE_TAG=$LBS_IMAGE_TAG.replace("-arm32v7",'') + $LBS_IMAGE_TAG=$LBS_IMAGE_TAG.replace("-arm64v8",'') echo $LBS_IMAGE_TAG echo "::set-env name=LBS_VERSION::$LBS_IMAGE_TAG" shell: pwsh @@ -65,8 +66,8 @@ runs: shell: bash run: | az extension add --name azure-iot - until `az iot edge deployment show-metric --deployment-id $IOT_EDGE_DEPLOYMENT_ID --metric-id reportedSuccessfulCount --metric-type system --login $IOTHUB_CONNECTION_STRING | grep -q $DEVICE_ID`; do sleep 10 && echo "waiting for deployment to be applied"; done - + until $(az iot hub module-twin show -m '\$edgeAgent' -d $DEVICE_ID --login $IOTHUB_CONNECTION_STRING | jq ".properties.desired.\"\$version\" == .properties.reported.lastDesiredVersion and .properties.reported.lastDesiredStatus.code == 200 and .configurations.$IOT_EDGE_DEPLOYMENT_ID.status == \"Applied\"" | grep -q true); do sleep 10 && echo "waiting for deployment to be applied"; done + - name: Wait for LNS start if: inputs.waitForLnsDeployment == 'true' shell: bash diff --git a/.github/actions/iotedgedev/entrypoint.sh b/.github/actions/iotedgedev/entrypoint.sh index 32c2cc492e..0ed5147dba 100755 --- a/.github/actions/iotedgedev/entrypoint.sh +++ b/.github/actions/iotedgedev/entrypoint.sh @@ -2,8 +2,8 @@ # This file generate an IoT Edge deployment file using token replacement to replace secrets with environment variables # then the file proceed to remove the previous deployment and replace it with the current one +sudo -E az extension add --name azure-iot sudo -E iotedgedev genconfig -f $2/$1 -P $3 --fail-on-validation-error -sudo az extension add --name azure-iot sudo -E az iot edge deployment delete --login "$IOTHUB_CONNECTION_STRING" --deployment-id "$IOT_EDGE_DEPLOYMENT_ID" sudo -E az iot edge deployment create --login "$IOTHUB_CONNECTION_STRING" --content "config/${1//'.template'}" --deployment-id "$IOT_EDGE_DEPLOYMENT_ID" --target-condition "deviceId='$DEVICE_ID'" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4396dbc5db..16a228a9fa 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,8 +5,14 @@ updates: directory: "/" schedule: interval: "daily" - # Maintain dependencies for universal decoder + # Maintain dependencies for universal decoder - package-ecosystem: "npm" directory: "/Samples/UniversalDecoder" schedule: interval: "daily" + # Maintain dependencies for github actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every weekday + interval: "daily" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f5332e8ffd..33fa762850 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,16 +18,19 @@ env: AZURE_FUNCTIONAPP_NAME: loramoduleintegrationtest TESTS_FOLDER: Tests TESTS_RESULTS_FOLDER: Tests/TestResults + CONTAINER_REGISTRY_ADDRESS: "${{ secrets.CI_ACR_NAME }}.azurecr.io" + CONTAINER_REGISTRY_USERNAME: "${{ secrets.CI_ACR_NAME }}" + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CI_ACR_CREDENTIALS }} jobs: build_and_test: name: Build and Test Solution runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 name: Checkout current branch - - uses: actions/setup-dotnet@v1 + - uses: actions/setup-dotnet@v3.0.1 with: dotnet-version: '6.0.x' @@ -79,7 +82,7 @@ jobs: ${{ env.TESTS_FOLDER }}/Integration/LoRaWan.Tests.Integration.csproj # Upload test results as artifact - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 if: success() || failure() with: name: test-results @@ -88,7 +91,7 @@ jobs: ${{ env.TESTS_RESULTS_FOLDER }}/Integration - name: Upload to Codecov test reports - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 with: directory: Tests/ @@ -96,20 +99,19 @@ jobs: name: Build Docker Images runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 name: Checkout current branch - - uses: docker/setup-buildx-action@v1 + - uses: docker/setup-buildx-action@v2 id: buildx with: install: true - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Build docker images run: | - docker buildx bake + echo ${{ env.CONTAINER_REGISTRY_PASSWORD }} | docker login "${{ env.CONTAINER_REGISTRY_ADDRESS }}" --username "${{ env.CONTAINER_REGISTRY_USERNAME }}" --password-stdin + docker buildx bake --set *.args.SOURCE_CONTAINER_REGISTRY_ADDRESS=${{ env.CONTAINER_REGISTRY_ADDRESS }} working-directory: LoRaEngine - env: - CONTAINER_REGISTRY_ADDRESS: docker.io/test diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index df64b12f5a..dcb9a21075 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,16 +20,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: csharp queries: +security-and-quality,security-extended - - uses: actions/setup-dotnet@v1 + - uses: actions/setup-dotnet@v3.0.1 with: dotnet-version: '6.0.x' @@ -38,7 +38,7 @@ jobs: run: dotnet build --configuration Release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 analyze_javascript: name: Analyze Javascript @@ -49,18 +49,18 @@ jobs: security-events: write steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: javascript queries: +security-and-quality,security-extended - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/e2e-ci.yaml b/.github/workflows/e2e-ci.yaml index c733cf442e..1fbab2624b 100644 --- a/.github/workflows/e2e-ci.yaml +++ b/.github/workflows/e2e-ci.yaml @@ -14,7 +14,7 @@ on: # rebuild any PRs and main branch changes description: 'Include Load Tests in current run' default: 'false' TestsToRun: - default: '[SensorDecodingTest,OTAAJoinTest,ABPTest,OTAATest,MacTest,ClassCTest,C2DMessageTest,MultiGatewayTest,MultiConcentratorTest,CupsTest]' + default: '[SensorDecodingTest,OTAAJoinTest,ABPTest,OTAATest,MacTest,ClassCTest,C2DMessageTest,MultiGatewayTest,MultiConcentratorTest,CupsTest,LnsDiscoveryTest,CloudDeploymentTest]' description: 'tests to run' TxPower: description: 'TXPower value to use in E2E tests' @@ -58,10 +58,10 @@ jobs: echo "::set-output name=E2ETestsToRun::${{ github.event.inputs.TestsToRun }}" elif [ ${{ github.event_name }} == 'pull_request' ]; then echo "Set up for pull request" - echo "::set-output name=E2ETestsToRun::[SensorDecodingTest,OTAAJoinTest,ABPTest,OTAATest,MacTest,ClassCTest,C2DMessageTest,MultiGatewayTest,MultiConcentratorTest,CupsTest]" + echo "::set-output name=E2ETestsToRun::[SensorDecodingTest,OTAAJoinTest,ABPTest,OTAATest,MacTest,ClassCTest,C2DMessageTest,MultiGatewayTest,MultiConcentratorTest,CupsTest,LnsDiscoveryTest,CloudDeploymentTest]" else echo "Set up for cron trigger" - echo "::set-output name=E2ETestsToRun::[SensorDecodingTest,OTAAJoinTest,ABPTest,OTAATest,MacTest,ClassCTest,C2DMessageTest,MultiGatewayTest,MultiConcentratorTest,CupsTest]" + echo "::set-output name=E2ETestsToRun::[SensorDecodingTest,OTAAJoinTest,ABPTest,OTAATest,MacTest,ClassCTest,C2DMessageTest,MultiGatewayTest,MultiConcentratorTest,CupsTest,LnsDiscoveryTest,CloudDeploymentTest]" fi - id: check-if-run @@ -92,10 +92,10 @@ jobs: power_on_azure_vm: name: Power ON EFLOW - if: always() + if: needs.env_var.outputs.StopFullCi != 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Power ON Azure VM uses: ./.github/actions/power-azure-vm @@ -107,6 +107,19 @@ jobs: AZURE_SP_SECRET: ${{ secrets.AZURE_SP_SECRET }} AZURE_TENANTID: ${{ secrets.AZURE_TENANTID }} + reset_redis_cache: + name: Reset the Redis Cache + if: needs.env_var.outputs.StopFullCi != 'true' + runs-on: ubuntu-latest + steps: + - name: Flush the database + shell: bash + run: | + wget https://github.com/IBM-Cloud/redli/releases/download/v0.5.2/redli_0.5.2_linux_amd64.tar.gz + tar xzf redli_0.5.2_linux_amd64.tar.gz + chmod +x redli + ./redli --tls -h ${{ secrets.REDIS_HOSTNAME }}.redis.cache.windows.net -p 6380 -a ${{ secrets.REDIS_PASSWORD }} FLUSHALL + # Build and deploy Facade Azure Function deploy_facade_function: environment: @@ -120,10 +133,10 @@ jobs: AZURE_FUNCTIONAPP_PACKAGE_PATH: 'LoRaEngine/LoraKeysManagerFacade/' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 name: Checkout current branch - - uses: actions/setup-dotnet@v1 + - uses: actions/setup-dotnet@v3.0.1 with: dotnet-version: '6.0.x' @@ -152,12 +165,13 @@ jobs: MASTER_IMAGE_TAG: master if: needs.env_var.outputs.RunE2ETestsOnly != 'true' && needs.env_var.outputs.StopFullCi != 'true' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 name: Checkout current branch with: fetch-depth: '2' - - run: | + - id: image_tag_definition + run: | if [ ${{ github.ref }} = "refs/heads/dev" ]; then echo "dev" IMAGE_TAG="$DEV_IMAGE_TAG" @@ -176,21 +190,22 @@ jobs: echo "Using image tag $IMAGE_TAG" echo "::set-env name=NET_SRV_VERSION::$IMAGE_TAG" echo "::set-env name=LBS_VERSION::$IMAGE_TAG" + echo "::set-output name=imagetag::$IMAGE_TAG" env: ACTIONS_ALLOW_UNSECURE_COMMANDS: true - - uses: docker/setup-buildx-action@v1 + - uses: docker/setup-buildx-action@v2 id: buildx with: install: true - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Build Docker LoRaWanNetworkServer images run: | echo ${{ env.CONTAINER_REGISTRY_PASSWORD }} | docker login "${{ env.CONTAINER_REGISTRY_ADDRESS }}" --username "${{ env.CONTAINER_REGISTRY_USERNAME }}" --password-stdin - docker buildx bake --push LoRaWanNetworkServerarm32 LoRaWanNetworkServerx64 + docker buildx bake --push LoRaWanNetworkServerarm32 LoRaWanNetworkServerarm64v8 LoRaWanNetworkServerx64 working-directory: LoRaEngine env: CONTAINER_REGISTRY_ADDRESS: ${{ env.CONTAINER_REGISTRY_ADDRESS }} @@ -198,11 +213,14 @@ jobs: # This image is very costly to rebuild and we rebuild it only when something change in the folders since last commit. - name: Build Docker LoRaWanBasicsStation images run: | - git diff --quiet HEAD HEAD~1 -- modules/LoRaBasicsStationModule/ || docker buildx bake --push LoraBasicsStationarm32v7 + git diff --quiet HEAD HEAD~1 -- modules/LoRaBasicsStationModule/ || docker buildx bake --push LoraBasicsStationarm32v7 LoraBasicsStationarm64v8 --set *.args.SOURCE_CONTAINER_REGISTRY_ADDRESS=${{ env.CONTAINER_REGISTRY_ADDRESS }} working-directory: LoRaEngine env: CONTAINER_REGISTRY_ADDRESS: ${{ env.CONTAINER_REGISTRY_ADDRESS }} + outputs: + imagetag: ${{ steps.image_tag_definition.outputs.imagetag }} + # Generate root and server certificates and copy required files to RPi certificates_job : timeout-minutes: 5 @@ -215,7 +233,7 @@ jobs: runs-on: [ self-hosted, x64 ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 name: Checkout current branch - id: generate_step @@ -274,6 +292,36 @@ jobs: clientfwdigest: ${{ steps.generate_step.outputs.clientfwdigest }} clientfwversion: ${{ steps.generate_step.outputs.clientfwversion }} + # Deploy Cloud based LoRaWAN Network Server + deploy_cloud_lns: + needs: + - env_var + - build_push_docker_images + - certificates_job + runs-on: ubuntu-latest + if: needs.env_var.outputs.RunE2ETestsOnly != 'true' && needs.env_var.outputs.StopFullCi != 'true' + name: Deploy Cloud based LNS + steps: + - name: "Deploy container instance" + id: "deploycloudlns" + shell: bash + run: | + az login --service-principal -u ${{ secrets.AZURE_SP_CLIENTID }} -p ${{ secrets.AZURE_SP_SECRET }} --tenant ${{ secrets.AZURE_TENANTID }} + az container create --resource-group ${{ secrets.AZURE_RG }} --ip-address Private --location westeurope --name cloudlns \ + --environment-variables LOG_TO_TCP_ADDRESS=${{ needs.certificates_job.outputs.itestupip }} LOG_TO_TCP_PORT=6100 LOG_TO_TCP=true LOG_LEVEL=1 IOTHUBHOSTNAME=${{secrets.IOTHUB_HOSTNAME}} ENABLE_GATEWAY=false CLOUD_DEPLOYMENT=true \ + --image ${{ env.CONTAINER_REGISTRY_ADDRESS }}/lorawannetworksrvmodule:${{needs.build_push_docker_images.outputs.imagetag}}-amd64 \ + --ports 5000 \ + --protocol TCP \ + --registry-username ${{ env.CONTAINER_REGISTRY_USERNAME }} \ + --registry-password ${{ env.CONTAINER_REGISTRY_PASSWORD }} \ + --restart-policy Never \ + --secure-environment-variables FACADE_AUTH_CODE=${{ secrets.FUNCTION_FACADE_AUTH_CODE }} FACADE_SERVER_URL=${{ secrets.FUNCTION_FACADE_SERVER_URL }} REDIS_CONNECTION_STRING=${{ secrets.REDIS_HOSTNAME }}.redis.cache.windows.net:6380,password=${{ secrets.REDIS_PASSWORD }},ssl=True,abortConnect=False \ + --subnet ${{ secrets.AZURE_SUBNET_NAME }} \ + --vnet ${{ secrets.AZURE_VNET_NAME }} --output none + echo "::set-output name=cloudlnsprivateip::$(az container show --name cloudlns --resource-group ${{ secrets.AZURE_RG }} --query ipAddress.ip -o tsv)" + outputs: + cloudlnsprivateip: ${{ steps.deploycloudlns.outputs.cloudlnsprivateip }} + # Deploy IoT Edge solution to ARM gateway deploy_arm_gw_iot_edge: timeout-minutes: 20 @@ -290,7 +338,7 @@ jobs: EDGE_AGENT_VERSION: 1.2.6 EDGE_HUB_VERSION: 1.2.6 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 name: Checkout current branch - name: Iot Edge Push Solution uses: ./.github/actions/deployedge @@ -305,7 +353,7 @@ jobs: IOT_HUB_RESOURCE_ID: ${{ secrets.IOT_HUB_RESOURCE_ID }} LOG_ANALYTICS_WORKSPACE_ID: ${{ secrets.LOG_ANALYTICS_WORKSPACE_ID }} LOG_ANALYTICS_SHARED_KEY: ${{ secrets.LOG_ANALYTICS_SHARED_KEY }} - APPINSIGHTS_INSTRUMENTATIONKEY: ${{ secrets.APPINSIGHTS_INSTRUMENTATIONKEY }} + APPLICATIONINSIGHTS_CONNECTION_STRING: ${{ secrets.APPLICATIONINSIGHTS_CONNECTION_STRING }} CERT_REMOTE_PATH: ${{ needs.env_var.outputs.CertRemotePath }} SERVER_PFX_PASSWORD: ${{ secrets.SERVER_PFX_PASSWORD }} NET_SRV_LOG_TO_TCP_ADDRESS: "itestup" @@ -337,9 +385,9 @@ jobs: IOT_HUB_RESOURCE_ID: ${{ secrets.IOT_HUB_RESOURCE_ID }} LOG_ANALYTICS_WORKSPACE_ID: ${{ secrets.LOG_ANALYTICS_WORKSPACE_ID }} LOG_ANALYTICS_SHARED_KEY: ${{ secrets.LOG_ANALYTICS_SHARED_KEY }} - APPINSIGHTS_INSTRUMENTATIONKEY: ${{ secrets.APPINSIGHTS_INSTRUMENTATIONKEY }} + APPLICATIONINSIGHTS_CONNECTION_STRING: ${{ secrets.APPLICATIONINSIGHTS_CONNECTION_STRING }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 name: Checkout current branch - name: Iot Edge Push Solution @@ -374,7 +422,7 @@ jobs: EDGE_AGENT_VERSION: 1.1.8 EDGE_HUB_VERSION: 1.1.8 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 name: Checkout current branch - name: Iot Edge Push Solution @@ -388,7 +436,7 @@ jobs: IOT_HUB_RESOURCE_ID: ${{ secrets.IOT_HUB_RESOURCE_ID }} LOG_ANALYTICS_WORKSPACE_ID: ${{ secrets.LOG_ANALYTICS_WORKSPACE_ID }} LOG_ANALYTICS_SHARED_KEY: ${{ secrets.LOG_ANALYTICS_SHARED_KEY }} - APPINSIGHTS_INSTRUMENTATIONKEY: ${{ secrets.APPINSIGHTS_INSTRUMENTATIONKEY }} + APPLICATIONINSIGHTS_CONNECTION_STRING: ${{ secrets.APPLICATIONINSIGHTS_CONNECTION_STRING }} with: deployment_file_name: '${{ secrets.DEPLOYMENT_FILE_NAME }}' architecture: '${{ secrets.ARCHITECTURE }}' @@ -401,7 +449,7 @@ jobs: environment: name: CI_AZURE_ENVIRONMENT url: ${{ needs.env_var.outputs.CheckSuiteUrl }} - if: always() && (github.event_name == 'schedule' || github.event.inputs.RunLoadTests == 'true') + if: always() && (github.event_name == 'schedule' || github.event.inputs.RunLoadTests == 'true') && needs.deploy_arm_gw_iot_edge.result == 'success' && needs.deploy_eflow_gw_iot_edge.result == 'success' && needs.deploy_facade_function.result == 'success' && needs.env_var.outputs.StopFullCi != 'true' needs: - deploy_arm_gw_iot_edge - deploy_eflow_gw_iot_edge @@ -419,14 +467,16 @@ jobs: INTEGRATIONTEST_RunningInCI: true INTEGRATIONTEST_LoadTestLnsEndpoints: ${{ secrets.LOAD_TEST_LNS_ENDPOINTS }} INTEGRATIONTEST_NumberOfLoadTestDevices: 10 - INTEGRATIONTEST_NumberOfLoadTestConcentrators: 2 - + INTEGRATIONTEST_NumberOfLoadTestConcentrators: 4 + INTEGRATIONTEST_FunctionAppCode: ${{ secrets.FUNCTION_FACADE_AUTH_CODE }} + INTEGRATIONTEST_FunctionAppBaseUrl: ${{ secrets.FUNCTION_FACADE_SERVER_URL }} + INTEGRATIONTEST_TcpLogPort: 6000 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 name: Checkout current branch - name: Setup .NET - uses: actions/setup-dotnet@v1.8.2 + uses: actions/setup-dotnet@v3.0.1 with: dotnet-version: '6.0.x' # SDK Version to use. @@ -447,16 +497,87 @@ jobs: shell: bash run: | dotnet test --logger trx --no-build --configuration ${{ env.BUILD_CONFIGURATION }} \ - -r ${{ env.TESTS_RESULTS_FOLDER }}/LoadTest/ \ + -r ${{ env.TESTS_RESULTS_FOLDER }}/LoadTest/ --filter "SimulatedLoadTests" \ ${{ env.TESTS_FOLDER }}/Simulation/LoRaWan.Tests.Simulation.csproj # Upload test results as artifact - - uses: actions/upload-artifact@v1 + - uses: actions/upload-artifact@v3 if: always() with: name: load-test-results path: ${{ env.TESTS_RESULTS_FOLDER }}/LoadTest/ + cloud_test_job: + timeout-minutes: 150 + name: Run cloud only deployment Tests + environment: + name: CI_AZURE_ENVIRONMENT + url: ${{ needs.env_var.outputs.CheckSuiteUrl }} + if: always() && needs.deploy_cloud_lns.result == 'success' && needs.deploy_facade_function.result == 'success' && needs.env_var.outputs.StopFullCi != 'true' && contains(needs.env_var.outputs.E2ETestsToRun, 'CloudDeploymentTest') && !(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'CloudDeploymentTest')) + needs: + - deploy_facade_function + - deploy_cloud_lns + - env_var + - certificates_job + runs-on: [ self-hosted, x64 ] + env: + INTEGRATIONTEST_IoTHubEventHubConnectionString: ${{ secrets.IOTHUB_EVENT_HUB_CONNECTION_STRING }} + INTEGRATIONTEST_IoTHubEventHubConsumerGroup: ${{ secrets.IOTHUB_CI_CONSUMER_GROUP }} + INTEGRATIONTEST_IoTHubConnectionString: ${{ secrets.IOTHUB_OWNER_CONNECTION_STRING }} + INTEGRATIONTEST_LeafDeviceGatewayID: itestarm1 + INTEGRATIONTEST_DevicePrefix: '12' + INTEGRATIONTEST_RunningInCI: true + INTEGRATIONTEST_LoadTestLnsEndpoints: "{ \\\"1\\\": \\\"ws://${{ needs.deploy_cloud_lns.outputs.cloudlnsprivateip }}:5000\\\" }" + INTEGRATIONTEST_NumberOfLoadTestDevices: 1 + INTEGRATIONTEST_NumberOfLoadTestConcentrators: 2 + INTEGRATIONTEST_FunctionAppCode: ${{ secrets.FUNCTION_FACADE_AUTH_CODE }} + INTEGRATIONTEST_FunctionAppBaseUrl: ${{ secrets.FUNCTION_FACADE_SERVER_URL }} + INTEGRATIONTEST_TcpLogPort: 6100 + + steps: + - uses: actions/checkout@v2 + name: Checkout current branch + + - name: Setup .NET + uses: actions/setup-dotnet@v3.0.1 + with: + dotnet-version: '6.0.x' # SDK Version to use. + + - name: .NET SDK Information + run: + dotnet --info + + - name: Configuration for simulated cloud tests + uses: cschleiden/replace-tokens@v1 + with: + files: '${{ env.TESTS_FOLDER }}/Simulation/appsettings.json' + + - name: Build simulated cloud tests + run: | + dotnet build --configuration ${{ env.BUILD_CONFIGURATION }} ${{ env.TESTS_FOLDER }}/Simulation/LoRaWan.Tests.Simulation.csproj + + - name: Runs simulated cloud tests + shell: bash + run: | + dotnet test --logger trx --no-build --configuration ${{ env.BUILD_CONFIGURATION }} \ + -r ${{ env.TESTS_RESULTS_FOLDER }}/LoadTest/ --filter "SimulatedCloudTests" \ + ${{ env.TESTS_FOLDER }}/Simulation/LoRaWan.Tests.Simulation.csproj + + - name: Add CloudDeploymentTest Test Label + uses: buildsville/add-remove-label@v1 + if: github.event_name == 'pull_request' && success() + with: + token: ${{ github.token }} + label: 'CloudDeploymentTest' + type: add + + # Upload simulated cloud results as artifact + - uses: actions/upload-artifact@v1 + if: always() + with: + name: simulated-cloud-test-results + path: ${{ env.TESTS_RESULTS_FOLDER }}/LoadTest/ + # Runs E2E tests in dedicated agent, while having modules deployed into PI (arm32v7) e2e_tests_job: timeout-minutes: 150 @@ -494,22 +615,23 @@ jobs: E2ETESTS_DefaultBasicStationEui: ${{ secrets.DEFAULTCISTATIONEUI }} E2ETESTS_RadioDev: ${{ secrets.CIBASICSTATIONRADIODEV }} E2ETESTS_CupsBasicStationEui: ${{ secrets.DEFAULTCUPSSTATIONEUI }} + E2ETESTS_IsCorecellBasicStation: false E2ETESTS_ClientThumbprint: ${{ needs.certificates_job.outputs.clientthumbprint }} E2ETESTS_ClientBundleCrc: ${{ needs.certificates_job.outputs.clientbundlecrc }} E2ETESTS_CupsSigKeyChecksum: ${{ needs.certificates_job.outputs.clientsigkeycrc }} E2ETESTS_CupsFwDigest: ${{ needs.certificates_job.outputs.clientfwdigest }} - E2ETESTS_CupsBasicStationVersion: "2.0.5(rak833x64/std)" + E2ETESTS_CupsBasicStationVersion: "2.0.6(rak833x64/std)" E2ETESTS_CupsBasicStationPackage: ${{ needs.certificates_job.outputs.clientfwversion }} E2ETESTS_CupsFwUrl: ${{ secrets.CUPSFIRMWAREBLOBURL }} TestsToRun: ${{ needs.env_var.outputs.E2ETestsToRun }} E2ETESTS_TxPower: ${{ needs.env_var.outputs.TxPower }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 name: Checkout current branch - name: Setup .NET - uses: actions/setup-dotnet@v1.8.2 + uses: actions/setup-dotnet@v3.0.1 with: dotnet-version: '6.0.x' # SDK Version to use. @@ -602,8 +724,15 @@ jobs: with: test_name: 'CupsTest' + - name: Runs LNS discovery E2E tests + if: always() && contains(env.TestsToRun, 'LnsDiscoveryTest') && !(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'LnsDiscoveryTest')) + uses: ./.github/actions/rune2etest + id: e2e_tests_lnsdiscoverytest + with: + test_name: 'LnsDiscoveryTest' + # Upload test results as artifact - - uses: actions/upload-artifact@v1 + - uses: actions/upload-artifact@v3 if: always() with: name: e2e-test-results @@ -616,8 +745,7 @@ jobs: needs: - e2e_tests_job steps: - - uses: actions/checkout@v2 - + - uses: actions/checkout@v3 - name: Power OFF Azure VM uses: ./.github/actions/power-azure-vm with: @@ -627,3 +755,18 @@ jobs: AZURE_SP_CLIENTID: ${{ secrets.AZURE_SP_CLIENTID }} AZURE_SP_SECRET: ${{ secrets.AZURE_SP_SECRET }} AZURE_TENANTID: ${{ secrets.AZURE_TENANTID }} + + delete_cloud_lns: + name: Delete cloud LNS + if: always() + runs-on: ubuntu-latest + needs: + - e2e_tests_job + steps: + - uses: actions/checkout@v2 + - name: "Delete azure container instance for cloudlns" + shell: bash + run: | + az login --service-principal -u ${{ secrets.AZURE_SP_CLIENTID }} -p ${{ secrets.AZURE_SP_SECRET }} --tenant ${{ secrets.AZURE_TENANTID }} + az container logs -g ${{ secrets.AZURE_RG }} --name cloudlns + az container delete --yes --resource-group ${{ secrets.AZURE_RG }} --name cloudlns --output none diff --git a/.github/workflows/power_eflow_vm.yml b/.github/workflows/power_eflow_vm.yml index e4e4647534..13a8c2821d 100644 --- a/.github/workflows/power_eflow_vm.yml +++ b/.github/workflows/power_eflow_vm.yml @@ -22,7 +22,7 @@ jobs: power_azure_vm: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Power Azure VM uses: ./.github/actions/power-azure-vm diff --git a/.github/workflows/publish-docs-new-version.yml b/.github/workflows/publish-docs-new-version.yml index 9bc958bdfa..e1737a329e 100644 --- a/.github/workflows/publish-docs-new-version.yml +++ b/.github/workflows/publish-docs-new-version.yml @@ -17,8 +17,8 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 with: python-version: 3.x - run: git fetch origin gh-pages --depth=1 diff --git a/.github/workflows/universal_decoder_ci.yaml b/.github/workflows/universal_decoder_ci.yaml index aa8c9e8eb5..2805ba5411 100644 --- a/.github/workflows/universal_decoder_ci.yaml +++ b/.github/workflows/universal_decoder_ci.yaml @@ -33,9 +33,9 @@ jobs: DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} steps: - name: Check out repository code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Use node JS - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: '14' - run: npm install @@ -52,6 +52,7 @@ jobs: matrix: image: - arm32v7 + - arm64v8 - amd64 defaults: run: @@ -60,15 +61,15 @@ jobs: tag: ${{ steps.vars.outputs.tag }} steps: - name: Check out repository code - uses: actions/checkout@v2 - - uses: docker/setup-buildx-action@v1 + uses: actions/checkout@v3 + - uses: docker/setup-buildx-action@v2 id: buildx with: install: true - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Use node JS - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: '14' - run: npm install @@ -96,7 +97,7 @@ jobs: - name: Login to docker registry run: docker login -u ${{ secrets.DOCKER_LOGIN }} -p ${{ secrets.DOCKER_PASSWORD }} - name: Create manifest list - run: docker manifest create ${{ secrets.DOCKER_REPOSITORY }}/universaldecoder:${{needs.Build_And_Push.outputs.tag}} ${{ secrets.DOCKER_REPOSITORY }}/universaldecoder:${{needs.Build_And_Push.outputs.tag}}-arm32v7 ${{ secrets.DOCKER_REPOSITORY }}/universaldecoder:${{needs.Build_And_Push.outputs.tag}}-amd64 --amend + run: docker manifest create ${{ secrets.DOCKER_REPOSITORY }}/universaldecoder:${{needs.Build_And_Push.outputs.tag}} ${{ secrets.DOCKER_REPOSITORY }}/universaldecoder:${{needs.Build_And_Push.outputs.tag}}-arm32v7 ${{ secrets.DOCKER_REPOSITORY }}/universaldecoder:${{needs.Build_And_Push.outputs.tag}}-amd64 ${{ secrets.DOCKER_REPOSITORY }}/universaldecoder:${{needs.Build_And_Push.outputs.tag}}-arm64v8 --amend - name: Push manifest run: docker manifest push ${{ secrets.DOCKER_REPOSITORY }}/universaldecoder:${{needs.Build_And_Push.outputs.tag}} diff --git a/Arduino/AS923-3RP1/TransmissionTestABPLoRa/TransmissionTestABPLoRa.ino b/Arduino/AS923-3RP1/TransmissionTestABPLoRa/TransmissionTestABPLoRa.ino new file mode 100644 index 0000000000..5d4d3ca339 --- /dev/null +++ b/Arduino/AS923-3RP1/TransmissionTestABPLoRa/TransmissionTestABPLoRa.ino @@ -0,0 +1,105 @@ + +#include +// The current sample is using AS923-3 on three different frequencies +// Refer to the repo documentation to adapt it to other AS923 variations +//set to true to send confirmed data up messages +bool confirmed = true; +//application information, should be similar to what was provisiionned in the device twins +char * deviceId = "46AAC86800430028"; +char * devAddr = "0228B1B1"; +char * appSKey = "2B7E151628AED2A6ABF7158809CF4F3C"; +char * nwkSKey = "3B7E151628AED2A6ABF7158809CF4F3C"; + + +/* + iot hub ABP tags for deviceid: 46AAC86800430028 + "desired": { + "AppSKey": "2B7E151628AED2A6ABF7158809CF4F3C", + "NwkSKey": "3B7E151628AED2A6ABF7158809CF4F3C", + "DevAddr": "0228B1B1", + "GatewayID" :"", + "SensorDecoder" :"DecoderValueSensor" + }, +*/ + +//set initial datarate and physical information for the device +_data_rate_t dr = DR5; +_physical_type_t physicalType = AS923 ; + +//internal variables +char data[10]; +char buffer[256]; +int i = 0; +int lastCall = 0; + + +void setup(void) +{ + SerialUSB.begin(115200); + while (!SerialUSB); + lora.init(); + lora.setDeviceDefault(); + + lora.setId(devAddr, deviceId, NULL); + lora.setKey(nwkSKey, appSKey, NULL); + + lora.setDeciveMode(LWABP); + lora.setDataRate(dr, physicalType); + lora.setChannel(0, 916.6); + lora.setChannel(1, 916.8); + lora.setChannel(2, 917.0); + + lora.setReceiceWindowFirst(2, 917.4); + + lora.setAdaptiveDataRate(false); + + lora.setDutyCycle(false); + lora.setJoinDutyCycle(false); + + + lora.setPower(14); + + +} + +void loop(void) +{ + if ((millis() - lastCall) > 5000) { + lastCall = millis(); + bool result = false; + String packetString = ""; + packetString = String(i); + SerialUSB.println(packetString); + packetString.toCharArray(data, 10); + + if (confirmed) + result = lora.transferPacketWithConfirmed(data, 10); + else + result = lora.transferPacket(data, 10); + i++; + + if (result) + { + short length; + short rssi; + + memset(buffer, 0, sizeof(buffer)); + length = lora.receivePacket(buffer, sizeof(buffer), &rssi); + + if (length) + { + SerialUSB.print("Length is: "); + SerialUSB.println(length); + SerialUSB.print("RSSI is: "); + SerialUSB.println(rssi); + SerialUSB.print("Data is: "); + for (unsigned char i = 0; i < length; i ++) + { + SerialUSB.print( char(buffer[i])); + + } + SerialUSB.println(); + } + } + } +} diff --git a/Arduino/AS923-3RP1/TransmissionTestOTAALoRa/TransmissionTestOTAALoRa.ino b/Arduino/AS923-3RP1/TransmissionTestOTAALoRa/TransmissionTestOTAALoRa.ino new file mode 100644 index 0000000000..40dd403fa3 --- /dev/null +++ b/Arduino/AS923-3RP1/TransmissionTestOTAALoRa/TransmissionTestOTAALoRa.ino @@ -0,0 +1,98 @@ + +#include +// The current sample is using AS923-3 on three different frequencies +// Refer to the repo documentation to adapt it to other AS923 variations +// set to true to send confirmed data up messages +bool confirmed=true; +// application information, should be similar to what was provisioned in the device twins +char * deviceId ="47AAC86800430028"; +char * appKey="8AFE71A145B253E49C3031AD068277A1"; +char * appEui ="BE7A0000000014E2"; + +/* +iot hub OTAA tags for deviceid: 47AAC86800430028 + "desired": { + "AppEUI": "BE7A0000000014E2", + "AppKey": "8AFE71A145B253E49C3031AD068277A1", + "GatewayID" :"", + "SensorDecoder" :"DecoderValueSensor" + }, + */ + +// set initial datarate and physical information for the device +_data_rate_t dr=DR5; +_physical_type_t physicalType=AS923 ; + +// internal variables +char data[10]; +char buffer[256]; +int i=0; +int lastCall=0; + +void setup(void) +{ + SerialUSB.begin(115200); + while(!SerialUSB); + lora.init(); + lora.setDeviceDefault(); + delay(1000); + lora.setId(NULL,deviceId , appEui); + lora.setKey(NULL, NULL, appKey); + + lora.setDeciveMode(LWOTAA); + lora.setDataRate(dr, physicalType); + + lora.setChannel(0, 916.6); + lora.setChannel(1, 916.8); + lora.setChannel(2, 917.0); + + lora.setReceiceWindowFirst(2, 917.4); + + lora.setAdaptiveDataRate(false); + lora.setDutyCycle(false); + lora.setJoinDutyCycle(false); + lora.setPower(2); + + while(!lora.setOTAAJoin(JOIN,20000)); +} + +void loop(void) +{ + if((millis()-lastCall)>5000){ + lastCall=millis(); + bool result = false; + String packetString = ""; + packetString=String(i); + SerialUSB.println(packetString); + packetString.toCharArray(data, 10); + + if(confirmed) + result = lora.transferPacketWithConfirmed(data, 10); + else + result = lora.transferPacket(data, 10); + i++; + + if(result) + { + short length; + short rssi; + + memset(buffer, 0, sizeof(buffer)); + length = lora.receivePacket(buffer, sizeof(buffer), &rssi); + + if(length) + { + SerialUSB.print("Length is: "); + SerialUSB.println(length); + SerialUSB.print("RSSI is: "); + SerialUSB.println(rssi); + SerialUSB.print("Data is: "); + for(unsigned char i = 0; i < length; i ++) + { + SerialUSB.print( char(buffer[i])); + } + SerialUSB.println(); + } + } + } +} diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs index 973d3c6301..124162d932 100644 --- a/AssemblyInfo.cs +++ b/AssemblyInfo.cs @@ -8,5 +8,8 @@ [assembly: InternalsVisibleTo("LoRaTools")] [assembly: InternalsVisibleTo("LoRaWan.NetworkServer")] [assembly: InternalsVisibleTo("LoRaWan.Tests.Unit")] +[assembly: InternalsVisibleTo("LoRaWan.Tests.Integration")] +[assembly: InternalsVisibleTo("LoRaWan.Tests.E2E")] [assembly: InternalsVisibleTo("LoRaWan.Tests.Common")] +[assembly: InternalsVisibleTo("LoRaWan.Tests.Simulation")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/Directory.Build.props b/Directory.Build.props index edd99bafab..00c26d9b97 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,43 +10,44 @@ net6.0 - 3.125.2 - 1.36.0 - 1.39.0 - 6.0.0 + 3.125.12 + 1.38.1 + 1.41.2 + 6.0.1 1.1.0 6.0.0 6.0.0 6.0.0 - 4.0.1 + 4.1.3 13.0.1 1.9.0 4.3.0 - 2.2.88 + 2.6.66 4.7.0 - 2.20.0 - 5.0.2 + 2.21.0 + 6.0.0 + 1.6.1 + 2.2.0 3.125.5 - 17.0.0 - 4.16.1 - 2.4.1 + 17.3.2 + 4.18.2 + 2.4.2 1.4.1 - 6.0.0 + 6.0.1 6.0.0 - 6.0.0 + 6.0.1 6.0.0 6.0.0 - 2.4.3 - 3.1.0 - 4.3.2 - 4.3.2 + 2.4.5 + 3.1.2 6.0.0 6.0.0 - 34.0.1 + 34.0.2 1.3.0 - 6.0.1 + 6.0.9 + 5.7.2 diff --git a/LoRaEngine.sln b/LoRaEngine.sln index 1066309314..630df24d17 100644 --- a/LoRaEngine.sln +++ b/LoRaEngine.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29025.244 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32126.317 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoraKeysManagerFacade", "LoRaEngine\LoraKeysManagerFacade\LoraKeysManagerFacade.csproj", "{7E3FDAAC-242D-40CD-9782-0A8487FD0070}" EndProject @@ -10,8 +10,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoRaWan.NetworkServer", "Lo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoRaWanNetworkSrvModule", "LoRaEngine\modules\LoRaWanNetworkSrvModule\LoRaWanNetworkSrvModule\LoRaWanNetworkSrvModule.csproj", "{B563A551-BF18-4A79-804F-6E8A3AB31990}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logger", "LoRaEngine\modules\LoRaWanNetworkSrvModule\Logger\Logger.csproj", "{0F8CDB3B-34C9-4C14-B4B2-34BCAD232D45}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoRaWan", "LoRaEngine\modules\LoRaWanNetworkSrvModule\LoRaWan\LoRaWan.csproj", "{E38EDA03-9026-4528-8B4E-3ACA37373E69}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{67C7C34B-B11C-4E10-9D97-728FB122683F}" @@ -36,6 +34,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{A3AF6806 Tests\.editorconfig = Tests\.editorconfig EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoRaWan.NetworkServerDiscovery", "LoRaEngine\modules\LoRaWan.NetworkServerDiscovery\LoRaWan.NetworkServerDiscovery.csproj", "{CF2A693D-59FF-490D-934C-8D7AD552A030}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -76,22 +76,6 @@ Global {B563A551-BF18-4A79-804F-6E8A3AB31990}.Release|Any CPU.Build.0 = Release|Any CPU {B563A551-BF18-4A79-804F-6E8A3AB31990}.Release|x86.ActiveCfg = Release|Any CPU {B563A551-BF18-4A79-804F-6E8A3AB31990}.Release|x86.Build.0 = Release|Any CPU - {0F8CDB3B-34C9-4C14-B4B2-34BCAD232D45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0F8CDB3B-34C9-4C14-B4B2-34BCAD232D45}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0F8CDB3B-34C9-4C14-B4B2-34BCAD232D45}.Debug|x86.ActiveCfg = Debug|Any CPU - {0F8CDB3B-34C9-4C14-B4B2-34BCAD232D45}.Debug|x86.Build.0 = Debug|Any CPU - {0F8CDB3B-34C9-4C14-B4B2-34BCAD232D45}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0F8CDB3B-34C9-4C14-B4B2-34BCAD232D45}.Release|Any CPU.Build.0 = Release|Any CPU - {0F8CDB3B-34C9-4C14-B4B2-34BCAD232D45}.Release|x86.ActiveCfg = Release|Any CPU - {0F8CDB3B-34C9-4C14-B4B2-34BCAD232D45}.Release|x86.Build.0 = Release|Any CPU - {4FA0718D-15A2-4D4E-9310-7FB17B285757}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4FA0718D-15A2-4D4E-9310-7FB17B285757}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4FA0718D-15A2-4D4E-9310-7FB17B285757}.Debug|x86.ActiveCfg = Debug|Any CPU - {4FA0718D-15A2-4D4E-9310-7FB17B285757}.Debug|x86.Build.0 = Debug|Any CPU - {4FA0718D-15A2-4D4E-9310-7FB17B285757}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4FA0718D-15A2-4D4E-9310-7FB17B285757}.Release|Any CPU.Build.0 = Release|Any CPU - {4FA0718D-15A2-4D4E-9310-7FB17B285757}.Release|x86.ActiveCfg = Release|Any CPU - {4FA0718D-15A2-4D4E-9310-7FB17B285757}.Release|x86.Build.0 = Release|Any CPU {E38EDA03-9026-4528-8B4E-3ACA37373E69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E38EDA03-9026-4528-8B4E-3ACA37373E69}.Debug|Any CPU.Build.0 = Debug|Any CPU {E38EDA03-9026-4528-8B4E-3ACA37373E69}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -108,6 +92,14 @@ Global {0A3CCF9E-46E8-4901-8C9A-BCFC0EBD213B}.Release|Any CPU.Build.0 = Release|Any CPU {0A3CCF9E-46E8-4901-8C9A-BCFC0EBD213B}.Release|x86.ActiveCfg = Release|Any CPU {0A3CCF9E-46E8-4901-8C9A-BCFC0EBD213B}.Release|x86.Build.0 = Release|Any CPU + {D4595D27-2318-4C8F-AD64-D42C6CD70626}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4595D27-2318-4C8F-AD64-D42C6CD70626}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4595D27-2318-4C8F-AD64-D42C6CD70626}.Debug|x86.ActiveCfg = Debug|Any CPU + {D4595D27-2318-4C8F-AD64-D42C6CD70626}.Debug|x86.Build.0 = Debug|Any CPU + {D4595D27-2318-4C8F-AD64-D42C6CD70626}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4595D27-2318-4C8F-AD64-D42C6CD70626}.Release|Any CPU.Build.0 = Release|Any CPU + {D4595D27-2318-4C8F-AD64-D42C6CD70626}.Release|x86.ActiveCfg = Release|Any CPU + {D4595D27-2318-4C8F-AD64-D42C6CD70626}.Release|x86.Build.0 = Release|Any CPU {8F615E91-40E2-4BF4-8573-577CEC7701FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8F615E91-40E2-4BF4-8573-577CEC7701FF}.Debug|Any CPU.Build.0 = Debug|Any CPU {8F615E91-40E2-4BF4-8573-577CEC7701FF}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -124,14 +116,22 @@ Global {A871D30D-2634-4321-A0EB-B116A1BA1CD3}.Release|Any CPU.Build.0 = Release|Any CPU {A871D30D-2634-4321-A0EB-B116A1BA1CD3}.Release|x86.ActiveCfg = Release|Any CPU {A871D30D-2634-4321-A0EB-B116A1BA1CD3}.Release|x86.Build.0 = Release|Any CPU - {D4595D27-2318-4C8F-AD64-D42C6CD70626}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D4595D27-2318-4C8F-AD64-D42C6CD70626}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D4595D27-2318-4C8F-AD64-D42C6CD70626}.Debug|x86.ActiveCfg = Debug|Any CPU - {D4595D27-2318-4C8F-AD64-D42C6CD70626}.Debug|x86.Build.0 = Debug|Any CPU - {D4595D27-2318-4C8F-AD64-D42C6CD70626}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D4595D27-2318-4C8F-AD64-D42C6CD70626}.Release|Any CPU.Build.0 = Release|Any CPU - {D4595D27-2318-4C8F-AD64-D42C6CD70626}.Release|x86.ActiveCfg = Release|Any CPU - {D4595D27-2318-4C8F-AD64-D42C6CD70626}.Release|x86.Build.0 = Release|Any CPU + {4FA0718D-15A2-4D4E-9310-7FB17B285757}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4FA0718D-15A2-4D4E-9310-7FB17B285757}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4FA0718D-15A2-4D4E-9310-7FB17B285757}.Debug|x86.ActiveCfg = Debug|Any CPU + {4FA0718D-15A2-4D4E-9310-7FB17B285757}.Debug|x86.Build.0 = Debug|Any CPU + {4FA0718D-15A2-4D4E-9310-7FB17B285757}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4FA0718D-15A2-4D4E-9310-7FB17B285757}.Release|Any CPU.Build.0 = Release|Any CPU + {4FA0718D-15A2-4D4E-9310-7FB17B285757}.Release|x86.ActiveCfg = Release|Any CPU + {4FA0718D-15A2-4D4E-9310-7FB17B285757}.Release|x86.Build.0 = Release|Any CPU + {CF2A693D-59FF-490D-934C-8D7AD552A030}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF2A693D-59FF-490D-934C-8D7AD552A030}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF2A693D-59FF-490D-934C-8D7AD552A030}.Debug|x86.ActiveCfg = Debug|Any CPU + {CF2A693D-59FF-490D-934C-8D7AD552A030}.Debug|x86.Build.0 = Debug|Any CPU + {CF2A693D-59FF-490D-934C-8D7AD552A030}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF2A693D-59FF-490D-934C-8D7AD552A030}.Release|Any CPU.Build.0 = Release|Any CPU + {CF2A693D-59FF-490D-934C-8D7AD552A030}.Release|x86.ActiveCfg = Release|Any CPU + {CF2A693D-59FF-490D-934C-8D7AD552A030}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/LoRaEngine/LoraKeysManagerFacade/ RedisChannelPublisher.cs b/LoRaEngine/LoraKeysManagerFacade/ RedisChannelPublisher.cs new file mode 100644 index 0000000000..a871d3f4fb --- /dev/null +++ b/LoRaEngine/LoraKeysManagerFacade/ RedisChannelPublisher.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoraKeysManagerFacade +{ + using StackExchange.Redis; + using System.Threading.Tasks; + using Microsoft.Extensions.Logging; + using LoRaTools; + using System.Text.Json; + + public class RedisChannelPublisher : IChannelPublisher + { + private readonly ConnectionMultiplexer redis; + private readonly ILogger logger; + + public RedisChannelPublisher(ConnectionMultiplexer redis, ILogger logger) + { + this.redis = redis; + this.logger = logger; + } + + public async Task PublishAsync(string channel, LnsRemoteCall lnsRemoteCall) + { + this.logger.LogDebug("Publishing message to channel '{Channel}'.", channel); + _ = await this.redis.GetSubscriber().PublishAsync(channel, JsonSerializer.Serialize(lnsRemoteCall)); + } + } +} diff --git a/LoRaEngine/LoraKeysManagerFacade/CheckPreferredGateway/PreferredGatewayExecutionItem.cs b/LoRaEngine/LoraKeysManagerFacade/CheckPreferredGateway/PreferredGatewayExecutionItem.cs index eda178b369..f57670ec81 100644 --- a/LoRaEngine/LoraKeysManagerFacade/CheckPreferredGateway/PreferredGatewayExecutionItem.cs +++ b/LoRaEngine/LoraKeysManagerFacade/CheckPreferredGateway/PreferredGatewayExecutionItem.cs @@ -101,7 +101,7 @@ private async Task ComputePreferredGateway(IPipelineExec { if (preferredGateway.FcntUp >= fcntUp) { - return new PreferredGatewayResult(devEUI, fcntUp, preferredGateway); + return new PreferredGatewayResult(fcntUp, preferredGateway); } } @@ -126,7 +126,7 @@ private async Task ComputePreferredGateway(IPipelineExec { this.log.LogError("Could not resolve closest gateway in {devEUI} and {fcntUp}", devEUI, fcntUp); - return new PreferredGatewayResult(devEUI, fcntUp, "Could not resolve closest gateway"); + return new PreferredGatewayResult(fcntUp, "Could not resolve closest gateway"); } preferredGateway = new LoRaDevicePreferredGateway(winner.GatewayID, fcntUp); @@ -155,13 +155,13 @@ private async Task ComputePreferredGateway(IPipelineExec { if (preferredGateway.FcntUp >= fcntUp) { - return new PreferredGatewayResult(devEUI, fcntUp, preferredGateway); + return new PreferredGatewayResult(fcntUp, preferredGateway); } } } this.log.LogError("Could not resolve closest gateway in {devEUI} and {fcntUp}", devEUI, fcntUp); - return new PreferredGatewayResult(devEUI, fcntUp, "Could not resolve closest gateway"); + return new PreferredGatewayResult(fcntUp, "Could not resolve closest gateway"); } } } diff --git a/LoRaEngine/LoraKeysManagerFacade/CheckPreferredGateway/PreferredGatewayResult.cs b/LoRaEngine/LoraKeysManagerFacade/CheckPreferredGateway/PreferredGatewayResult.cs index 8e36240394..6f0d5af3f0 100644 --- a/LoRaEngine/LoraKeysManagerFacade/CheckPreferredGateway/PreferredGatewayResult.cs +++ b/LoRaEngine/LoraKeysManagerFacade/CheckPreferredGateway/PreferredGatewayResult.cs @@ -3,7 +3,6 @@ namespace LoraKeysManagerFacade { - using LoRaWan; using Newtonsoft.Json; using System; @@ -12,8 +11,6 @@ namespace LoraKeysManagerFacade /// public class PreferredGatewayResult { - public DevEui DevEUI { get; } - public uint RequestFcntUp { get; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] @@ -38,20 +35,18 @@ public PreferredGatewayResult() { } - public PreferredGatewayResult(DevEui devEUI, uint fcntUp, LoRaDevicePreferredGateway preferredGateway) + public PreferredGatewayResult(uint fcntUp, LoRaDevicePreferredGateway preferredGateway) { if (preferredGateway is null) throw new ArgumentNullException(nameof(preferredGateway)); - DevEUI = devEUI; RequestFcntUp = fcntUp; CurrentFcntUp = preferredGateway.FcntUp; PreferredGatewayID = preferredGateway.GatewayID; Conflict = fcntUp != preferredGateway.FcntUp; } - public PreferredGatewayResult(DevEui devEUI, uint fcntUp, string errorMessage) + public PreferredGatewayResult(uint fcntUp, string errorMessage) { - DevEUI = devEUI; RequestFcntUp = fcntUp; ErrorMessage = errorMessage; } diff --git a/LoRaEngine/LoraKeysManagerFacade/ClearLnsCache.cs b/LoRaEngine/LoraKeysManagerFacade/ClearLnsCache.cs new file mode 100644 index 0000000000..e7ad344af0 --- /dev/null +++ b/LoRaEngine/LoraKeysManagerFacade/ClearLnsCache.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoraKeysManagerFacade +{ + using System; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using LoRaTools; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Azure.Devices; + using Microsoft.Azure.WebJobs; + using Microsoft.Azure.WebJobs.Extensions.Http; + using Microsoft.Extensions.Logging; + + public sealed class ClearLnsCache + { + private readonly IEdgeDeviceGetter edgeDeviceGetter; + private readonly IServiceClient serviceClient; + private readonly IChannelPublisher channelPublisher; + private readonly ILogger logger; + + public ClearLnsCache(IEdgeDeviceGetter edgeDeviceGetter, + IServiceClient serviceClient, + IChannelPublisher channelPublisher, + ILogger logger) + { + this.edgeDeviceGetter = edgeDeviceGetter; + this.serviceClient = serviceClient; + this.channelPublisher = channelPublisher; + this.logger = logger; + } + + [FunctionName(nameof(ClearNetworkServerCache))] + public async Task ClearNetworkServerCache([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, CancellationToken cancellationToken) + { + if (req is null) throw new ArgumentNullException(nameof(req)); + + try + { + VersionValidator.Validate(req); + } + catch (IncompatibleVersionException ex) + { + return new BadRequestObjectResult(ex.Message); + } + + await ClearLnsCacheInternalAsync(cancellationToken); + + return new AcceptedResult(); + } + + internal async Task ClearLnsCacheInternalAsync(CancellationToken cancellationToken) + { + this.logger.LogInformation("Clearing device cache for all edge and Pub/Sub channel based Network Servers."); + // Edge device discovery for invoking direct methods + var edgeDevices = await this.edgeDeviceGetter.ListEdgeDevicesAsync(cancellationToken); + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("Invoking clear cache direct method for following devices: {deviceList}", string.Join(',', edgeDevices)); + } + var tasks = edgeDevices.Select(e => InvokeClearViaDirectMethodAsync(e, cancellationToken)).ToArray(); + // Publishing a single message for all cloud based LNSes + await PublishClearMessageAsync(); + await Task.WhenAll(tasks); + } + + internal async Task PublishClearMessageAsync() + { + await this.channelPublisher.PublishAsync(LoraKeysManagerFacadeConstants.ClearCacheMethodName, new LnsRemoteCall(RemoteCallKind.ClearCache, null)); + this.logger.LogInformation("Cache clear message published on Pub/Sub channel"); + } + + internal async Task InvokeClearViaDirectMethodAsync(string lnsId, CancellationToken cancellationToken) + { + //Reason why the yield is needed is to avoid any potential "synchronous" code that might fail the publishing of a message on the pub/sub channel + await Task.Yield(); + var res = await this.serviceClient.InvokeDeviceMethodAsync(lnsId, + Constants.NetworkServerModuleId, + new CloudToDeviceMethod(LoraKeysManagerFacadeConstants.ClearCacheMethodName), + cancellationToken); + if (HttpUtilities.IsSuccessStatusCode(res.Status)) + { + this.logger.LogInformation("Cache cleared for {gatewayID} via direct method", lnsId); + } + else + { + throw new InvalidOperationException($"Direct method call to {lnsId} failed with {res.Status}. Response: {res.GetPayloadAsJson()}"); + } + } + } +} diff --git a/LoRaEngine/LoraKeysManagerFacade/ConcentratorCredentialsFunction.cs b/LoRaEngine/LoraKeysManagerFacade/ConcentratorCredentialsFunction.cs index 3282115d95..9ccc040fa2 100644 --- a/LoRaEngine/LoraKeysManagerFacade/ConcentratorCredentialsFunction.cs +++ b/LoRaEngine/LoraKeysManagerFacade/ConcentratorCredentialsFunction.cs @@ -11,12 +11,12 @@ namespace LoraKeysManagerFacade using System.Threading.Tasks; using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; + using LoRaTools; using LoRaTools.CommonAPI; using LoRaTools.Utils; using LoRaWan; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; - using Microsoft.Azure.Devices; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Extensions.Azure; @@ -30,11 +30,11 @@ public class ConcentratorCredentialsFunction internal const string CupsPropertyName = "cups"; internal const string CupsCredentialsUrlPropertyName = "cupsCredentialUrl"; internal const string LnsCredentialsUrlPropertyName = "tcCredentialUrl"; - private readonly RegistryManager registryManager; + private readonly IDeviceRegistryManager registryManager; private readonly IAzureClientFactory azureClientFactory; private readonly ILogger logger; - public ConcentratorCredentialsFunction(RegistryManager registryManager, + public ConcentratorCredentialsFunction(IDeviceRegistryManager registryManager, IAzureClientFactory azureClientFactory, ILogger logger) { @@ -70,6 +70,8 @@ internal async Task RunFetchConcentratorCredentials(HttpRequest r return new BadRequestObjectResult("StationEui missing in request or invalid"); } + using var stationScope = this.logger.BeginEuiScope(stationEui); + var credentialTypeQueryString = req.Query["CredentialType"]; if (StringValues.IsNullOrEmpty(credentialTypeQueryString)) { diff --git a/LoRaEngine/LoraKeysManagerFacade/ConcentratorFirmwareFunction.cs b/LoRaEngine/LoraKeysManagerFacade/ConcentratorFirmwareFunction.cs index c0fc4c0b36..9b99a88345 100644 --- a/LoRaEngine/LoraKeysManagerFacade/ConcentratorFirmwareFunction.cs +++ b/LoRaEngine/LoraKeysManagerFacade/ConcentratorFirmwareFunction.cs @@ -11,11 +11,11 @@ namespace LoraKeysManagerFacade using System.Threading.Tasks; using Azure; using Azure.Storage.Blobs; + using LoRaTools; using LoRaTools.Utils; using LoRaWan; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; - using Microsoft.Azure.Devices; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Extensions.Azure; @@ -28,11 +28,11 @@ public class ConcentratorFirmwareFunction internal const string CupsPropertyName = "cups"; internal const string CupsFwUrlPropertyName = "fwUrl"; - private readonly RegistryManager registryManager; + private readonly IDeviceRegistryManager registryManager; private readonly IAzureClientFactory azureClientFactory; private readonly ILogger logger; - public ConcentratorFirmwareFunction(RegistryManager registryManager, + public ConcentratorFirmwareFunction(IDeviceRegistryManager registryManager, IAzureClientFactory azureClientFactory, ILogger logger) { @@ -68,6 +68,8 @@ internal async Task RunFetchConcentratorFirmware(HttpRequest req, return new BadRequestObjectResult("StationEui missing in request or invalid"); } + using var stationScope = this.logger.BeginEuiScope(stationEui); + var twin = await this.registryManager.GetTwinAsync(stationEui.ToString("N", CultureInfo.InvariantCulture), cancellationToken); if (twin != null) { @@ -90,9 +92,8 @@ internal async Task RunFetchConcentratorFirmware(HttpRequest req, StatusCode = (int)HttpStatusCode.InternalServerError, }; } - catch (RequestFailedException ex) + catch (RequestFailedException ex) when (ExceptionFilterUtility.True(() => this.logger.LogError(ex, "Failed to download firmware from storage."))) { - this.logger.LogError(ex, "Failed to download firmware from storage."); return new ObjectResult("Failed to download firmware") { StatusCode = (int)HttpStatusCode.InternalServerError diff --git a/LoRaEngine/LoraKeysManagerFacade/CreateEdgeDevice.cs b/LoRaEngine/LoraKeysManagerFacade/CreateEdgeDevice.cs index ead3147481..4772062e12 100644 --- a/LoRaEngine/LoraKeysManagerFacade/CreateEdgeDevice.cs +++ b/LoRaEngine/LoraKeysManagerFacade/CreateEdgeDevice.cs @@ -4,31 +4,26 @@ namespace LoraKeysManagerFacade { using System; - using System.Collections.Generic; using System.Net; using System.Net.Http; - using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; + using LoRaTools; using LoRaWan; using Microsoft.AspNetCore.Http; - using Microsoft.Azure.Devices; - using Microsoft.Azure.Devices.Shared; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Extensions.Logging; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; public class CreateEdgeDevice { - private const string AbpDeviceId = "46AAC86800430028"; - private const string OtaaDeviceId = "47AAC86800430028"; - private readonly RegistryManager registryManager; + private readonly IDeviceRegistryManager registryManager; + private readonly IHttpClientFactory httpClientFactory; - public CreateEdgeDevice(RegistryManager registryManager) + public CreateEdgeDevice(IDeviceRegistryManager registryManager, IHttpClientFactory httpClientFactory) { this.registryManager = registryManager; + this.httpClientFactory = httpClientFactory; } [FunctionName(nameof(CreateEdgeDevice))] @@ -61,120 +56,17 @@ public CreateEdgeDevice(RegistryManager registryManager) _ = bool.TryParse(Environment.GetEnvironmentVariable("DEPLOY_DEVICE"), out var deployEndDevice); - // Get function facade key - var base64Auth = Convert.ToBase64String(Encoding.Default.GetBytes($"{publishingUserName}:{publishingPassword}")); - var apiUrl = new Uri($"https://{Environment.GetEnvironmentVariable("WEBSITE_CONTENTSHARE")}.scm.azurewebsites.net/api"); - var siteUrl = new Uri($"https://{Environment.GetEnvironmentVariable("WEBSITE_CONTENTSHARE")}.azurewebsites.net"); - string jwt; - using (var client = new HttpClient()) - { - client.DefaultRequestHeaders.Add("Authorization", $"Basic {base64Auth}"); - var result = await client.GetAsync(new Uri($"{apiUrl}/functions/admin/token")); - jwt = (await result.Content.ReadAsStringAsync()).Trim('"'); // get JWT for call funtion key - } - - var facadeKey = string.Empty; - using (var client = new HttpClient()) - { - client.DefaultRequestHeaders.Add("Authorization", "Bearer " + jwt); - - var response = await client.GetAsync(new Uri($"{siteUrl}/admin/host/keys")); - var jsonResult = await response.Content.ReadAsStringAsync(); - dynamic resObject = JsonConvert.DeserializeObject(jsonResult); - facadeKey = resObject.keys[0].value; - } - - var edgeGatewayDevice = new Device(deviceName) - { - Capabilities = new DeviceCapabilities() - { - IotEdge = true - } - }; - try { - _ = await this.registryManager.AddDeviceAsync(edgeGatewayDevice); - _ = await this.registryManager.AddModuleAsync(new Module(deviceName, "LoRaWanNetworkSrvModule")); - - static async Task GetConfigurationContentAsync(Uri configLocation, IDictionary tokenReplacements) - { - using var httpClient = new HttpClient(); - var json = await httpClient.GetStringAsync(configLocation); - foreach (var r in tokenReplacements) - json = json.Replace(r.Key, r.Value, StringComparison.Ordinal); - return JsonConvert.DeserializeObject(json); - } - - var deviceConfigurationContent = await GetConfigurationContentAsync(new Uri(Environment.GetEnvironmentVariable("DEVICE_CONFIG_LOCATION")), new Dictionary - { - ["[$reset_pin]"] = resetPin, - ["[$spi_speed]"] = string.IsNullOrEmpty(spiSpeed) || string.Equals(spiSpeed, "8", StringComparison.OrdinalIgnoreCase) ? string.Empty : ",'SPI_SPEED':{'value':'2'}", - ["[$spi_dev]"] = string.IsNullOrEmpty(spiDev) || string.Equals(spiDev, "0", StringComparison.OrdinalIgnoreCase) ? string.Empty : $",'SPI_DEV':{{'value':'{spiDev}'}}" - }); + await this.registryManager.DeployEdgeDeviceAsync(deviceName, resetPin, spiSpeed, spiDev, publishingUserName, publishingPassword); - await this.registryManager.ApplyConfigurationContentOnDeviceAsync(deviceName, deviceConfigurationContent); - - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("LOG_ANALYTICS_WORKSPACE_ID"))) - { - log.LogDebug("Opted-in to use Azure Monitor on the edge. Deploying the observability layer."); - // If Appinsights Key is set this means that user opted in to use Azure Monitor. - _ = await this.registryManager.AddModuleAsync(new Module(deviceName, "IotHubMetricsCollectorModule")); - var observabilityConfigurationContent = await GetConfigurationContentAsync(new Uri(Environment.GetEnvironmentVariable("OBSERVABILITY_CONFIG_LOCATION")), new Dictionary - { - ["[$iot_hub_resource_id]"] = Environment.GetEnvironmentVariable("IOT_HUB_RESOURCE_ID"), - ["[$log_analytics_workspace_id]"] = Environment.GetEnvironmentVariable("LOG_ANALYTICS_WORKSPACE_ID"), - ["[$log_analytics_shared_key]"] = Environment.GetEnvironmentVariable("LOG_ANALYTICS_WORKSPACE_KEY") - }); - - _ = await this.registryManager.AddConfigurationAsync(new Configuration($"obs-{Guid.NewGuid()}") - { - Content = observabilityConfigurationContent, - TargetCondition = $"deviceId='{deviceName}'" - }); - } - - var twin = new Twin(); - twin.Properties.Desired = new TwinCollection($"{{FacadeServerUrl:'https://{GetEnvironmentVariable("FACADE_HOST_NAME")}.azurewebsites.net/api/',FacadeAuthCode: '{facadeKey}'}}"); - var remoteTwin = await this.registryManager.GetTwinAsync(deviceName); - - _ = await this.registryManager.UpdateTwinAsync(deviceName, "LoRaWanNetworkSrvModule", twin, remoteTwin.ETag); - - // Deploy concentrator - using var httpClient = new HttpClient(); - var regionalConfiguration = region switch - { - var s when string.Equals("EU", s, StringComparison.OrdinalIgnoreCase) => await httpClient.GetStringAsync(new Uri(GetEnvironmentVariable("EU863_CONFIG_LOCATION"))), - var s when string.Equals("US", s, StringComparison.OrdinalIgnoreCase) => await httpClient.GetStringAsync(new Uri(GetEnvironmentVariable("US902_CONFIG_LOCATION"))), - _ => throw new SwitchExpressionException("Region should be either 'EU' or 'US'") - }; - - var concentratorDevice = new Device(stationEuiString); - _ = await this.registryManager.AddDeviceAsync(concentratorDevice); - var concentratorTwin = await this.registryManager.GetTwinAsync(stationEuiString); - var concentratorJObject = JsonConvert.DeserializeObject(regionalConfiguration); - concentratorTwin.Properties.Desired["routerConfig"] = concentratorJObject; - _ = await this.registryManager.UpdateTwinAsync(stationEuiString, concentratorTwin, concentratorTwin.ETag); + await this.registryManager.DeployConcentratorAsync(stationEuiString, region); // This section will get deployed ONLY if the user selected the "deploy end device" options. // Information in this if clause, is for demo purpose only and should not be used for productive workloads. if (deployEndDevice) { - var otaaDevice = new Device(OtaaDeviceId); - - _ = await this.registryManager.AddDeviceAsync(otaaDevice); - - var otaaEndTwin = new Twin(); - otaaEndTwin.Properties.Desired = new TwinCollection(@"{AppEUI:'BE7A0000000014E2',AppKey:'8AFE71A145B253E49C3031AD068277A1',GatewayID:'',SensorDecoder:'DecoderValueSensor'}"); - var otaaRemoteTwin = _ = await this.registryManager.GetTwinAsync(OtaaDeviceId); - _ = await this.registryManager.UpdateTwinAsync(OtaaDeviceId, otaaEndTwin, otaaRemoteTwin.ETag); - - var abpDevice = new Device(AbpDeviceId); - _ = await this.registryManager.AddDeviceAsync(abpDevice); - var abpTwin = new Twin(); - abpTwin.Properties.Desired = new TwinCollection(@"{AppSKey:'2B7E151628AED2A6ABF7158809CF4F3C',NwkSKey:'3B7E151628AED2A6ABF7158809CF4F3C',GatewayID:'',DevAddr:'0228B1B1',SensorDecoder:'DecoderValueSensor'}"); - var abpRemoteTwin = await this.registryManager.GetTwinAsync(AbpDeviceId); - _ = await this.registryManager.UpdateTwinAsync(AbpDeviceId, abpTwin, abpRemoteTwin.ETag); + _ = await this.registryManager.DeployEndDevicesAsync(); } } #pragma warning disable CA1031 // Do not catch general exception types. This will go away when we implement #242 @@ -184,22 +76,16 @@ static async Task GetConfigurationContentAsync(Uri configL log.LogWarning(ex.Message); // In case of an exception in device provisioning we want to make sure that we return a proper template if our devices are successfullycreated - var edgeGateway = await this.registryManager.GetDeviceAsync(deviceName); + var edgeGateway = await this.registryManager.GetTwinAsync(deviceName); if (edgeGateway == null) { return PrepareResponse(HttpStatusCode.Conflict); } - if (deployEndDevice) + if (deployEndDevice && !await this.registryManager.DeployEndDevicesAsync()) { - var abpDevice = await this.registryManager.GetDeviceAsync(AbpDeviceId); - var otaaDevice = await this.registryManager.GetDeviceAsync(OtaaDeviceId); - - if (abpDevice == null || otaaDevice == null) - { - return PrepareResponse(HttpStatusCode.Conflict); - } + return PrepareResponse(HttpStatusCode.Conflict); } return PrepareResponse(HttpStatusCode.OK); @@ -219,10 +105,5 @@ private static HttpResponseMessage PrepareResponse(HttpStatusCode httpStatusCode return response; } - - public static string GetEnvironmentVariable(string name) - { - return Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.Process); - } } } diff --git a/LoRaEngine/LoraKeysManagerFacade/DeviceGetter.cs b/LoRaEngine/LoraKeysManagerFacade/DeviceGetter.cs index 1de779bb4c..52b9bcb7b3 100644 --- a/LoRaEngine/LoraKeysManagerFacade/DeviceGetter.cs +++ b/LoRaEngine/LoraKeysManagerFacade/DeviceGetter.cs @@ -7,10 +7,10 @@ namespace LoraKeysManagerFacade using System.Collections.Generic; using System.Globalization; using System.Threading.Tasks; + using LoRaTools; using LoRaWan; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; - using Microsoft.Azure.Devices; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Extensions.Logging; @@ -18,13 +18,15 @@ namespace LoraKeysManagerFacade public class DeviceGetter { - private readonly RegistryManager registryManager; + private readonly IDeviceRegistryManager registryManager; private readonly ILoRaDeviceCacheStore cacheStore; + private readonly ILogger logger; - public DeviceGetter(RegistryManager registryManager, ILoRaDeviceCacheStore cacheStore) + public DeviceGetter(IDeviceRegistryManager registryManager, ILoRaDeviceCacheStore cacheStore, ILogger logger) { this.registryManager = registryManager; this.cacheStore = cacheStore; + this.logger = logger; } /// @@ -32,8 +34,7 @@ public DeviceGetter(RegistryManager registryManager, ILoRaDeviceCacheStore cache /// [FunctionName(nameof(GetDevice))] public async Task GetDevice( - [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, - ILogger log) + [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req) { if (req is null) throw new ArgumentNullException(nameof(req)); @@ -66,11 +67,16 @@ public DeviceGetter(RegistryManager registryManager, ILoRaDeviceCacheStore cache } } + using var deviceScope = this.logger.BeginDeviceScope(devEui); + try { DevNonce? devNonce = ushort.TryParse(rawDevNonce, NumberStyles.None, CultureInfo.InvariantCulture, out var d) ? new DevNonce(d) : null; DevAddr? devAddr = DevAddr.TryParse(devAddrString, out var someDevAddr) ? someDevAddr : null; - var results = await GetDeviceList(devEui, gatewayId, devNonce, devAddr, log); + + using var devAddrScope = this.logger.BeginDeviceAddressScope(devAddr); + + var results = await GetDeviceList(devEui, gatewayId, devNonce, devAddr); var json = JsonConvert.SerializeObject(results); return new OkObjectResult(json); } @@ -78,9 +84,8 @@ public DeviceGetter(RegistryManager registryManager, ILoRaDeviceCacheStore cache { return new BadRequestObjectResult("UsedDevNonce"); } - catch (JoinRefusedException ex) + catch (JoinRefusedException ex) when (ExceptionFilterUtility.True(() => this.logger.LogDebug("Join refused: {msg}", ex.Message))) { - log.LogDebug("Join refused: {msg}", ex.Message); return new BadRequestObjectResult("JoinRefused: " + ex.Message); } catch (ArgumentException ex) @@ -89,13 +94,13 @@ public DeviceGetter(RegistryManager registryManager, ILoRaDeviceCacheStore cache } } - public async Task> GetDeviceList(DevEui? devEUI, string gatewayId, DevNonce? devNonce, DevAddr? devAddr, ILogger log = null) + public async Task> GetDeviceList(DevEui? devEUI, string gatewayId, DevNonce? devNonce, DevAddr? devAddr) { var results = new List(); if (devEUI is { } someDevEui) { - var joinInfo = await TryGetJoinInfoAndValidateAsync(someDevEui, gatewayId, log); + var joinInfo = await TryGetJoinInfoAndValidateAsync(someDevEui, gatewayId); // OTAA join using var deviceCache = new LoRaDeviceCache(this.cacheStore, someDevEui, gatewayId); @@ -114,16 +119,16 @@ public async Task> GetDeviceList(DevEui? devEUI, string g if (await deviceCache.TryToLockAsync()) { deviceCache.ClearCache(); // clear the fcnt up/down after the join - log?.LogDebug("Removed key '{key}':{gwid}", someDevEui, gatewayId); + this.logger.LogDebug("Removed key '{key}':{gwid}", someDevEui, gatewayId); } else { - log?.LogWarning("Failed to acquire lock for '{key}'", someDevEui); + this.logger.LogWarning("Failed to acquire lock for '{key}'", someDevEui); } } else { - log?.LogDebug("dev nonce already used. Ignore request '{key}':{gwid}", someDevEui, gatewayId); + this.logger.LogDebug("dev nonce already used. Ignore request '{key}':{gwid}", someDevEui, gatewayId); throw new DeviceNonceUsedException(); } } @@ -132,7 +137,7 @@ public async Task> GetDeviceList(DevEui? devEUI, string g // ABP or normal message // TODO check for sql injection - var devAddrCache = new LoRaDevAddrCache(this.cacheStore, this.registryManager, log, gatewayId); + var devAddrCache = new LoRaDevAddrCache(this.cacheStore, this.registryManager, this.logger, gatewayId); if (await devAddrCache.TryTakeDevAddrUpdateLock(someDevAddr)) { try @@ -166,22 +171,21 @@ public async Task> GetDeviceList(DevEui? devEUI, string g // if the device is not found is the cache we query, if there was something, it is probably not our device. if (results.Count == 0 && devAddressesInfo == null) { - var query = this.registryManager.CreateQuery($"SELECT * FROM devices WHERE properties.desired.DevAddr = '{someDevAddr}' OR properties.reported.DevAddr ='{someDevAddr}'", 100); + var query = this.registryManager.FindLoRaDeviceByDevAddr(someDevAddr); var resultCount = 0; while (query.HasMoreResults) { - var page = await query.GetNextAsTwinAsync(); + var page = await query.GetNextPageAsync(); foreach (var twin in page) { if (twin.DeviceId != null) { - var device = await this.registryManager.GetDeviceAsync(twin.DeviceId); var iotHubDeviceInfo = new DevAddrCacheInfo { DevAddr = someDevAddr, DevEUI = DevEui.Parse(twin.DeviceId), - PrimaryKey = device.Authentication.SymmetricKey.PrimaryKey, + PrimaryKey = await this.registryManager.GetDevicePrimaryKeyAsync(twin.DeviceId), GatewayId = twin.GetGatewayID(), NwkSKey = twin.GetNwkSKey(), LastUpdatedTwins = twin.Properties.Desired.GetLastUpdated() @@ -220,16 +224,10 @@ public async Task> GetDeviceList(DevEui? devEUI, string g private async Task LoadPrimaryKeyAsync(DevEui devEUI) { - var device = await this.registryManager.GetDeviceAsync(devEUI.ToString()); - if (device == null) - { - return null; - } - - return device.Authentication.SymmetricKey?.PrimaryKey; + return await this.registryManager.GetDevicePrimaryKeyAsync(devEUI.ToString()); } - private async Task TryGetJoinInfoAndValidateAsync(DevEui devEUI, string gatewayId, ILogger log) + private async Task TryGetJoinInfoAndValidateAsync(DevEui devEUI, string gatewayId) { var cacheKeyJoinInfo = string.Concat(devEUI, ":joininfo"); var lockKeyJoinInfo = string.Concat(devEUI, ":joinlockjoininfo"); @@ -242,22 +240,20 @@ private async Task TryGetJoinInfoAndValidateAsync(DevEui devEUI, strin joinInfo = this.cacheStore.GetObject(cacheKeyJoinInfo); if (joinInfo == null) { - joinInfo = new JoinInfo(); + joinInfo = new JoinInfo + { + PrimaryKey = await this.registryManager.GetDevicePrimaryKeyAsync(devEUI.ToString()) + }; - var device = await this.registryManager.GetDeviceAsync(devEUI.ToString()); - if (device != null) + var twin = await this.registryManager.GetLoRaDeviceTwinAsync(devEUI.ToString()); + var deviceGatewayId = twin.GetGatewayID(); + if (!string.IsNullOrEmpty(deviceGatewayId)) { - joinInfo.PrimaryKey = device.Authentication.SymmetricKey.PrimaryKey; - var twin = await this.registryManager.GetTwinAsync(devEUI.ToString()); - var deviceGatewayId = twin.GetGatewayID(); - if (!string.IsNullOrEmpty(deviceGatewayId)) - { - joinInfo.DesiredGateway = deviceGatewayId; - } + joinInfo.DesiredGateway = deviceGatewayId; } _ = this.cacheStore.ObjectSet(cacheKeyJoinInfo, joinInfo, TimeSpan.FromMinutes(60)); - log?.LogDebug("updated cache with join info '{key}':{gwid}", devEUI, gatewayId); + this.logger.LogDebug("updated cache with join info '{key}':{gwid}", devEUI, gatewayId); } } finally @@ -276,7 +272,7 @@ private async Task TryGetJoinInfoAndValidateAsync(DevEui devEUI, strin throw new JoinRefusedException($"Not the owning gateway. Owning gateway is '{joinInfo.DesiredGateway}'"); } - log?.LogDebug("got LogInfo '{key}':{gwid} attached gw: {desiredgw}", devEUI, gatewayId, joinInfo.DesiredGateway); + this.logger.LogDebug("got LogInfo '{key}':{gwid} attached gw: {desiredgw}", devEUI, gatewayId, joinInfo.DesiredGateway); } else { diff --git a/LoRaEngine/LoraKeysManagerFacade/EdgeDeviceGetter.cs b/LoRaEngine/LoraKeysManagerFacade/EdgeDeviceGetter.cs new file mode 100644 index 0000000000..e9fbc4fc1d --- /dev/null +++ b/LoRaEngine/LoraKeysManagerFacade/EdgeDeviceGetter.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoraKeysManagerFacade +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using LoRaTools; + using LoRaTools.IoTHubImpl; + using Microsoft.Azure.Devices.Shared; + using Microsoft.Extensions.Logging; + + public class EdgeDeviceGetter : IEdgeDeviceGetter + { + private readonly IDeviceRegistryManager registryManager; + private readonly ILoRaDeviceCacheStore cacheStore; + private readonly ILogger logger; + private DateTimeOffset? lastUpdateTime; + + public EdgeDeviceGetter(IDeviceRegistryManager registryManager, + ILoRaDeviceCacheStore cacheStore, + ILogger logger) + { + this.registryManager = registryManager; + this.cacheStore = cacheStore; + this.logger = logger; + } + + private async Task> GetEdgeDevicesAsync(CancellationToken cancellationToken) + { + this.logger.LogDebug("Getting Azure IoT Edge devices"); + var twins = new List(); + var query = this.registryManager.GetEdgeDevices(); + + do + { + var items = await query.GetNextPageAsync(); + + twins.AddRange(items); + } while (query.HasMoreResults && !cancellationToken.IsCancellationRequested); + + return twins; + } + + public async Task IsEdgeDeviceAsync(string lnsId, CancellationToken cancellationToken) + { + const string keyLock = $"{nameof(EdgeDeviceGetter)}-lock"; + const string owner = nameof(EdgeDeviceGetter); + var isEdgeDevice = false; + try + { + if (await this.cacheStore.LockTakeAsync(keyLock, owner, TimeSpan.FromSeconds(10))) + { + var findInCache = () => this.cacheStore.GetObject(RedisLnsDeviceCacheKey(lnsId)); + var firstSearch = findInCache(); + if (firstSearch is null) + { + await RefreshEdgeDevicesCacheAsync(cancellationToken); + isEdgeDevice = findInCache() is { IsEdge: true }; + if (!isEdgeDevice) + { + var marked = MarkDeviceAsNonEdge(lnsId); + if (!marked) + this.logger.LogError("Could not update Redis Edge Device cache status for device {}", lnsId); + } + } + else + { + return firstSearch.IsEdge; + } + } + else + { + throw new TimeoutException("Timed out while taking a lock on Redis Edge Device cache"); + } + } + finally + { + _ = this.cacheStore.LockRelease(keyLock, owner); + } + return isEdgeDevice; + } + + private static string RedisLnsDeviceCacheKey(string lnsId) => $"lnsInstance-{lnsId}"; + + private bool MarkDeviceAsNonEdge(string lnsId) + => this.cacheStore.ObjectSet(RedisLnsDeviceCacheKey(lnsId), + new DeviceKind(isEdge: false), + TimeSpan.FromDays(1), + onlyIfNotExists: true); + + private async Task RefreshEdgeDevicesCacheAsync(CancellationToken cancellationToken) + { + this.logger.LogDebug("Refreshing Azure IoT Edge devices cache"); + if (this.lastUpdateTime is null + || this.lastUpdateTime - DateTimeOffset.UtcNow >= TimeSpan.FromMinutes(1)) + { + var twins = await GetEdgeDevicesAsync(cancellationToken); + foreach (var t in twins) + { + _ = this.cacheStore.ObjectSet(RedisLnsDeviceCacheKey(t.DeviceId), + new DeviceKind(isEdge: true), + TimeSpan.FromDays(1), + onlyIfNotExists: true); + } + this.lastUpdateTime = DateTimeOffset.UtcNow; + } + } + + public async Task> ListEdgeDevicesAsync(CancellationToken cancellationToken) + { + var edgeDevices = await GetEdgeDevicesAsync(cancellationToken); + return edgeDevices.Select(e => e.DeviceId).ToList(); + } + } + + internal class DeviceKind + { + public bool IsEdge { get; private set; } + public DeviceKind(bool isEdge) + { + IsEdge = isEdge; + } + } +} diff --git a/LoRaEngine/LoraKeysManagerFacade/FCntCacheCheck.cs b/LoRaEngine/LoraKeysManagerFacade/FCntCacheCheck.cs index 81bcd448b4..264ff3c57f 100644 --- a/LoRaEngine/LoraKeysManagerFacade/FCntCacheCheck.cs +++ b/LoRaEngine/LoraKeysManagerFacade/FCntCacheCheck.cs @@ -5,6 +5,7 @@ namespace LoraKeysManagerFacade { using System; using System.Threading.Tasks; + using LoRaTools; using LoRaWan; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -16,16 +17,17 @@ namespace LoraKeysManagerFacade public class FCntCacheCheck { private readonly ILoRaDeviceCacheStore deviceCache; + private readonly ILogger logger; - public FCntCacheCheck(ILoRaDeviceCacheStore deviceCache) + public FCntCacheCheck(ILoRaDeviceCacheStore deviceCache, ILogger logger) { this.deviceCache = deviceCache; + this.logger = logger; } [FunctionName("NextFCntDown")] public async Task NextFCntDownInvoke( - [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, - ILogger log) + [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req) { if (req is null) throw new ArgumentNullException(nameof(req)); @@ -49,6 +51,8 @@ public FCntCacheCheck(ILoRaDeviceCacheStore deviceCache) return new BadRequestObjectResult("Dev EUI is invalid."); } + using var deviceScope = this.logger.BeginDeviceScope(devEui); + if (!uint.TryParse(fCntUp, out var clientFCntUp)) { throw new ArgumentException("Missing FCntUp"); @@ -67,7 +71,7 @@ public FCntCacheCheck(ILoRaDeviceCacheStore deviceCache) // and continued processing if (deviceInfo.FCntUp > 1) { - log.LogDebug("Resetting cache for device {devEUI}. FCntUp: {fcntup}", devEui, deviceInfo.FCntUp); + this.logger.LogDebug("Resetting cache for device {devEUI}. FCntUp: {fcntup}", devEui, deviceInfo.FCntUp); deviceCache.ClearCache(); } } diff --git a/LoRaEngine/LoraKeysManagerFacade/FacadeStartup.cs b/LoRaEngine/LoraKeysManagerFacade/FacadeStartup.cs index a9670ffcab..7db7d3db71 100644 --- a/LoRaEngine/LoraKeysManagerFacade/FacadeStartup.cs +++ b/LoRaEngine/LoraKeysManagerFacade/FacadeStartup.cs @@ -6,8 +6,11 @@ namespace LoraKeysManagerFacade { using System; + using System.Net.Http; using LoraKeysManagerFacade.FunctionBundler; + using LoRaTools; using LoRaTools.ADR; + using LoRaTools.IoTHubImpl; using Microsoft.Azure.Devices; using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Extensions.Azure; @@ -42,31 +45,36 @@ public override void Configure(IFunctionsHostBuilder builder) var redisCache = redis.GetDatabase(); var deviceCacheStore = new LoRaDeviceCacheRedisStore(redisCache); -#pragma warning disable CA2000 // Dispose objects before losing scope - // Object is handled by DI container. - _ = builder.Services.AddSingleton(RegistryManager.CreateFromConnectionString(iotHubConnectionString)); -#pragma warning restore CA2000 // Dispose objects before losing scope builder.Services.AddAzureClients(builder => { _ = builder.AddBlobServiceClient(configHandler.StorageConnectionString) .WithName(WebJobsStorageClientName); }); _ = builder.Services - .AddSingleton(new ServiceClientAdapter(ServiceClient.CreateFromConnectionString(iotHubConnectionString))) - .AddSingleton(deviceCacheStore) - .AddSingleton(sp => new LoRaADRServerManager(new LoRaADRRedisStore(redisCache, sp.GetRequiredService>()), - new LoRaADRStrategyProvider(sp.GetRequiredService()), - deviceCacheStore, - sp.GetRequiredService>())) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddHttpClient() + .AddSingleton(sp => IoTHubRegistryManager.CreateWithProvider(() => + RegistryManager.CreateFromConnectionString(iotHubConnectionString), + sp.GetRequiredService(), + sp.GetRequiredService>())) + .AddSingleton(new ServiceClientAdapter(ServiceClient.CreateFromConnectionString(iotHubConnectionString))) + .AddSingleton(deviceCacheStore) + .AddSingleton(sp => new LoRaADRServerManager(new LoRaADRRedisStore(redisCache, sp.GetRequiredService>()), + new LoRaADRStrategyProvider(sp.GetRequiredService()), + deviceCacheStore, + sp.GetRequiredService(), + sp.GetRequiredService>())) + .AddSingleton() + .AddSingleton(sp => new RedisChannelPublisher(redis, sp.GetRequiredService>())) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddApplicationInsightsTelemetry(); } private abstract class ConfigHandler @@ -127,6 +135,7 @@ internal LocalConfigHandler() internal override string RedisConnectionString => this.config.GetValue(RedisConnectionStringKey); internal override string IoTHubConnectionString => this.config.GetValue(IoTHubConnectionStringKey); + internal override string StorageConnectionString => this.config.GetConnectionStringOrSetting(StorageConnectionStringKey); } } diff --git a/LoRaEngine/LoraKeysManagerFacade/FunctionsBundler/DeduplicationExecutionItem.cs b/LoRaEngine/LoraKeysManagerFacade/FunctionsBundler/DeduplicationExecutionItem.cs index f1c675f816..894abc660c 100644 --- a/LoRaEngine/LoraKeysManagerFacade/FunctionsBundler/DeduplicationExecutionItem.cs +++ b/LoRaEngine/LoraKeysManagerFacade/FunctionsBundler/DeduplicationExecutionItem.cs @@ -3,23 +3,51 @@ namespace LoraKeysManagerFacade.FunctionBundler { + using System; + using System.Threading; using System.Threading.Tasks; + using LoRaTools; using LoRaTools.CommonAPI; using LoRaWan; + using Microsoft.ApplicationInsights; + using Microsoft.ApplicationInsights.Extensibility; + using Microsoft.ApplicationInsights.Metrics; + using Microsoft.Azure.Devices; + using Microsoft.Azure.Devices.Client.Exceptions; using Microsoft.Extensions.Logging; + using Newtonsoft.Json; public class DeduplicationExecutionItem : IFunctionBundlerExecutionItem { + private const string ConnectionOwnershipChangeMetricName = "ConnectionOwnershipChange"; + private readonly ILoRaDeviceCacheStore cacheStore; + private readonly IServiceClient serviceClient; + private readonly IEdgeDeviceGetter edgeDeviceGetter; + private readonly IChannelPublisher channelPublisher; + private readonly Microsoft.ApplicationInsights.Metric connectionOwnershipChangedMetric; + + private static readonly TimeSpan DuplicateMessageTimeout = TimeSpan.FromSeconds(30); - public DeduplicationExecutionItem(ILoRaDeviceCacheStore cacheStore) + public DeduplicationExecutionItem( + ILoRaDeviceCacheStore cacheStore, + IServiceClient serviceClient, + IEdgeDeviceGetter edgeDeviceGetter, + IChannelPublisher channelPublisher, + TelemetryConfiguration telemetryConfiguration) { this.cacheStore = cacheStore; + this.serviceClient = serviceClient; + this.edgeDeviceGetter = edgeDeviceGetter; + this.channelPublisher = channelPublisher; + var telemetryClient = new TelemetryClient(telemetryConfiguration); + var metricIdentifier = new MetricIdentifier(LoraKeysManagerFacadeConstants.MetricNamespace, ConnectionOwnershipChangeMetricName); + this.connectionOwnershipChangedMetric = telemetryClient.GetMetric(metricIdentifier); } public async Task ExecuteAsync(IPipelineExecutionContext context) { - if (context is null) throw new System.ArgumentNullException(nameof(context)); + ArgumentNullException.ThrowIfNull(context); context.Result.DeduplicationResult = await GetDuplicateMessageResultAsync(context.DevEUI, context.Request.GatewayId, context.Request.ClientFCntUp, context.Request.ClientFCntDown, context.Logger); @@ -40,6 +68,7 @@ public Task OnAbortExecutionAsync(IPipelineExecutionContext context) internal async Task GetDuplicateMessageResultAsync(DevEui devEUI, string gatewayId, uint clientFCntUp, uint clientFCntDown, ILogger logger = null) { + using var cts = new CancellationTokenSource(DuplicateMessageTimeout); var isDuplicate = true; var processedDevice = gatewayId; @@ -47,7 +76,6 @@ internal async Task GetDuplicateMessageResultAsync(DevEui de { if (await deviceCache.TryToLockAsync()) { - // we are owning the lock now if (deviceCache.TryGetInfo(out var cachedDeviceState)) { var updateCacheState = false; @@ -69,9 +97,54 @@ internal async Task GetDuplicateMessageResultAsync(DevEui de if (updateCacheState) { + var previousGateway = cachedDeviceState.GatewayId; + cachedDeviceState.FCntUp = clientFCntUp; cachedDeviceState.GatewayId = gatewayId; _ = deviceCache.StoreInfo(cachedDeviceState); + + if (previousGateway != gatewayId) + { + this.connectionOwnershipChangedMetric.TrackValue(1); + + var loraC2DMessage = new LoRaCloudToDeviceMessage() + { + DevEUI = devEUI, + Fport = FramePort.AppMin, + MessageId = Guid.NewGuid().ToString() + }; + + var method = new CloudToDeviceMethod(LoraKeysManagerFacadeConstants.CloudToDeviceCloseConnection, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); + var jsonContents = JsonConvert.SerializeObject(loraC2DMessage); + _ = method.SetPayloadJson(jsonContents); + + try + { + if (await this.edgeDeviceGetter.IsEdgeDeviceAsync(previousGateway, cts.Token)) + { + var res = await this.serviceClient.InvokeDeviceMethodAsync(previousGateway, Constants.NetworkServerModuleId, method, default); + logger?.LogDebug("Connection owner changed and direct method was called on previous gateway '{PreviousConnectionOwner}' to close connection; result is '{Status}'", previousGateway, res?.Status); + + if (res is null || (res is { } && !HttpUtilities.IsSuccessStatusCode(res.Status))) + { + logger?.LogError("Failed to invoke direct method on LNS '{PreviousConnectionOwner}' to close the connection for device '{DevEUI}'; status '{Status}'", previousGateway, devEUI, res?.Status); + } + + } + else + { + await this.channelPublisher.PublishAsync(previousGateway, new LnsRemoteCall(RemoteCallKind.CloseConnection, jsonContents)); + logger?.LogDebug("Connection owner changed and message was published to previous gateway '{PreviousConnectionOwner}' to close connection", previousGateway); + } + } + catch (IotHubException ex) + { + logger?.LogError(ex, "Exception when invoking direct method on LNS '{PreviousConnectionOwner}' to close the connection for device '{DevEUI}'", previousGateway, devEUI); + + // The exception is not rethrown because closing the connection on the losing gateway + // is performed on best effort basis. + } + } } } else @@ -79,7 +152,7 @@ internal async Task GetDuplicateMessageResultAsync(DevEui de // initialize isDuplicate = false; var state = deviceCache.Initialize(clientFCntUp, clientFCntDown); - logger?.LogDebug("initialized state for {id}:{gwid} = {state}", devEUI, gatewayId, state); + logger?.LogDebug("Connection owner for {DevEui} set to {GatewayId}; state {State}", devEUI, gatewayId, state); } } else diff --git a/LoRaEngine/LoraKeysManagerFacade/FunctionsBundler/FunctionBundlerFunction.cs b/LoRaEngine/LoraKeysManagerFacade/FunctionsBundler/FunctionBundlerFunction.cs index 77191d04d1..6952b46f42 100644 --- a/LoRaEngine/LoraKeysManagerFacade/FunctionsBundler/FunctionBundlerFunction.cs +++ b/LoRaEngine/LoraKeysManagerFacade/FunctionsBundler/FunctionBundlerFunction.cs @@ -5,6 +5,7 @@ namespace LoraKeysManagerFacade.FunctionBundler { using System.Linq; using System.Threading.Tasks; + using LoRaTools; using LoRaWan; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -16,17 +17,18 @@ namespace LoraKeysManagerFacade.FunctionBundler public class FunctionBundlerFunction { private readonly IFunctionBundlerExecutionItem[] executionItems; + private readonly ILogger logger; public FunctionBundlerFunction( - IFunctionBundlerExecutionItem[] items) + IFunctionBundlerExecutionItem[] items, ILogger logger) { this.executionItems = items.OrderBy(x => x.Priority).ToArray(); + this.logger = logger; } [FunctionName("FunctionBundler")] public async Task FunctionBundler( [HttpTrigger(AuthorizationLevel.Function, "post", Route = "FunctionBundler/{devEUI}")] HttpRequest req, - ILogger logger, string devEUI) { try @@ -43,6 +45,8 @@ public class FunctionBundlerFunction return new BadRequestObjectResult("Dev EUI is invalid."); } + using var deviceScope = this.logger.BeginDeviceScope(parsedDevEui); + var requestBody = await req.ReadAsStringAsync(); if (string.IsNullOrEmpty(requestBody)) { @@ -50,14 +54,14 @@ public class FunctionBundlerFunction } var functionBundlerRequest = JsonConvert.DeserializeObject(requestBody); - var result = await HandleFunctionBundlerInvoke(parsedDevEui, functionBundlerRequest, logger); + var result = await HandleFunctionBundlerInvoke(parsedDevEui, functionBundlerRequest); return new OkObjectResult(result); } - public async Task HandleFunctionBundlerInvoke(DevEui devEUI, FunctionBundlerRequest request, ILogger logger = null) + public async Task HandleFunctionBundlerInvoke(DevEui devEUI, FunctionBundlerRequest request) { - var pipeline = new FunctionBundlerPipelineExecuter(this.executionItems, devEUI, request, logger); + var pipeline = new FunctionBundlerPipelineExecuter(this.executionItems, devEUI, request, this.logger); return await pipeline.Execute(); } } diff --git a/LoRaEngine/LoraKeysManagerFacade/HttpUtilities.cs b/LoRaEngine/LoraKeysManagerFacade/HttpUtilities.cs index 70058e5182..373791a0d4 100644 --- a/LoRaEngine/LoraKeysManagerFacade/HttpUtilities.cs +++ b/LoRaEngine/LoraKeysManagerFacade/HttpUtilities.cs @@ -40,5 +40,10 @@ public static ApiVersion GetRequestedVersion(this HttpRequest req) return ApiVersion.Parse(versionText); } + + /// + /// Checks if the http status code indicates success. + /// + public static bool IsSuccessStatusCode(int statusCode) => statusCode is >= 200 and <= 299; } } diff --git a/LoRaEngine/LoraKeysManagerFacade/IChannelPublisher.cs b/LoRaEngine/LoraKeysManagerFacade/IChannelPublisher.cs new file mode 100644 index 0000000000..4bd58a05bc --- /dev/null +++ b/LoRaEngine/LoraKeysManagerFacade/IChannelPublisher.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoraKeysManagerFacade +{ + using System.Threading.Tasks; + using LoRaTools; + + /// + /// Interface for publisher interation. + /// + public interface IChannelPublisher + { + Task PublishAsync(string channel, LnsRemoteCall lnsRemoteCall); + } +} diff --git a/LoRaEngine/LoraKeysManagerFacade/IEdgeDeviceGetter.cs b/LoRaEngine/LoraKeysManagerFacade/IEdgeDeviceGetter.cs new file mode 100644 index 0000000000..d96d87ba9b --- /dev/null +++ b/LoRaEngine/LoraKeysManagerFacade/IEdgeDeviceGetter.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoraKeysManagerFacade +{ + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + public interface IEdgeDeviceGetter + { + Task IsEdgeDeviceAsync(string lnsId, CancellationToken cancellationToken); + Task> ListEdgeDevicesAsync(CancellationToken cancellationToken); + } +} diff --git a/LoRaEngine/LoraKeysManagerFacade/IServiceClient.cs b/LoRaEngine/LoraKeysManagerFacade/IServiceClient.cs index 7899971ba9..eacbb6eff5 100644 --- a/LoRaEngine/LoraKeysManagerFacade/IServiceClient.cs +++ b/LoRaEngine/LoraKeysManagerFacade/IServiceClient.cs @@ -3,6 +3,7 @@ namespace LoraKeysManagerFacade { + using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Devices; @@ -11,7 +12,7 @@ namespace LoraKeysManagerFacade /// public interface IServiceClient { - Task InvokeDeviceMethodAsync(string deviceId, string moduleId, CloudToDeviceMethod cloudToDeviceMethod); + Task InvokeDeviceMethodAsync(string deviceId, string moduleId, CloudToDeviceMethod cloudToDeviceMethod, CancellationToken cancellationToken); Task SendAsync(string deviceId, Message message); } diff --git a/LoRaEngine/LoraKeysManagerFacade/LoRaADRFunction/LoRaADRServerManager.cs b/LoRaEngine/LoraKeysManagerFacade/LoRaADRFunction/LoRaADRServerManager.cs index 70418ce9a3..0907ed4412 100644 --- a/LoRaEngine/LoraKeysManagerFacade/LoRaADRFunction/LoRaADRServerManager.cs +++ b/LoRaEngine/LoraKeysManagerFacade/LoRaADRFunction/LoRaADRServerManager.cs @@ -11,16 +11,22 @@ namespace LoraKeysManagerFacade public class LoRaADRServerManager : LoRaADRManagerBase { private readonly ILoRaDeviceCacheStore deviceCacheStore; + private readonly ILoggerFactory loggerFactory; - public LoRaADRServerManager(ILoRaADRStore store, ILoRaADRStrategyProvider strategyProvider, ILoRaDeviceCacheStore deviceCacheStore, ILogger logger) + public LoRaADRServerManager(ILoRaADRStore store, + ILoRaADRStrategyProvider strategyProvider, + ILoRaDeviceCacheStore deviceCacheStore, + ILoggerFactory loggerFactory, + ILogger logger) : base(store, strategyProvider, logger) { this.deviceCacheStore = deviceCacheStore; + this.loggerFactory = loggerFactory; } public override async Task NextFCntDown(DevEui devEUI, string gatewayId, uint clientFCntUp, uint clientFCntDown) { - var fcntCheck = new FCntCacheCheck(this.deviceCacheStore); + var fcntCheck = new FCntCacheCheck(this.deviceCacheStore, this.loggerFactory.CreateLogger()); return await fcntCheck.GetNextFCntDownAsync(devEUI, gatewayId, clientFCntUp, clientFCntDown); } } diff --git a/LoRaEngine/LoraKeysManagerFacade/LoRaDevAddrCache.cs b/LoRaEngine/LoraKeysManagerFacade/LoRaDevAddrCache.cs index d55ef36518..209cf3ac6d 100644 --- a/LoRaEngine/LoraKeysManagerFacade/LoRaDevAddrCache.cs +++ b/LoRaEngine/LoraKeysManagerFacade/LoRaDevAddrCache.cs @@ -8,9 +8,9 @@ namespace LoraKeysManagerFacade using System.Globalization; using System.Linq; using System.Threading.Tasks; + using LoRaTools; using LoRaTools.Utils; using LoRaWan; - using Microsoft.Azure.Devices; using Microsoft.Azure.Devices.Common.Exceptions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -19,7 +19,10 @@ namespace LoraKeysManagerFacade public sealed class LoRaDevAddrCache { - private const string LastDeltaUpdateKeyValue = "lastDeltaUpdateKeyValue"; + /// + /// The value for this key contains the most recent twin update collected after a delta update. + /// + internal const string LastDeltaUpdateKeyValue = "lastDeltaUpdateKeyValue"; /// /// This is the lock controlling a complete update of the cache. @@ -57,7 +60,7 @@ public sealed class LoRaDevAddrCache private static string GenerateKey(DevAddr devAddr) => CacheKeyPrefix + devAddr; - public LoRaDevAddrCache(ILoRaDeviceCacheStore cacheStore, RegistryManager registryManager, ILogger logger, string gatewayId) + public LoRaDevAddrCache(ILoRaDeviceCacheStore cacheStore, IDeviceRegistryManager registryManager, ILogger logger, string gatewayId) { this.cacheStore = cacheStore; this.logger = logger ?? NullLogger.Instance; @@ -110,7 +113,7 @@ public void StoreInfo(DevAddrCacheInfo info) this.logger.LogInformation($"Successfully saved dev address info on dictionary key: {cacheKeyToUse}, hashkey: {info.DevEUI}, object: {serializedObjectValue}"); } - internal async Task PerformNeededSyncs(RegistryManager registryManager) + internal async Task PerformNeededSyncs(IDeviceRegistryManager registryManager) { // If a full update is expected if (await this.cacheStore.LockTakeAsync(FullUpdateLockKey, this.lockOwner, FullUpdateKeyTimeSpan, block: false)) @@ -191,57 +194,73 @@ internal async Task PerformNeededSyncs(RegistryManager registryManager) /// /// Perform a full relaoad on the dev address cache. This occur typically once every 24 h. /// - private async Task PerformFullReload(RegistryManager registryManager) + private async Task PerformFullReload(IDeviceRegistryManager registryManager) { - var query = $"SELECT * FROM devices WHERE is_defined(properties.desired.AppKey) OR is_defined(properties.desired.AppSKey) OR is_defined(properties.desired.NwkSKey)"; - var devAddrCacheInfos = await GetDeviceTwinsFromIotHub(registryManager, query); + var query = registryManager.GetAllLoRaDevices(); + var devAddrCacheInfos = await GetDeviceTwinsFromIotHub(query, null); BulkSaveDevAddrCache(devAddrCacheInfos, true); } /// /// Method performing a deltaReload. Typically occur every 5 minutes. + /// + /// Please be aware that changes to twin are delayed in IoT Hub queries. + /// The Delta reload is keeping track of the most recent update and using that timestamp + /// as the start date/time for the next iteration. + /// No one is guaranteeing that the twin changes are propagated all together and in order, + /// therefore there could be a chance where we are missing some items. + /// At the same time, the LoRaWanNetworkServer is proactively storing the changes in Redis + /// after a successful join, therefore the chance of missing items should be very very low. /// - private async Task PerformDeltaReload(RegistryManager registryManager) + private async Task PerformDeltaReload(IDeviceRegistryManager registryManager) { - // if the value is null (first call), we take five minutes before this call - var lastUpdate = this.cacheStore.StringGet(LastDeltaUpdateKeyValue) ?? DateTime.UtcNow.AddMinutes(-5).ToString(LoraKeysManagerFacadeConstants.RoundTripDateTimeStringFormat, CultureInfo.InvariantCulture); - var query = $"SELECT * FROM devices where properties.desired.$metadata.$lastUpdated >= '{lastUpdate}' OR properties.reported.$metadata.DevAddr.$lastUpdated >= '{lastUpdate}'"; - var devAddrCacheInfos = await GetDeviceTwinsFromIotHub(registryManager, query); + // if the value is null (first call), we take updates from one hour before this call + var lastUpdate = long.TryParse(this.cacheStore.StringGet(LastDeltaUpdateKeyValue), out var cachedTicks) ? cachedTicks : DateTime.UtcNow.AddHours(-1).Ticks; + var lastUpdateDateTime = new DateTime(lastUpdate, DateTimeKind.Utc); + var query = registryManager.GetLastUpdatedLoRaDevices(lastUpdateDateTime); + var devAddrCacheInfos = await GetDeviceTwinsFromIotHub(query, lastUpdate); BulkSaveDevAddrCache(devAddrCacheInfos, false); } - private async Task> GetDeviceTwinsFromIotHub(RegistryManager registryManager, string inputQuery) + private async Task> GetDeviceTwinsFromIotHub(IRegistryPageResult query, long? lastDeltaUpdateFromCacheTicks) { - var query = registryManager.CreateQuery(inputQuery); - var lastQueryTs = DateTime.UtcNow.AddSeconds(-10); // account for some clock drift - _ = this.cacheStore.StringSet(LastDeltaUpdateKeyValue, lastQueryTs.ToString(LoraKeysManagerFacadeConstants.RoundTripDateTimeStringFormat, CultureInfo.InvariantCulture), TimeSpan.FromDays(1)); + var isFullReload = lastDeltaUpdateFromCacheTicks is null; var devAddrCacheInfos = new List(); while (query.HasMoreResults) { - var page = await query.GetNextAsTwinAsync(); + var page = await query.GetNextPageAsync(); - foreach (var twin in page) + foreach (var twin in page.Where(twin => twin.DeviceId != null)) { - if (twin.DeviceId != null) + if (!twin.Properties.Desired.TryRead(TwinPropertiesConstants.DevAddr, this.logger, out DevAddr devAddr) && + !twin.Properties.Reported.TryRead(TwinPropertiesConstants.DevAddr, this.logger, out devAddr)) { - if (!twin.Properties.Desired.TryRead(LoraKeysManagerFacadeConstants.TwinProperty_DevAddr, this.logger, out DevAddr devAddr) && - !twin.Properties.Reported.TryRead(LoraKeysManagerFacadeConstants.TwinProperty_DevAddr, this.logger, out devAddr)) - { - continue; - } + continue; + } - devAddrCacheInfos.Add(new DevAddrCacheInfo() - { - DevAddr = devAddr, - DevEUI = DevEui.Parse(twin.DeviceId), - GatewayId = twin.GetGatewayID(), - NwkSKey = twin.GetNwkSKey(), - LastUpdatedTwins = twin.Properties.Desired.GetLastUpdated() - }); + devAddrCacheInfos.Add(new DevAddrCacheInfo() + { + DevAddr = devAddr, + DevEUI = DevEui.Parse(twin.DeviceId), + GatewayId = twin.GetGatewayID(), + NwkSKey = twin.GetNwkSKey(), + LastUpdatedTwins = twin.Properties.Desired.GetLastUpdated() + }); + + if (!isFullReload + && twin.Properties.Desired.GetMetadata() is { LastUpdated: { } desiredUpdateTime } + && twin.Properties.Reported.GetMetadata() is { LastUpdated: { } reportedUpdateTime }) + { + lastDeltaUpdateFromCacheTicks = Math.Max(lastDeltaUpdateFromCacheTicks.Value, Math.Max(desiredUpdateTime.Ticks, reportedUpdateTime.Ticks)); } } } + if (!isFullReload) + { + _ = this.cacheStore.StringSet(LastDeltaUpdateKeyValue, lastDeltaUpdateFromCacheTicks.Value.ToString(CultureInfo.InvariantCulture), TimeSpan.FromDays(1)); + } + return devAddrCacheInfos; } diff --git a/LoRaEngine/LoraKeysManagerFacade/LoRaDeviceJoinNotificationFunction.cs b/LoRaEngine/LoraKeysManagerFacade/LoRaDeviceJoinNotificationFunction.cs new file mode 100644 index 0000000000..45754fae45 --- /dev/null +++ b/LoRaEngine/LoraKeysManagerFacade/LoRaDeviceJoinNotificationFunction.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoraKeysManagerFacade +{ + using LoRaTools; + using LoRaTools.CommonAPI; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Azure.WebJobs; + using Microsoft.Azure.WebJobs.Extensions.Http; + using Microsoft.Extensions.Logging; + + internal class LoRaDeviceJoinNotificationFunction + { + private readonly LoRaDevAddrCache loRaDevAddrCache; + private readonly ILogger logger; + + public LoRaDeviceJoinNotificationFunction(LoRaDevAddrCache loRaDevAddrCache, ILogger logger) + { + this.loRaDevAddrCache = loRaDevAddrCache; + this.logger = logger; + } + + [FunctionName("DeviceJoinNotification")] + public IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = "devicejoinnotification")] DeviceJoinNotification joinNotification, + HttpRequest req) + { + try + { + VersionValidator.Validate(req); + } + catch (IncompatibleVersionException ex) + { + return new BadRequestObjectResult(ex.Message); + } + + using var deviceScope = this.logger.BeginDeviceScope(joinNotification.DevEUI); + + this.loRaDevAddrCache.StoreInfo(new DevAddrCacheInfo + { + DevAddr = joinNotification.DevAddr, + DevEUI = joinNotification.DevEUI, + GatewayId = joinNotification.GatewayId, + NwkSKey = joinNotification.NwkSKeyString + }); + + return new OkResult(); + } + } +} diff --git a/LoRaEngine/LoraKeysManagerFacade/LoraKeysManagerFacade.csproj b/LoRaEngine/LoraKeysManagerFacade/LoraKeysManagerFacade.csproj index 2ae8c9b9ee..9bfab5c29d 100644 --- a/LoRaEngine/LoraKeysManagerFacade/LoraKeysManagerFacade.csproj +++ b/LoRaEngine/LoraKeysManagerFacade/LoraKeysManagerFacade.csproj @@ -4,15 +4,16 @@ v4 - - - + + + + diff --git a/LoRaEngine/LoraKeysManagerFacade/LoraKeysManagerFacadeConstants.cs b/LoRaEngine/LoraKeysManagerFacade/LoraKeysManagerFacadeConstants.cs index 74cff0e696..906451505b 100644 --- a/LoRaEngine/LoraKeysManagerFacade/LoraKeysManagerFacadeConstants.cs +++ b/LoRaEngine/LoraKeysManagerFacade/LoraKeysManagerFacadeConstants.cs @@ -5,13 +5,9 @@ namespace LoraKeysManagerFacade { internal static class LoraKeysManagerFacadeConstants { - internal const string TwinProperty_GatewayID = "GatewayID"; - internal const string TwinProperty_ClassType = "ClassType"; - internal const string TwinProperty_PreferredGatewayID = "PreferredGatewayID"; - internal const string TwinProperty_DevAddr = "DevAddr"; - internal const string TwinProperty_NwkSKey = "NwkSKey"; - internal const string NetworkServerModuleId = "LoRaWanNetworkSrvModule"; + internal const string ClearCacheMethodName = "clearcache"; internal const string CloudToDeviceMessageMethodName = "cloudtodevicemessage"; - public const string RoundTripDateTimeStringFormat = "o"; + internal const string CloudToDeviceCloseConnection = "closeconnection"; + internal const string MetricNamespace = "LoRaWAN"; } } diff --git a/LoRaEngine/LoraKeysManagerFacade/SearchDeviceByDevEUI.cs b/LoRaEngine/LoraKeysManagerFacade/SearchDeviceByDevEUI.cs index e91d122ac7..c65ec94265 100644 --- a/LoRaEngine/LoraKeysManagerFacade/SearchDeviceByDevEUI.cs +++ b/LoRaEngine/LoraKeysManagerFacade/SearchDeviceByDevEUI.cs @@ -5,25 +5,27 @@ namespace LoraKeysManagerFacade { using System; using System.Threading.Tasks; + using LoRaTools; using LoRaWan; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; - using Microsoft.Azure.Devices; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Extensions.Logging; public class SearchDeviceByDevEUI { - private readonly RegistryManager registryManager; + private readonly IDeviceRegistryManager registryManager; + private readonly ILogger logger; - public SearchDeviceByDevEUI(RegistryManager registryManager) + public SearchDeviceByDevEUI(IDeviceRegistryManager registryManager, ILogger logger) { this.registryManager = registryManager; + this.logger = logger; } [FunctionName(nameof(GetDeviceByDevEUI))] - public async Task GetDeviceByDevEUI([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ILogger log) + public async Task GetDeviceByDevEUI([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req) { if (req is null) throw new ArgumentNullException(nameof(req)); @@ -33,14 +35,14 @@ public async Task GetDeviceByDevEUI([HttpTrigger(AuthorizationLev } catch (IncompatibleVersionException ex) { - log.LogError(ex, "Invalid version"); + this.logger.LogError(ex, "Invalid version"); return new BadRequestObjectResult(ex.Message); } - return await RunGetDeviceByDevEUI(req, log); + return await RunGetDeviceByDevEUI(req); } - private async Task RunGetDeviceByDevEUI(HttpRequest req, ILogger log) + private async Task RunGetDeviceByDevEUI(HttpRequest req) { string devEui = req.Query["DevEUI"]; if (!DevEui.TryParse(devEui, out var parsedDevEui)) @@ -48,19 +50,21 @@ private async Task RunGetDeviceByDevEUI(HttpRequest req, ILogger return new BadRequestObjectResult("DevEUI missing or invalid."); } - var device = await this.registryManager.GetDeviceAsync(parsedDevEui.ToString()); - if (device != null) + using var deviceScope = this.logger.BeginDeviceScope(parsedDevEui); + + var primaryKey = await this.registryManager.GetDevicePrimaryKeyAsync(parsedDevEui.ToString()); + if (primaryKey != null) { - log.LogDebug($"Search for {devEui} found 1 device"); + this.logger.LogDebug($"Search for {devEui} found 1 device"); return new OkObjectResult(new { DevEUI = devEui, - device.Authentication.SymmetricKey.PrimaryKey + PrimaryKey = primaryKey }); } else { - log.LogInformation($"Search for {devEui} found 0 devices"); + this.logger.LogInformation($"Search for {devEui} found 0 devices"); return new NotFoundResult(); } } diff --git a/LoRaEngine/LoraKeysManagerFacade/SendCloudToDeviceMessage/SendCloudToDeviceMessage.cs b/LoRaEngine/LoraKeysManagerFacade/SendCloudToDeviceMessage/SendCloudToDeviceMessage.cs index 7cba58acd9..a447b5672a 100644 --- a/LoRaEngine/LoraKeysManagerFacade/SendCloudToDeviceMessage/SendCloudToDeviceMessage.cs +++ b/LoRaEngine/LoraKeysManagerFacade/SendCloudToDeviceMessage/SendCloudToDeviceMessage.cs @@ -8,7 +8,9 @@ namespace LoraKeysManagerFacade using System.Linq; using System.Net; using System.Text; + using System.Threading; using System.Threading.Tasks; + using LoRaTools; using LoRaTools.CommonAPI; using LoRaTools.Utils; using LoRaWan; @@ -30,22 +32,32 @@ namespace LoraKeysManagerFacade public class SendCloudToDeviceMessage { private readonly ILoRaDeviceCacheStore cacheStore; - private readonly RegistryManager registryManager; + private readonly IDeviceRegistryManager registryManager; private readonly IServiceClient serviceClient; + private readonly IEdgeDeviceGetter edgeDeviceGetter; + private readonly IChannelPublisher channelPublisher; private readonly ILogger log; - public SendCloudToDeviceMessage(ILoRaDeviceCacheStore cacheStore, RegistryManager registryManager, IServiceClient serviceClient, ILogger log) + public SendCloudToDeviceMessage(ILoRaDeviceCacheStore cacheStore, + IDeviceRegistryManager registryManager, + IServiceClient serviceClient, + IEdgeDeviceGetter edgeDeviceGetter, + IChannelPublisher channelPublisher, + ILogger log) { this.cacheStore = cacheStore; this.registryManager = registryManager; this.serviceClient = serviceClient; + this.edgeDeviceGetter = edgeDeviceGetter; + this.channelPublisher = channelPublisher; this.log = log; } [FunctionName("SendCloudToDeviceMessage")] public async Task Run( [HttpTrigger(AuthorizationLevel.Function, "post", Route = "cloudtodevicemessage/{devEUI}")] HttpRequest req, - string devEUI) + string devEUI, + CancellationToken cancellationToken) { DevEui parsedDevEui; @@ -66,6 +78,8 @@ public SendCloudToDeviceMessage(ILoRaDeviceCacheStore cacheStore, RegistryManage return new BadRequestObjectResult(ex.Message); } + using var deviceScope = this.log.BeginDeviceScope(parsedDevEui); + var requestBody = await req.ReadAsStringAsync(); if (string.IsNullOrEmpty(requestBody)) { @@ -75,10 +89,10 @@ public SendCloudToDeviceMessage(ILoRaDeviceCacheStore cacheStore, RegistryManage var c2dMessage = JsonConvert.DeserializeObject(requestBody); c2dMessage.DevEUI = parsedDevEui; - return await SendCloudToDeviceMessageImplementationAsync(parsedDevEui, c2dMessage); + return await SendCloudToDeviceMessageImplementationAsync(parsedDevEui, c2dMessage, cancellationToken); } - public async Task SendCloudToDeviceMessageImplementationAsync(DevEui devEUI, LoRaCloudToDeviceMessage c2dMessage) + public async Task SendCloudToDeviceMessageImplementationAsync(DevEui devEUI, LoRaCloudToDeviceMessage c2dMessage, CancellationToken cancellationToken) { if (c2dMessage == null) { @@ -93,21 +107,20 @@ public async Task SendCloudToDeviceMessageImplementationAsync(Dev var cachedPreferredGateway = LoRaDevicePreferredGateway.LoadFromCache(this.cacheStore, devEUI); if (cachedPreferredGateway != null && !string.IsNullOrEmpty(cachedPreferredGateway.GatewayID)) { - return await SendMessageViaDirectMethodAsync(cachedPreferredGateway.GatewayID, devEUI, c2dMessage); + return await SendMessageViaDirectMethodOrPubSubAsync(cachedPreferredGateway.GatewayID, devEUI, c2dMessage, cancellationToken); } - var queryText = $"SELECT * FROM devices WHERE deviceId = '{devEUI}'"; - var query = this.registryManager.CreateQuery(queryText, 1); + var query = this.registryManager.FindDeviceByDevEUI(devEUI); if (query.HasMoreResults) { - IEnumerable deviceTwins; + IEnumerable deviceTwins; try { - deviceTwins = await query.GetNextAsTwinAsync(); + deviceTwins = await query.GetNextPageAsync(); } catch (IotHubException ex) { - this.log.LogError(ex, "Failed to query devices with {query}", queryText); + this.log.LogError(ex, "Failed to query devices"); return new ObjectResult("Failed to query devices") { StatusCode = (int)HttpStatusCode.InternalServerError }; } @@ -119,22 +132,22 @@ public async Task SendCloudToDeviceMessageImplementationAsync(Dev var reportedReader = new TwinCollectionReader(twin.Properties.Reported, this.log); // the device must have a DevAddr - if (!desiredReader.TryRead(LoraKeysManagerFacadeConstants.TwinProperty_DevAddr, out DevAddr _) && !reportedReader.TryRead(LoraKeysManagerFacadeConstants.TwinProperty_DevAddr, out DevAddr _)) + if (!desiredReader.TryRead(TwinPropertiesConstants.DevAddr, out DevAddr _) && !reportedReader.TryRead(TwinPropertiesConstants.DevAddr, out DevAddr _)) { return new BadRequestObjectResult("Device DevAddr is unknown. Ensure the device has been correctly setup as a LoRa device and that it has connected to network at least once."); } - if (desiredReader.TryRead(LoraKeysManagerFacadeConstants.TwinProperty_ClassType, out string deviceClass) && string.Equals("c", deviceClass, StringComparison.OrdinalIgnoreCase)) + if (desiredReader.TryRead(TwinPropertiesConstants.ClassType, out string deviceClass) && string.Equals("c", deviceClass, StringComparison.OrdinalIgnoreCase)) { - if ((reportedReader.TryRead(LoraKeysManagerFacadeConstants.TwinProperty_PreferredGatewayID, out string gatewayID) - || desiredReader.TryRead(LoraKeysManagerFacadeConstants.TwinProperty_GatewayID, out gatewayID)) + if ((reportedReader.TryRead(TwinPropertiesConstants.PreferredGatewayID, out string gatewayID) + || desiredReader.TryRead(TwinPropertiesConstants.GatewayID, out gatewayID)) && !string.IsNullOrEmpty(gatewayID)) { // add it to cache (if it does not exist) var preferredGateway = new LoRaDevicePreferredGateway(gatewayID, 0); _ = LoRaDevicePreferredGateway.SaveToCache(this.cacheStore, devEUI, preferredGateway, onlyIfNotExists: true); - return await SendMessageViaDirectMethodAsync(gatewayID, devEUI, c2dMessage); + return await SendMessageViaDirectMethodOrPubSubAsync(gatewayID, devEUI, c2dMessage, cancellationToken); } // class c device that did not send a single upstream message @@ -159,6 +172,7 @@ private async Task SendMessageViaCloudToDeviceMessageAsync(DevEui using var message = new Message(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(c2dMessage))); message.MessageId = string.IsNullOrEmpty(c2dMessage.MessageId) ? Guid.NewGuid().ToString() : c2dMessage.MessageId; + // class a devices only listen for 1-2 seconds, so we send to a queue on the device - we don't care about this for redis try { await this.serviceClient.SendAsync(devEUI.ToString(), message); @@ -185,20 +199,44 @@ private async Task SendMessageViaCloudToDeviceMessageAsync(DevEui } } - private async Task SendMessageViaDirectMethodAsync( + private async Task SendMessageViaDirectMethodOrPubSubAsync( string preferredGatewayID, DevEui devEUI, - LoRaCloudToDeviceMessage c2dMessage) + LoRaCloudToDeviceMessage c2dMessage, + CancellationToken cancellationToken) { try { var method = new CloudToDeviceMethod(LoraKeysManagerFacadeConstants.CloudToDeviceMessageMethodName, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); - _ = method.SetPayloadJson(JsonConvert.SerializeObject(c2dMessage)); + var jsonContent = JsonConvert.SerializeObject(c2dMessage); + _ = method.SetPayloadJson(jsonContent); - var res = await this.serviceClient.InvokeDeviceMethodAsync(preferredGatewayID, LoraKeysManagerFacadeConstants.NetworkServerModuleId, method); - if (IsSuccessStatusCode(res.Status)) + if (await edgeDeviceGetter.IsEdgeDeviceAsync(preferredGatewayID, cancellationToken)) { - this.log.LogInformation("Direct method call to {gatewayID} and {devEUI} succeeded with {statusCode}", preferredGatewayID, devEUI, res.Status); + var res = await this.serviceClient.InvokeDeviceMethodAsync(preferredGatewayID, Constants.NetworkServerModuleId, method, cancellationToken); + if (HttpUtilities.IsSuccessStatusCode(res.Status)) + { + this.log.LogInformation("Direct method call to {gatewayID} and {devEUI} succeeded with {statusCode}", preferredGatewayID, devEUI, res.Status); + + return new OkObjectResult(new SendCloudToDeviceMessageResult() + { + DevEui = devEUI, + MessageID = c2dMessage.MessageId, + ClassType = "C", + }); + } + + this.log.LogError("Direct method call to {gatewayID} failed with {statusCode}. Response: {response}", preferredGatewayID, res.Status, res.GetPayloadAsJson()); + + return new ObjectResult(res.GetPayloadAsJson()) + { + StatusCode = res.Status, + }; + } + else + { + await this.channelPublisher.PublishAsync(preferredGatewayID, new LnsRemoteCall(RemoteCallKind.CloudToDeviceMessage, jsonContent)); + this.log.LogInformation("C2D message to {gatewayID} and {devEUI} published to Redis queue", preferredGatewayID, devEUI); return new OkObjectResult(new SendCloudToDeviceMessageResult() { @@ -207,13 +245,6 @@ private async Task SendMessageViaCloudToDeviceMessageAsync(DevEui ClassType = "C", }); } - - this.log.LogError("Direct method call to {gatewayID} failed with {statusCode}. Response: {response}", preferredGatewayID, res.Status, res.GetPayloadAsJson()); - - return new ObjectResult(res.GetPayloadAsJson()) - { - StatusCode = res.Status, - }; } catch (JsonSerializationException ex) { @@ -233,10 +264,5 @@ private async Task SendMessageViaCloudToDeviceMessageAsync(DevEui }; } } - - /// - /// Gets if the http status code indicates success. - /// - private static bool IsSuccessStatusCode(int statusCode) => statusCode is >= 200 and <= 299; } } diff --git a/LoRaEngine/LoraKeysManagerFacade/ServiceClientAdapter.cs b/LoRaEngine/LoraKeysManagerFacade/ServiceClientAdapter.cs index afc086e350..9e3da32d2c 100644 --- a/LoRaEngine/LoraKeysManagerFacade/ServiceClientAdapter.cs +++ b/LoRaEngine/LoraKeysManagerFacade/ServiceClientAdapter.cs @@ -3,6 +3,7 @@ namespace LoraKeysManagerFacade { + using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Devices; @@ -15,7 +16,10 @@ public ServiceClientAdapter(ServiceClient serviceClient) this.serviceClient = serviceClient ?? throw new System.ArgumentNullException(nameof(serviceClient)); } - public Task InvokeDeviceMethodAsync(string deviceId, string moduleId, CloudToDeviceMethod cloudToDeviceMethod) => this.serviceClient.InvokeDeviceMethodAsync(deviceId, moduleId, cloudToDeviceMethod); + public Task InvokeDeviceMethodAsync(string deviceId, + string moduleId, + CloudToDeviceMethod cloudToDeviceMethod, + CancellationToken cancellationToken) => this.serviceClient.InvokeDeviceMethodAsync(deviceId, moduleId, cloudToDeviceMethod, cancellationToken); public Task SendAsync(string deviceId, Message message) => this.serviceClient.SendAsync(deviceId, message); } diff --git a/LoRaEngine/LoraKeysManagerFacade/SyncDevAddrCache.cs b/LoRaEngine/LoraKeysManagerFacade/SyncDevAddrCache.cs index bf2963d96a..4486e58d6d 100644 --- a/LoRaEngine/LoraKeysManagerFacade/SyncDevAddrCache.cs +++ b/LoRaEngine/LoraKeysManagerFacade/SyncDevAddrCache.cs @@ -5,16 +5,16 @@ namespace LoraKeysManagerFacade { using System; using System.Threading.Tasks; - using Microsoft.Azure.Devices; + using LoRaTools; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; public class SyncDevAddrCache { private readonly LoRaDevAddrCache loRaDevAddrCache; - private readonly RegistryManager registryManager; + private readonly IDeviceRegistryManager registryManager; - public SyncDevAddrCache(LoRaDevAddrCache loRaDevAddrCache, RegistryManager registryManager) + public SyncDevAddrCache(LoRaDevAddrCache loRaDevAddrCache, IDeviceRegistryManager registryManager) { this.loRaDevAddrCache = loRaDevAddrCache; this.registryManager = registryManager; diff --git a/LoRaEngine/LoraKeysManagerFacade/TwinExtensions.cs b/LoRaEngine/LoraKeysManagerFacade/TwinExtensions.cs deleted file mode 100644 index b4fe365ea3..0000000000 --- a/LoRaEngine/LoraKeysManagerFacade/TwinExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace LoraKeysManagerFacade -{ - using LoRaTools.Utils; - using Microsoft.Azure.Devices.Shared; - - internal static class TwinExtensions - { - internal static string GetGatewayID(this Twin twin) - => twin.Properties.Desired.TryRead(LoraKeysManagerFacadeConstants.TwinProperty_GatewayID, null, out var someGatewayId) - ? someGatewayId - : string.Empty; - - internal static string GetNwkSKey(this Twin twin) - => twin.Properties.Desired.TryRead(LoraKeysManagerFacadeConstants.TwinProperty_NwkSKey, null, out string nwkSKey) - ? nwkSKey - : twin.Properties.Reported.TryRead(LoraKeysManagerFacadeConstants.TwinProperty_NwkSKey, null, out nwkSKey) - ? nwkSKey - : null; - } -} diff --git a/LoRaEngine/compose.yaml b/LoRaEngine/compose.yaml index e58fef85a5..deaaba8a5f 100644 --- a/LoRaEngine/compose.yaml +++ b/LoRaEngine/compose.yaml @@ -12,6 +12,13 @@ services: dockerfile: LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.arm32v7 image: ${CONTAINER_REGISTRY_ADDRESS:?"CONTAINER_REGISTRY_ADDRESS environment variable need to be set"}/lorawannetworksrvmodule:${NET_SRV_VERSION:-0.0.1-local-arm32v7} + lorawannetworkserver-arm64v8: + build: + context: .. + dockerfile: LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.arm64v8 + image: ${CONTAINER_REGISTRY_ADDRESS:?"CONTAINER_REGISTRY_ADDRESS environment variable need to be set"}/lorawannetworksrvmodule:${NET_SRV_VERSION:-0.0.1-local-arm64v8} + + lorabasicsstationmodule-arm32v7: build: context: .. diff --git a/LoRaEngine/deployment.ci.aio.template.json b/LoRaEngine/deployment.ci.aio.template.json index 37c8f78f87..793ade45c5 100644 --- a/LoRaEngine/deployment.ci.aio.template.json +++ b/LoRaEngine/deployment.ci.aio.template.json @@ -113,8 +113,8 @@ "LOG_TO_TCP_ADDRESS": { "value": "$NET_SRV_LOG_TO_TCP_ADDRESS" }, - "APPINSIGHTS_INSTRUMENTATIONKEY": { - "value": "$APPINSIGHTS_INSTRUMENTATIONKEY" + "APPLICATIONINSIGHTS_CONNECTION_STRING": { + "value": "$APPLICATIONINSIGHTS_CONNECTION_STRING" }, "LNS_SERVER_PFX_PATH": { "value": "/var/lorastarterkit/certs/server.pfx" diff --git a/LoRaEngine/deployment.ci.lns.eflow.template.json b/LoRaEngine/deployment.ci.lns.eflow.template.json index 26f2e81499..4a8da3d966 100644 --- a/LoRaEngine/deployment.ci.lns.eflow.template.json +++ b/LoRaEngine/deployment.ci.lns.eflow.template.json @@ -104,8 +104,8 @@ "LOG_TO_TCP_ADDRESS": { "value": "$NET_SRV_LOG_TO_TCP_ADDRESS" }, - "APPINSIGHTS_INSTRUMENTATIONKEY": { - "value": "$APPINSIGHTS_INSTRUMENTATIONKEY" + "APPLICATIONINSIGHTS_CONNECTION_STRING": { + "value": "$APPLICATIONINSIGHTS_CONNECTION_STRING" }, "LNS_VERSION": { "value": "$NET_SRV_VERSION" diff --git a/LoRaEngine/deployment.template.json b/LoRaEngine/deployment.template.json index e1bd14f595..88e7d0f376 100644 --- a/LoRaEngine/deployment.template.json +++ b/LoRaEngine/deployment.template.json @@ -102,8 +102,8 @@ "LOG_TO_TCP_ADDRESS": { "value": "$NET_SRV_LOG_TO_TCP_ADDRESS" }, - "APPINSIGHTS_INSTRUMENTATIONKEY": { - "value": "$APPINSIGHTS_INSTRUMENTATIONKEY" + "APPLICATIONINSIGHTS_CONNECTION_STRING": { + "value": "$APPLICATIONINSIGHTS_CONNECTION_STRING" } }, "status": "running", diff --git a/LoRaEngine/docker-bake.hcl b/LoRaEngine/docker-bake.hcl index 6103b8dcd8..0d6ce5c1b0 100644 --- a/LoRaEngine/docker-bake.hcl +++ b/LoRaEngine/docker-bake.hcl @@ -10,7 +10,7 @@ variable "CONTAINER_REGISTRY_ADDRESS" { } group "default" { - targets = ["LoRaWanNetworkServerx64", "LoRaWanNetworkServerarm32", "LoraBasicsStationx64", "LoraBasicsStationarm32v7"] + targets = ["LoRaWanNetworkServerx64", "LoRaWanNetworkServerarm32", "LoRaWanNetworkServerarm64v8", "LoraBasicsStationx64", "LoraBasicsStationarm32v7", "LoraBasicsStationarm64v8"] } target "LoRaWanNetworkServer" { @@ -37,6 +37,13 @@ target "LoRaWanNetworkServerarm32" { platforms = ["linux/arm/v7"] } +target "LoRaWanNetworkServerarm64v8" { + inherits = ["LoRaWanNetworkServer"] + dockerfile = "LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.arm64v8" + tags = ["${CONTAINER_REGISTRY_ADDRESS}/lorawannetworksrvmodule:${NET_SRV_VERSION}-arm64v8"] + platforms = ["linux/arm64"] +} + target "LoraBasicsStationx64" { inherits = ["LoraBasicsStation"] dockerfile = "LoRaEngine/modules/LoRaBasicsStationModule/Dockerfile.amd64" @@ -50,3 +57,10 @@ target "LoraBasicsStationarm32v7" { tags = ["${CONTAINER_REGISTRY_ADDRESS}/lorabasicsstation:${LBS_VERSION}-arm32v7"] platforms = ["linux/arm/v7"] } + +target "LoraBasicsStationarm64v8" { + inherits = ["LoraBasicsStation"] + dockerfile = "LoRaEngine/modules/LoRaBasicsStationModule/Dockerfile.arm64v8" + tags = ["${CONTAINER_REGISTRY_ADDRESS}/lorabasicsstation:${LBS_VERSION}-arm64v8"] + platforms = ["linux/arm64"] +} diff --git a/LoRaEngine/example.env b/LoRaEngine/example.env index fb5343765f..5d3a3152dc 100644 --- a/LoRaEngine/example.env +++ b/LoRaEngine/example.env @@ -18,7 +18,7 @@ EDGEHUB_ROUTE='FROM /* INTO $upstream' ################################## # LoRaWanNetworkSrvModule settings ################################## -NET_SRV_VERSION=1.0.7 +NET_SRV_VERSION=2.1.0 NET_SRV_LOG_LEVEL=Error NET_SRV_LOG_TO_HUB=false NET_SRV_LOG_TO_TCP=false @@ -38,7 +38,7 @@ FACADE_AUTH_CODE=yourauthcode RESET_PIN=7 # not yet implemented LBS_SPI_SPEED=2 LBS_TC_URI=ws://192.168.0.10:5000 -LBS_VERSION=1.0.6 +LBS_VERSION=2.1.0 #SPI DEV version information. Only needed for x86 gateways. Possible values are 1 or 2. LBS_SPI_DEV=0 #Set a custom speed for your SPI or leave commented if not in use. Supported custom speed: 2Mb/sec. @@ -47,7 +47,7 @@ LBS_SPI_DEV=0 ############################## # Azure Monitor settings ############################## -APPINSIGHTS_INSTRUMENTATIONKEY="" +APPLICATIONINSIGHTS_CONNECTION_STRING="" IOT_HUB_RESOURCE_ID="" LOG_ANALYTICS_WORKSPACE_ID="" LOG_ANALYTICS_SHARED_KEY="" diff --git a/LoRaEngine/modules/LoRaBasicsStationModule/Dockerfile.amd64 b/LoRaEngine/modules/LoRaBasicsStationModule/Dockerfile.amd64 index aff53d9cd0..76ebaa8a3e 100644 --- a/LoRaEngine/modules/LoRaBasicsStationModule/Dockerfile.amd64 +++ b/LoRaEngine/modules/LoRaBasicsStationModule/Dockerfile.amd64 @@ -1,23 +1,22 @@ #This docker builds a container for the LoRa Basics station on amd64 architecture -FROM amd64/debian:buster as build +# NOTE: Use either docker.io or your own registry address to build the image +ARG SOURCE_CONTAINER_REGISTRY_ADDRESS=your-registry-address.azurecr.io +FROM $SOURCE_CONTAINER_REGISTRY_ADDRESS/amd64/debian:buster as build RUN apt-get update RUN apt-get install -y git RUN apt-get install -y --no-install-recommends apt-utils build-essential -RUN git clone --branch v2.0.5 --single-branch --depth 1 https://github.com/lorabasics/basicstation.git +RUN git clone --branch v2.0.6 --single-branch --depth 1 https://github.com/lorabasics/basicstation.git WORKDIR /basicstation -# temporary fix for https://github.com/lorabasics/basicstation/issues/142 -RUN sed -i "s|mbedtls-2.6|archive/mbedtls-2.6|g" /basicstation/deps/mbedtls/prep.sh - RUN make platform=linux variant=std -FROM amd64/debian:buster-slim +FROM $SOURCE_CONTAINER_REGISTRY_ADDRESS/amd64/debian:buster-slim WORKDIR /basicstation COPY --from=build /basicstation/build-linux-std/bin/station ./station.std COPY LoRaEngine/modules/LoRaBasicsStationModule/helper-functions.sh . COPY LoRaEngine/modules/LoRaBasicsStationModule/start_basicsstation.sh . -COPY LoRaEngine/modules/LoRaBasicsStationModule/station.conf . +COPY LoRaEngine/modules/LoRaBasicsStationModule/sx1301.station.conf . COPY --from=build /basicstation/deps/lgw/platform-linux/reset_lgw.sh . COPY LICENSE . COPY ./LoRaEngine/modules/LoRaBasicsStationModule/NOTICE.txt . diff --git a/LoRaEngine/modules/LoRaBasicsStationModule/Dockerfile.arm32v7 b/LoRaEngine/modules/LoRaBasicsStationModule/Dockerfile.arm32v7 index 028aa6492b..ccca295964 100644 --- a/LoRaEngine/modules/LoRaBasicsStationModule/Dockerfile.arm32v7 +++ b/LoRaEngine/modules/LoRaBasicsStationModule/Dockerfile.arm32v7 @@ -1,24 +1,29 @@ #This docker builds a container for the LoRa Basics station on arm32 architecture -FROM amd64/debian:buster as build -RUN apt-get update -RUN apt-get install -y git -RUN apt-get install -y apt-utils build-essential gcc-arm-linux-gnueabihf -RUN git clone --branch v2.0.5 --single-branch --depth 1 https://github.com/lorabasics/basicstation.git -RUN mkdir -p ~/toolchain-rpi/bin/ -RUN ln -s /usr/bin/arm-linux-gnueabihf-gcc ~/toolchain-rpi/bin/arm-linux-gnueabihf-gcc -RUN ln -s /usr/bin/arm-linux-gnueabihf-ld ~/toolchain-rpi/bin/arm-linux-gnueabihf-ld -RUN ln -s /usr/bin/arm-linux-gnueabihf-ar ~/toolchain-rpi/bin/arm-linux-gnueabihf-ar -RUN ln -s /usr/bin/arm-linux-gnueabihf-objdump ~/toolchain-rpi/bin/arm-linux-gnueabihf-objdump -RUN ln -s /usr/bin/arm-linux-gnueabihf-objcopy ~/toolchain-rpi/bin/arm-linux-gnueabihf-objcopy +# NOTE: Use either docker.io or your own registry address to build the image +ARG SOURCE_CONTAINER_REGISTRY_ADDRESS=your-registry-address.azurecr.io +FROM $SOURCE_CONTAINER_REGISTRY_ADDRESS/amd64/debian:buster as build +RUN apt-get update \ + && apt-get install -y git apt-utils build-essential gcc-arm-linux-gnueabihf +RUN git clone --branch v2.0.6 --single-branch --depth 1 https://github.com/lorabasics/basicstation.git + +# creating links for allowing cross-compilation of arm-linux-gnueabihf binaries +RUN mkdir -p ~/toolchain-rpi/bin/ ~/toolchain-corecell/bin/ \ + && ln -s /usr/bin/arm-linux-gnueabihf-gcc ~/toolchain-rpi/bin/arm-linux-gnueabihf-gcc \ + && ln -s /usr/bin/arm-linux-gnueabihf-ld ~/toolchain-rpi/bin/arm-linux-gnueabihf-ld \ + && ln -s /usr/bin/arm-linux-gnueabihf-ar ~/toolchain-rpi/bin/arm-linux-gnueabihf-ar \ + && ln -s /usr/bin/arm-linux-gnueabihf-objdump ~/toolchain-rpi/bin/arm-linux-gnueabihf-objdump \ + && ln -s /usr/bin/arm-linux-gnueabihf-objcopy ~/toolchain-rpi/bin/arm-linux-gnueabihf-objcopy \ + && cp -RP ~/toolchain-rpi/bin/* ~/toolchain-corecell/bin/ WORKDIR /basicstation -# temporary fix for https://github.com/lorabasics/basicstation/issues/142 -RUN sed -i "s|mbedtls-2.6|archive/mbedtls-2.6|g" /basicstation/deps/mbedtls/prep.sh +# make corecell version +RUN make platform=corecell variant=std +RUN mkdir built && cp ./build-corecell-std/bin/station ./built/station.corecell # make standard version RUN make platform=rpi variant=std -RUN mkdir built && cp ./build-rpi-std/bin/station ./built/station.std +RUN cp ./build-rpi-std/bin/station ./built/station.std # clean before making spi speed 2 version RUN make platform=rpi variant=std clean @@ -27,11 +32,12 @@ RUN sed -i "s|8000000|2000000|g" /basicstation/deps/lgw/platform-rpi/libloragw/s RUN make platform=rpi variant=std RUN cp ./build-rpi-std/bin/station ./built/station.spispeed2 -FROM arm32v7/debian:buster +FROM $SOURCE_CONTAINER_REGISTRY_ADDRESS/arm32v7/debian:buster WORKDIR /basicstation COPY --from=build /basicstation/deps/lgw/platform-rpi/reset_lgw.sh . COPY --from=build /basicstation/built/* . -COPY LoRaEngine/modules/LoRaBasicsStationModule/station.conf . +COPY LoRaEngine/modules/LoRaBasicsStationModule/sx1301.station.conf . +COPY LoRaEngine/modules/LoRaBasicsStationModule/corecell.station.conf . COPY LoRaEngine/modules/LoRaBasicsStationModule/helper-functions.sh . COPY LoRaEngine/modules/LoRaBasicsStationModule/start_basicsstation.sh . COPY LICENSE . diff --git a/LoRaEngine/modules/LoRaBasicsStationModule/Dockerfile.arm64v8 b/LoRaEngine/modules/LoRaBasicsStationModule/Dockerfile.arm64v8 new file mode 100644 index 0000000000..57f71e94d5 --- /dev/null +++ b/LoRaEngine/modules/LoRaBasicsStationModule/Dockerfile.arm64v8 @@ -0,0 +1,47 @@ +#This docker builds a container for the LoRa Basics station on arm64v8 architecture + +# NOTE: Use either docker.io or your own registry address to build the image +ARG SOURCE_CONTAINER_REGISTRY_ADDRESS=your-registry-address.azurecr.io +FROM $SOURCE_CONTAINER_REGISTRY_ADDRESS/amd64/debian:buster as build +RUN apt-get update \ + && apt-get install -y git apt-utils build-essential gcc-aarch64-linux-gnu +RUN git clone --branch v2.0.6 --single-branch --depth 1 https://github.com/lorabasics/basicstation.git + +# creating links for allowing cross-compilation of arm-linux-gnueabihf binaries +RUN mkdir -p ~/toolchain-rpi/bin/ ~/toolchain-corecell/bin/ \ + && ln -s /usr/bin/aarch64-linux-gnu-gcc ~/toolchain-rpi/bin/aarch64-linux-gnu-gcc \ + && ln -s /usr/bin/aarch64-linux-gnu-ld ~/toolchain-rpi/bin/aarch64-linux-gnu-ld \ + && ln -s /usr/bin/aarch64-linux-gnu-ar ~/toolchain-rpi/bin/aarch64-linux-gnu-ar \ + && ln -s /usr/bin/aarch64-linux-gnu-objdump ~/toolchain-rpi/bin/aarch64-linux-gnu-objdump \ + && ln -s /usr/bin/aarch64-linux-gnu-objcopy ~/toolchain-rpi/bin/aarch64-linux-gnu-objcopy \ + && cp -RP ~/toolchain-rpi/bin/* ~/toolchain-corecell/bin/ +WORKDIR /basicstation + +# make corecell version +RUN sed -i "s|arm-linux-gnueabihf|aarch64-linux-gnu|g" /basicstation/setup.gmk +RUN make platform=corecell variant=std +RUN mkdir built && cp ./build-corecell-std/bin/station ./built/station.corecell + +# make standard version +RUN make platform=rpi variant=std +RUN cp ./build-rpi-std/bin/station ./built/station.std + +# clean before making spi speed 2 version +RUN make platform=rpi variant=std clean +# make spi speed 2 version +RUN sed -i "s|8000000|2000000|g" /basicstation/deps/lgw/platform-rpi/libloragw/src/loragw_spi.native.c +RUN make platform=rpi variant=std +RUN cp ./build-rpi-std/bin/station ./built/station.spispeed2 + +FROM $SOURCE_CONTAINER_REGISTRY_ADDRESS/arm64v8/debian:buster +WORKDIR /basicstation +COPY --from=build /basicstation/deps/lgw/platform-rpi/reset_lgw.sh . +COPY --from=build /basicstation/built/* . +COPY LoRaEngine/modules/LoRaBasicsStationModule/sx1301.station.conf . +COPY LoRaEngine/modules/LoRaBasicsStationModule/corecell.station.conf . +COPY LoRaEngine/modules/LoRaBasicsStationModule/helper-functions.sh . +COPY LoRaEngine/modules/LoRaBasicsStationModule/start_basicsstation.sh . +COPY LICENSE . +COPY ./LoRaEngine/modules/LoRaBasicsStationModule/NOTICE.txt . +RUN chmod +x ./start_basicsstation.sh +ENTRYPOINT ["./start_basicsstation.sh"] diff --git a/LoRaEngine/modules/LoRaBasicsStationModule/corecell.station.conf b/LoRaEngine/modules/LoRaBasicsStationModule/corecell.station.conf new file mode 100644 index 0000000000..031d8c400d --- /dev/null +++ b/LoRaEngine/modules/LoRaBasicsStationModule/corecell.station.conf @@ -0,0 +1,50 @@ +{ + /* If slave-X.conf present this acts as default settings */ + "SX1302_conf": { /* Actual channel plan is controlled by server */ + "lorawan_public": true, /* is default */ + "clksrc": 0, /* radio_0 provides clock to concentrator */ + "full_duplex": false, + "radio_0": { + /* freq/enable provided by LNS - only HW specific settings listed here */ + "type": "SX1250", + "rssi_offset": -215.4, + "rssi_tcomp": {"coeff_a": 0, "coeff_b": 0, "coeff_c": 20.41, "coeff_d": 2162.56, "coeff_e": 0}, + "tx_enable": true, + "antenna_gain": 0, /* antenna gain, in dBi */ + "tx_gain_lut":[ + {"rf_power": 12, "pa_gain": 0, "pwr_idx": 15}, + {"rf_power": 13, "pa_gain": 0, "pwr_idx": 16}, + {"rf_power": 14, "pa_gain": 0, "pwr_idx": 17}, + {"rf_power": 15, "pa_gain": 0, "pwr_idx": 19}, + {"rf_power": 16, "pa_gain": 0, "pwr_idx": 20}, + {"rf_power": 17, "pa_gain": 0, "pwr_idx": 22}, + {"rf_power": 18, "pa_gain": 1, "pwr_idx": 1}, + {"rf_power": 19, "pa_gain": 1, "pwr_idx": 2}, + {"rf_power": 20, "pa_gain": 1, "pwr_idx": 3}, + {"rf_power": 21, "pa_gain": 1, "pwr_idx": 4}, + {"rf_power": 22, "pa_gain": 1, "pwr_idx": 5}, + {"rf_power": 23, "pa_gain": 1, "pwr_idx": 6}, + {"rf_power": 24, "pa_gain": 1, "pwr_idx": 7}, + {"rf_power": 25, "pa_gain": 1, "pwr_idx": 9}, + {"rf_power": 26, "pa_gain": 1, "pwr_idx": 11}, + {"rf_power": 27, "pa_gain": 1, "pwr_idx": 14} + ] + }, + "radio_1": { + "type": "SX1250", + "rssi_offset": -215.4, + "rssi_tcomp": {"coeff_a": 0, "coeff_b": 0, "coeff_c": 20.41, "coeff_d": 2162.56, "coeff_e": 0}, + "tx_enable": false + } + /* chan_multiSF_X, chan_Lora_std, chan_FSK provided by LNS */ + }, + "station_conf": { + "routerIdPlaceholder": "routerIdPlaceholder", + "RX_POLL_INTV": "10ms", + "log_file": "stderr", /* "station.log" */ + "log_level": "INFO", /* XDEBUG,DEBUG,VERBOSE,INFO,NOTICE,WARNING,ERROR,CRITICAL */ + "log_size": 10000000, + "log_rotate": 3 + } +} + diff --git a/LoRaEngine/modules/LoRaBasicsStationModule/helper-functions.sh b/LoRaEngine/modules/LoRaBasicsStationModule/helper-functions.sh index f56fe78a4c..c38531cc08 100644 --- a/LoRaEngine/modules/LoRaBasicsStationModule/helper-functions.sh +++ b/LoRaEngine/modules/LoRaBasicsStationModule/helper-functions.sh @@ -81,4 +81,12 @@ conditionallySetupTc() { echo "TC_URI is set to: $TC_URI" touch tc.uri && echo "$TC_URI" > tc.uri fi -} \ No newline at end of file +} + +setLogLevel() { + if [[ -z "$LOG_LEVEL" ]]; then + echo "No custom LOG_LEVEL has been set. Defaulting to INFO." + else + sed -i "s/\"log_level\": \"INFO\",/\"log_level\":\"$LOG_LEVEL\",/g" station.conf + fi +} diff --git a/LoRaEngine/modules/LoRaBasicsStationModule/module.json b/LoRaEngine/modules/LoRaBasicsStationModule/module.json index 8a9700443a..32de48559a 100644 --- a/LoRaEngine/modules/LoRaBasicsStationModule/module.json +++ b/LoRaEngine/modules/LoRaBasicsStationModule/module.json @@ -7,7 +7,8 @@ "version": "$LBS_VERSION", "platforms": { "amd64": "./Dockerfile.amd64", - "arm32v7": "./Dockerfile.arm32v7" + "arm32v7": "./Dockerfile.arm32v7", + "arm64v8": "./Dockerfile.arm64v8" } }, "buildOptions": [], diff --git a/LoRaEngine/modules/LoRaBasicsStationModule/start_basicsstation.sh b/LoRaEngine/modules/LoRaBasicsStationModule/start_basicsstation.sh index 75c786717d..75e0f288bd 100644 --- a/LoRaEngine/modules/LoRaBasicsStationModule/start_basicsstation.sh +++ b/LoRaEngine/modules/LoRaBasicsStationModule/start_basicsstation.sh @@ -3,7 +3,6 @@ set -e export MODULE_WORKDIR=$(cd $(dirname $0) && pwd) . "$MODULE_WORKDIR/helper-functions.sh" - if [[ -z "$TC_URI" ]] && [[ -z "$CUPS_URI" ]]; then echo "One of CUPS_URI or TC_URI should be specified. Exiting..." exit 1 @@ -13,10 +12,17 @@ if [[ -z "$STATION_PATH" ]]; then STATION_PATH=/basicstation fi +if [[ "$CORECELL" == true ]]; then + cp corecell.station.conf station.conf +else + cp sx1301.station.conf station.conf +fi + resetPin setFixedStationEui conditionallySetupCups conditionallySetupTc +setLogLevel if [[ -z "$RADIODEV" ]]; then if [[ -z "$SPI_DEV" ]] || [[ $SPI_DEV == '$LBS_SPI_DEV' ]]; then @@ -26,17 +32,18 @@ if [[ -z "$RADIODEV" ]]; then export RADIODEV=/dev/spidev$SPI_DEV.0 fi -#start basestation -echo "Starting base station..." -if [[ -z "$SPI_SPEED" ]] || [[ "$SPI_SPEED" == '$LBS_SPI_SPEED' ]] || [ "$SPI_SPEED" == "8" ]; then - echo "Spi speed set to 8 mbps" +#start basics station +echo "Starting basics station..." +if [[ "$CORECELL" == true ]]; then + echo "Starting Corecell (SX1302) binary" + $STATION_PATH/station.corecell -f +elif [[ -z "$SPI_SPEED" ]] || [[ "$SPI_SPEED" == '$LBS_SPI_SPEED' ]] || [ "$SPI_SPEED" == "8" ]; then + echo "Starting SX1301 binary with spi speed set to 8 mbps" $STATION_PATH/station.std -f +elif [ "$SPI_SPEED" == "2" ]; then + echo "Starting SX1301 binary with spi speed set to 2 mbps" + $STATION_PATH/station.spispeed2 -f else - if [ "$SPI_SPEED" == "2" ]; then - echo "Spi speed set to 2 mbps" - $STATION_PATH/station.spispeed2 -f - else - echo "The value $SPI_SPEED is not supported as custom value. Supported values are 2 or 8" - exit 1; - fi + echo "The value $SPI_SPEED is not supported as custom value. Supported values are 2 or 8" + exit 1; fi diff --git a/LoRaEngine/modules/LoRaBasicsStationModule/station.conf b/LoRaEngine/modules/LoRaBasicsStationModule/sx1301.station.conf similarity index 91% rename from LoRaEngine/modules/LoRaBasicsStationModule/station.conf rename to LoRaEngine/modules/LoRaBasicsStationModule/sx1301.station.conf index c13c46ead3..3fb49229e2 100644 --- a/LoRaEngine/modules/LoRaBasicsStationModule/station.conf +++ b/LoRaEngine/modules/LoRaBasicsStationModule/sx1301.station.conf @@ -22,7 +22,7 @@ "station_conf": { "routerIdPlaceholder": "routerIdPlaceholder", "log_file": "stderr", - "log_level": "XDEBUG", /* XDEBUG,DEBUG,VERBOSE,INFO,NOTICE,WARNING,ERROR,CRITICAL */ + "log_level": "INFO", /* XDEBUG,DEBUG,VERBOSE,INFO,NOTICE,WARNING,ERROR,CRITICAL */ "log_size": 10000000, "log_rotate": 3 } diff --git a/LoRaEngine/modules/LoRaWan.NetworkServerDiscovery/LoRaWan.NetworkServerDiscovery.csproj b/LoRaEngine/modules/LoRaWan.NetworkServerDiscovery/LoRaWan.NetworkServerDiscovery.csproj new file mode 100644 index 0000000000..9b5001a507 --- /dev/null +++ b/LoRaEngine/modules/LoRaWan.NetworkServerDiscovery/LoRaWan.NetworkServerDiscovery.csproj @@ -0,0 +1,26 @@ + + + + $(TargetFramework) + enable + enable + + + + + + + + + + + + + + + PreserveNewest + Never + + + + diff --git a/LoRaEngine/modules/LoRaWan.NetworkServerDiscovery/Program.cs b/LoRaEngine/modules/LoRaWan.NetworkServerDiscovery/Program.cs new file mode 100644 index 0000000000..66308e5d95 --- /dev/null +++ b/LoRaEngine/modules/LoRaWan.NetworkServerDiscovery/Program.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using LoRaTools.NetworkServerDiscovery; +using LoRaWan; +using LoRaWan.NetworkServerDiscovery; +using Prometheus; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Configuration.AddJsonFile("appsettings.local.json", optional: true); +builder.Services.AddSingleton() + .AddSingleton() + .AddMemoryCache() + .AddApplicationInsightsTelemetry() + .AddHttpClient(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. + +app.UseWebSockets(); + +app.MapGet(ILnsDiscovery.EndpointName, async (DiscoveryService discoveryService, HttpContext httpContext, ILogger logger, CancellationToken cancellationToken) => +{ + try + { + await discoveryService.HandleDiscoveryRequestAsync(httpContext, cancellationToken); + } + catch (Exception ex) when (ExceptionFilterUtility.False(() => logger.LogError(ex, "Exception when executing discovery endpoint: '{Exception}'.", ex))) + { } +}); + +app.MapMetrics(); + +app.Run(); diff --git a/LoRaEngine/modules/LoRaWan.NetworkServerDiscovery/TagBasedLnsDiscovery.cs b/LoRaEngine/modules/LoRaWan.NetworkServerDiscovery/TagBasedLnsDiscovery.cs new file mode 100644 index 0000000000..d00fb866cb --- /dev/null +++ b/LoRaEngine/modules/LoRaWan.NetworkServerDiscovery/TagBasedLnsDiscovery.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.NetworkServerDiscovery +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Azure.Identity; + using LoRaTools; + using LoRaTools.IoTHubImpl; + using LoRaTools.NetworkServerDiscovery; + using LoRaTools.Utils; + using LoRaWan; + using Microsoft.Azure.Devices; + using Microsoft.Extensions.Caching.Memory; + using Microsoft.Extensions.Configuration; + + public sealed class TagBasedLnsDiscovery : ILnsDiscovery, IDisposable + { + private const string IotHubConnectionStringName = "IotHub"; + private const string HostName = "IotHubHostName"; + private const string NetworkTagName = "network"; + private const string LnsByNetworkCacheKey = "LnsUriByNetwork"; + private const string NetworkByStationCacheKey = "NetworkByStation"; + + private static readonly TimeSpan CacheItemExpiration = TimeSpan.FromHours(6); + private static readonly IJsonReader HostAddressReader = + JsonReader.Object(JsonReader.Property("hostAddress", + from s in JsonReader.String() + select Uri.TryCreate(s, UriKind.Absolute, out var uri) + && uri.Scheme is "ws" or "wss" + ? uri : null, + (true, null)), + JsonReader.Property("deviceId", JsonReader.String()), + (hostAddress, deviceId) => new LnsHostAddressParseResult(hostAddress, deviceId)); + + private readonly ILogger logger; + private readonly IMemoryCache memoryCache; + private readonly IDeviceRegistryManager registryManager; + private readonly Dictionary lastLnsUriByStationId = new(); + private readonly object lastLnsUriByStationIdLock = new(); + private readonly SemaphoreSlim lnsByNetworkCacheSemaphore = new SemaphoreSlim(1); + + public TagBasedLnsDiscovery(IMemoryCache memoryCache, IConfiguration configuration, ILogger logger, ILogger registryManagerLogger, IHttpClientFactory httpClientFactory) + : this(memoryCache, InitializeRegistryManager(configuration, logger, registryManagerLogger, httpClientFactory), logger) + { } + + private static IDeviceRegistryManager InitializeRegistryManager(IConfiguration configuration, ILogger logger, ILogger registryManagerLogger, IHttpClientFactory httpClientFactory) + { + var iotHubConnectionString = configuration.GetConnectionString(IotHubConnectionStringName); + if (!string.IsNullOrEmpty(iotHubConnectionString)) + { + logger.LogInformation("Using connection string based auth for IoT Hub."); + return IoTHubRegistryManager.CreateWithProvider(() => RegistryManager.CreateFromConnectionString(iotHubConnectionString), httpClientFactory, registryManagerLogger); + } + + var hostName = configuration.GetValue(HostName); + + if (string.IsNullOrEmpty(hostName)) + throw new InvalidOperationException($"Specify either 'ConnectionStrings__{IotHubConnectionStringName}' or '{HostName}'."); + + logger.LogInformation("Using managed identity based auth for IoT Hub."); + + return IoTHubRegistryManager.CreateWithProvider(() => + RegistryManager.Create(hostName, new ManagedIdentityCredential()), httpClientFactory, registryManagerLogger); + } + + internal TagBasedLnsDiscovery(IMemoryCache memoryCache, IDeviceRegistryManager registryManager, ILogger logger) + { + this.memoryCache = memoryCache; + this.registryManager = registryManager; + this.logger = logger; + } + + public async Task ResolveLnsAsync(StationEui stationEui, CancellationToken cancellationToken) + { + var twin = + await GetOrCreateAsync($"{NetworkByStationCacheKey}:{stationEui}", + _ => + { + this.logger.LogInformation("Loaded twin for station '{Station}'", stationEui); + return this.registryManager.GetTwinAsync(stationEui.ToString(), cancellationToken); + }, + null, cancellationToken); + + if (twin is null) + throw new LoRaProcessingException($"Could not find twin for station '{stationEui}'", LoRaProcessingErrorCode.TwinFetchFailed); + + var reader = new TwinCollectionReader(twin.Tags, this.logger); + var networkId = reader.ReadRequiredString(NetworkTagName); + + // Protect against SQL injection. + if (networkId.Any(n => !char.IsLetterOrDigit(n))) + throw new LoRaProcessingException("Network ID may not be empty and only contain alphanumeric characters.", LoRaProcessingErrorCode.InvalidDeviceConfiguration); + + var lnsUris = await GetOrCreateAsync( + $"{LnsByNetworkCacheKey}:{networkId}", + async _ => + { + var query = this.registryManager.FindLnsByNetworkId(networkId); + var results = new List(); + var parseFailures = new List(); + while (query.HasMoreResults) + { + var matches = await query.GetNextPageAsync(); + + var parseResult = matches.Select(hostAddressInfo => HostAddressReader.Read(hostAddressInfo)).ToList(); + + results.AddRange(parseResult.Select(r => r.HostAddress) + .Where(hostAddress => hostAddress != null) + .Cast()); + + parseFailures.AddRange(parseResult.Where(r => r.HostAddress is null) + .Select(r => r.DeviceId)); + } + + this.logger.LogInformation("Loaded {Count} LNS candidates for network '{NetworkId}'", results.Count, networkId); + + if (parseFailures.Count > 0) + this.logger.LogWarning("The following LNS in network '{NetworkId}' have a misconfigured host address: {DeviceIds}.", networkId, string.Join(',', parseFailures)); + + // Also cache if no LNS URIs are found for the given network. + // This makes sure that rogue LBS do not cause too many registry operations. + return results; + }, + this.lnsByNetworkCacheSemaphore, + cancellationToken); + + if (lnsUris.Count == 0) + throw new LoRaProcessingException($"No LNS found in network '{networkId}'.", LoRaProcessingErrorCode.LnsDiscoveryFailed); + + lock (this.lastLnsUriByStationIdLock) + { + var next = this.lastLnsUriByStationId.TryGetValue(stationEui, out var lastLnsUri) ? lnsUris[(lnsUris.FindIndex(u => u == lastLnsUri) + 1) % lnsUris.Count] : lnsUris[0]; + this.lastLnsUriByStationId[stationEui] = next; + return next; + } + } + + /// + /// Unifies all cache operations by setting the same absolute expiration on all cache entries. + /// By passing a semaphore it serializes all cache operations to avoid having two concurrent requests to the registry for the same operation. + /// + private async Task GetOrCreateAsync(string key, Func> factory, SemaphoreSlim? semaphore, CancellationToken cancellationToken) + { + if (semaphore is { } someSemaphore) + { + await someSemaphore.WaitAsync(cancellationToken); + } + + try + { + return await this.memoryCache.GetOrCreateAsync(key, ce => + { + _ = ce.SetAbsoluteExpiration(CacheItemExpiration); + return factory(ce); + }); + } + finally + { + _ = semaphore?.Release(); + } + } + + public void Dispose() => this.lnsByNetworkCacheSemaphore.Dispose(); + + private record struct LnsHostAddressParseResult(Uri? HostAddress, string DeviceId); + } +} diff --git a/LoRaEngine/modules/LoRaWan.NetworkServerDiscovery/appsettings.json b/LoRaEngine/modules/LoRaWan.NetworkServerDiscovery/appsettings.json new file mode 100644 index 0000000000..0e30a3d2de --- /dev/null +++ b/LoRaEngine/modules/LoRaWan.NetworkServerDiscovery/appsettings.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "ApplicationInsights": { + "LogLevel": { + "Default": "Information" + } + } + }, + "ConnectionStrings": { + "IotHub": "" + }, + "IotHubHostName": "" +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.amd64 b/LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.amd64 index f84075d743..c9b493e98c 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.amd64 +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.amd64 @@ -8,20 +8,24 @@ COPY global.json ./ COPY LICENSE ./ COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/NOTICE.txt ./ +WORKDIR /build/LoRaEngine/modules +COPY ["LoRaEngine/modules/LoRaWan.NetworkServerDiscovery/LoRaWan.NetworkServerDiscovery.csproj", "LoRaWan.NetworkServerDiscovery/"] + WORKDIR /build/LoRaEngine/modules/LoRaWanNetworkSrvModule COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule.csproj", "LoRaWanNetworkSrvModule/"] COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaWan.NetworkServer.csproj", "LoRaWan.NetworkServer/"] COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/LoRaWan.csproj", "LoRaWan/"] COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/LoRaTools.csproj", "LoraTools/"] -COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/Logger.csproj", "Logger/"] RUN dotnet restore "LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule.csproj" -COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger Logger COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools LoraTools COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer LoRaWan.NetworkServer COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule LoRaWanNetworkSrvModule COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan LoRaWan +WORKDIR /build/LoRaEngine/modules +COPY ./LoRaEngine/modules/LoRaWan.NetworkServerDiscovery LoRaWan.NetworkServerDiscovery + WORKDIR /build/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule RUN dotnet publish "LoRaWanNetworkSrvModule.csproj" -c Release -o out --no-restore diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.amd64.debug b/LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.amd64.debug index f6370ed174..0e99f9ad5c 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.amd64.debug +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.amd64.debug @@ -18,20 +18,24 @@ COPY .editorconfig ./ COPY LICENSE ./ COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/NOTICE.txt ./ +WORKDIR /build/LoRaEngine/modules +COPY ["LoRaEngine/modules/LoRaWan.NetworkServerDiscovery/LoRaWan.NetworkServerDiscovery.csproj", "LoRaWan.NetworkServerDiscovery/"] + WORKDIR /build/LoRaEngine/modules/LoRaWanNetworkSrvModule COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule.csproj", "LoRaWanNetworkSrvModule/"] COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaWan.NetworkServer.csproj", "LoRaWan.NetworkServer/"] COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/LoRaWan.csproj", "LoRaWan/"] COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/LoRaTools.csproj", "LoraTools/"] -COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/Logger.csproj", "Logger/"] RUN dotnet restore "LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule.csproj" -COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger Logger COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools LoraTools COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer LoRaWan.NetworkServer COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule LoRaWanNetworkSrvModule COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan LoRaWan +WORKDIR /build/LoRaEngine/modules +COPY ./LoRaEngine/modules/LoRaWan.NetworkServerDiscovery LoRaWan.NetworkServerDiscovery + WORKDIR /build/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule RUN dotnet publish "LoRaWanNetworkSrvModule.csproj" -c Release -o out --no-restore diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.arm32v7 b/LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.arm32v7 index 2fb2f13077..75c63a9c77 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.arm32v7 +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.arm32v7 @@ -8,20 +8,24 @@ COPY global.json ./ COPY LICENSE ./ COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/NOTICE.txt ./ +WORKDIR /build/LoRaEngine/modules +COPY ["LoRaEngine/modules/LoRaWan.NetworkServerDiscovery/LoRaWan.NetworkServerDiscovery.csproj", "LoRaWan.NetworkServerDiscovery/"] + WORKDIR /build/LoRaEngine/modules/LoRaWanNetworkSrvModule COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule.csproj", "LoRaWanNetworkSrvModule/"] COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaWan.NetworkServer.csproj", "LoRaWan.NetworkServer/"] COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/LoRaWan.csproj", "LoRaWan/"] COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/LoRaTools.csproj", "LoraTools/"] -COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/Logger.csproj", "Logger/"] RUN dotnet restore "LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule.csproj" -COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger Logger COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools LoraTools COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer LoRaWan.NetworkServer COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule LoRaWanNetworkSrvModule COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan LoRaWan +WORKDIR /build/LoRaEngine/modules +COPY ./LoRaEngine/modules/LoRaWan.NetworkServerDiscovery LoRaWan.NetworkServerDiscovery + WORKDIR /build/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule RUN dotnet publish "LoRaWanNetworkSrvModule.csproj" -c Release -o out --no-restore diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.arm64v8 b/LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.arm64v8 new file mode 100644 index 0000000000..a2f900c4cb --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/Dockerfile.arm64v8 @@ -0,0 +1,38 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim-amd64 AS build-env + +WORKDIR /build +COPY Directory.Build.props ./ +COPY AssemblyInfo.cs ./ +COPY .editorconfig ./ +COPY global.json ./ +COPY LICENSE ./ +COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/NOTICE.txt ./ + +WORKDIR /build/LoRaEngine/modules +COPY ["LoRaEngine/modules/LoRaWan.NetworkServerDiscovery/LoRaWan.NetworkServerDiscovery.csproj", "LoRaWan.NetworkServerDiscovery/"] + +WORKDIR /build/LoRaEngine/modules/LoRaWanNetworkSrvModule +COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule.csproj", "LoRaWanNetworkSrvModule/"] +COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaWan.NetworkServer.csproj", "LoRaWan.NetworkServer/"] +COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/LoRaWan.csproj", "LoRaWan/"] +COPY ["LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/LoRaTools.csproj", "LoraTools/"] +RUN dotnet restore "LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule.csproj" + +COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools LoraTools +COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer LoRaWan.NetworkServer +COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule LoRaWanNetworkSrvModule +COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan LoRaWan + +WORKDIR /build/LoRaEngine/modules +COPY ./LoRaEngine/modules/LoRaWan.NetworkServerDiscovery LoRaWan.NetworkServerDiscovery + +WORKDIR /build/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule + +RUN dotnet publish "LoRaWanNetworkSrvModule.csproj" -c Release -o out --no-restore + +FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim-arm64v8 +WORKDIR /app +COPY --from=build-env /build/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWanNetworkSrvModule/out/* ./ +COPY ./LoRaEngine/modules/LoRaWanNetworkSrvModule/start.sh ./ +RUN chmod +x ./start.sh +ENTRYPOINT ["./start.sh"] diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ADR/LoRAADRManagerFactory.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ADR/LoRAADRManagerFactory.cs index a450cd3685..95e65d78b8 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ADR/LoRAADRManagerFactory.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ADR/LoRAADRManagerFactory.cs @@ -42,6 +42,7 @@ private static LoRaADRInMemoryStore CurrentInMemoryStore lock (InMemoryStoreLock) { +#pragma warning disable IDE0074 // Use compound assignment #pragma warning disable CA1508 // Avoid dead conditional code // False positive. if (inMemoryStore == null) @@ -49,6 +50,7 @@ private static LoRaADRInMemoryStore CurrentInMemoryStore { inMemoryStore = new LoRaADRInMemoryStore(); } +#pragma warning restore IDE0074 // Use compound assignment } return inMemoryStore; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ApplicationInsightsTracing.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ApplicationInsightsTracing.cs new file mode 100644 index 0000000000..164e110770 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ApplicationInsightsTracing.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.NetworkServer +{ + using System; + using LoRaTools; + using Microsoft.ApplicationInsights; + using Microsoft.ApplicationInsights.DataContracts; + + public interface ITracing + { + IDisposable TrackDataMessage(); + IDisposable TrackIotHubDependency(string dependencyName, string data); + } + + internal sealed class ApplicationInsightsTracing : ITracing + { + // Equal to https://github.com/microsoft/ApplicationInsights-dotnet/blob/main/WEB/Src/DependencyCollector/DependencyCollector/Implementation/RemoteDependencyConstants.cs. + private const string IotHubDependencyTypeName = "Azure IoT Hub"; + + private readonly TelemetryClient telemetryClient; + private readonly string iotHubHostName; + private readonly string iotHubDependencySuffix; + + public ApplicationInsightsTracing(TelemetryClient telemetryClient, NetworkServerConfiguration networkServerConfiguration) + { + this.telemetryClient = telemetryClient; + this.iotHubHostName = networkServerConfiguration.IoTHubHostName; + this.iotHubDependencySuffix = networkServerConfiguration.EnableGateway ? "(Gateway)" : "(Direct)"; + } + + public IDisposable TrackDataMessage() => this.telemetryClient.StartOperation("Data message"); + + public IDisposable TrackIotHubDependency(string dependencyName, string data) + { + var dependencyTelemetry = new DependencyTelemetry(IotHubDependencyTypeName, this.iotHubHostName, $"{dependencyName} {this.iotHubDependencySuffix}", data); + return this.telemetryClient.StartOperation(dependencyTelemetry); + } + } + + internal sealed class NoopTracing : ITracing + { + public IDisposable TrackDataMessage() => NoopDisposable.Instance; + public IDisposable TrackIotHubDependency(string dependencyName, string data) => NoopDisposable.Instance; + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/AssemblyInfo.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/AssemblyInfo.cs index 2c948b9a19..2b2840ebe5 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/AssemblyInfo.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/AssemblyInfo.cs @@ -5,3 +5,4 @@ [assembly: InternalsVisibleTo("LoRaWan.Tests.Common")] [assembly: InternalsVisibleTo("LoRaWan.Tests.Unit")] [assembly: InternalsVisibleTo("LoRaWan.Tests.Integration")] +[assembly: InternalsVisibleTo("LoRaWan.Tests.Simulation")] diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/AsyncDisposable.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/AsyncDisposable.cs new file mode 100644 index 0000000000..82dabd0f06 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/AsyncDisposable.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.NetworkServer +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + internal sealed class AsyncDisposable : IAsyncDisposable + { + public static readonly IAsyncDisposable Nop = new AsyncDisposable(_ => ValueTask.CompletedTask); + + private Func? handler; + + public AsyncDisposable(Func handler) => this.handler = handler; + + public CancellationToken CancellationToken { get; set; } + + public ValueTask DisposeAsync() => + Interlocked.CompareExchange(ref this.handler, null, this.handler) is { } handler + ? handler(CancellationToken) + : ValueTask.CompletedTask; + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/BasicsStationConfigurationService.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/BasicsStationConfigurationService.cs index 6c82035564..e999a9164d 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/BasicsStationConfigurationService.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/BasicsStationConfigurationService.cs @@ -7,6 +7,7 @@ namespace LoRaWan.NetworkServer.BasicsStation using System.Text.Json; using System.Threading; using System.Threading.Tasks; + using LoRaTools; using LoRaTools.Regions; using LoRaTools.Utils; using LoRaWan.NetworkServer.BasicsStation.JsonHandlers; @@ -70,7 +71,7 @@ private async Task GetTwinDesiredPropertiesAsync(StationEui stat LoRaProcessingErrorCode.InvalidDeviceConfiguration); } - using var client = this.loRaDeviceFactory.CreateDeviceClient(stationEui.ToString(), key); + await using var client = this.loRaDeviceFactory.CreateDeviceClient(stationEui.ToString(), key); var twin = await client.GetTwinAsync(cancellationToken); return twin.Properties.Desired; }); @@ -141,7 +142,7 @@ public async Task SetReportedPackageVersionAsync(StationEui stationEui, string p LoRaProcessingErrorCode.InvalidDeviceConfiguration); } - using var client = this.loRaDeviceFactory.CreateDeviceClient(stationEui.ToString(), key); + await using var client = this.loRaDeviceFactory.CreateDeviceClient(stationEui.ToString(), key); var twinCollection = new TwinCollection(); twinCollection[TwinProperty.Package] = package; _ = await client.UpdateReportedPropertiesAsync(twinCollection, cancellationToken); diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/BasicsStationNetworkServerStartup.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/BasicsStationNetworkServerStartup.cs index 41939efcbf..ed629d4b64 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/BasicsStationNetworkServerStartup.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/BasicsStationNetworkServerStartup.cs @@ -11,11 +11,15 @@ namespace LoRaWan.NetworkServer.BasicsStation using System.Threading.Tasks; using Logger; using LoRaTools.ADR; + using LoRaTools.CommonAPI; + using LoRaTools.NetworkServerDiscovery; using LoRaWan; + using LoRaWan.NetworkServer; using LoRaWan.NetworkServer.ADR; using LoRaWan.NetworkServer.BasicsStation.ModuleConnection; using LoRaWan.NetworkServer.BasicsStation.Processors; using Microsoft.ApplicationInsights; + using Microsoft.ApplicationInsights.AspNetCore.Extensions; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -28,6 +32,7 @@ namespace LoRaWan.NetworkServer.BasicsStation using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.ApplicationInsights; using Prometheus; + using StackExchange.Redis; internal sealed class BasicsStationNetworkServerStartup { @@ -45,76 +50,93 @@ public void ConfigureServices(IServiceCollection services) ITransportSettings[] settings = { new AmqpTransportSettings(TransportType.Amqp_Tcp_Only) }; var loraModuleFactory = new LoRaModuleClientFactory(settings); - var appInsightsKey = Configuration.GetValue("APPINSIGHTS_INSTRUMENTATIONKEY"); - var useApplicationInsights = !string.IsNullOrEmpty(appInsightsKey); + var appInsightsConnectionString = Configuration.GetValue("APPLICATIONINSIGHTS_CONNECTION_STRING"); + var useApplicationInsights = !string.IsNullOrEmpty(appInsightsConnectionString); _ = services.AddLogging(loggingBuilder => - { - _ = loggingBuilder.ClearProviders(); - var logLevel = int.TryParse(NetworkServerConfiguration.LogLevel, NumberStyles.Integer, CultureInfo.InvariantCulture, out var logLevelNum) - ? (LogLevel)logLevelNum is var level && Enum.IsDefined(typeof(LogLevel), level) ? level : throw new InvalidCastException() - : Enum.Parse(NetworkServerConfiguration.LogLevel, true); - - _ = loggingBuilder.SetMinimumLevel(logLevel); - _ = loggingBuilder.AddLoRaConsoleLogger(c => c.LogLevel = logLevel); - - if (NetworkServerConfiguration.LogToTcp) - { - _ = loggingBuilder.AddTcpLogger(new TcpLoggerConfiguration(logLevel, NetworkServerConfiguration.LogToTcpAddress, - NetworkServerConfiguration.LogToTcpPort, - NetworkServerConfiguration.GatewayID)); - } - if (NetworkServerConfiguration.LogToHub) - _ = loggingBuilder.AddIotHubLogger(c => c.LogLevel = logLevel); - - if (useApplicationInsights) - { - _ = loggingBuilder.AddApplicationInsights(appInsightsKey) - .AddFilter(string.Empty, logLevel); - _ = services.AddSingleton(_ => new TelemetryInitializer(NetworkServerConfiguration)); - } - }) - .AddMemoryCache() - .AddSingleton(NetworkServerConfiguration) - .AddSingleton(LoRaTools.CommonAPI.ApiVersion.LatestVersion) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(loraModuleFactory) - .AddSingleton() - .AddSingleton>() - .AddSingleton() - .AddSingleton() - .AddSingleton(new LoRaDeviceCacheOptions { MaxUnobservedLifetime = TimeSpan.FromDays(10), RefreshInterval = TimeSpan.FromDays(2), ValidationInterval = TimeSpan.FromMinutes(10) }) - .AddTransient() - .AddTransient() - .AddSingleton() - .AddSingleton(new RegistryMetricTagBag(NetworkServerConfiguration)) - .AddSingleton(_ => new Meter(MetricRegistry.Namespace, MetricRegistry.Version)) - .AddHostedService(sp => - new MetricExporterHostedService( - new CompositeMetricExporter(useApplicationInsights ? new ApplicationInsightsMetricExporter(sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService>()) : null, - new PrometheusMetricExporter(sp.GetRequiredService(), sp.GetRequiredService>())))); + { + _ = loggingBuilder.ClearProviders(); + var logLevel = int.TryParse(NetworkServerConfiguration.LogLevel, NumberStyles.Integer, CultureInfo.InvariantCulture, out var logLevelNum) + ? (LogLevel)logLevelNum is var level && Enum.IsDefined(typeof(LogLevel), level) ? level : throw new InvalidCastException() + : Enum.Parse(NetworkServerConfiguration.LogLevel, true); + + _ = loggingBuilder.SetMinimumLevel(logLevel); + _ = loggingBuilder.AddLoRaConsoleLogger(c => c.LogLevel = logLevel); + + if (NetworkServerConfiguration.LogToTcp) + { + _ = loggingBuilder.AddTcpLogger(new TcpLoggerConfiguration(logLevel, NetworkServerConfiguration.LogToTcpAddress, + NetworkServerConfiguration.LogToTcpPort, + NetworkServerConfiguration.GatewayID)); + } + if (NetworkServerConfiguration.LogToHub) + _ = loggingBuilder.AddIotHubLogger(c => c.LogLevel = logLevel); + + if (useApplicationInsights) + { + _ = loggingBuilder.AddApplicationInsights(telemetryConfiguration => { telemetryConfiguration.ConnectionString = appInsightsConnectionString; }, + loggerOptions => { }) + .AddFilter(string.Empty, logLevel); + _ = services.AddSingleton(_ => new TelemetryInitializer(NetworkServerConfiguration)); + } + }) + .AddMemoryCache() + .AddHttpClient() + .AddApiClient(NetworkServerConfiguration, ApiVersion.LatestVersion) + .AddSingleton(NetworkServerConfiguration) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(loraModuleFactory) + .AddSingleton() + .AddSingleton>() + .AddSingleton() + .AddSingleton() + .AddSingleton(new LoRaDeviceCacheOptions { MaxUnobservedLifetime = TimeSpan.FromDays(10), RefreshInterval = TimeSpan.FromDays(2), ValidationInterval = TimeSpan.FromMinutes(10) }) + .AddTransient() + .AddTransient() + .AddSingleton() + .AddSingleton(new RegistryMetricTagBag(NetworkServerConfiguration)) + .AddSingleton(_ => new Meter(MetricRegistry.Namespace, MetricRegistry.Version)) + .AddHostedService(sp => + new MetricExporterHostedService( + new CompositeMetricExporter(useApplicationInsights ? new ApplicationInsightsMetricExporter(sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>()) : null, + new PrometheusMetricExporter(sp.GetRequiredService(), sp.GetRequiredService>())))); if (useApplicationInsights) - _ = services.AddApplicationInsightsTelemetry(appInsightsKey); + { + _ = services.AddApplicationInsightsTelemetry(new ApplicationInsightsServiceOptions { ConnectionString = appInsightsConnectionString }) + .AddSingleton(); + } + else + { + _ = services.AddSingleton(); + } if (NetworkServerConfiguration.ClientCertificateMode is not ClientCertificateMode.NoCertificate) _ = services.AddSingleton(); + + _ = NetworkServerConfiguration.RunningAsIoTEdgeModule || NetworkServerConfiguration.IsLocalDevelopment + ? services.AddSingleton() + : services.AddHostedService() + .AddSingleton() + .AddSingleton(sp => + new RedisRemoteCallListener(ConnectionMultiplexer.Connect(NetworkServerConfiguration.RedisConnectionString), + sp.GetRequiredService>(), + sp.GetRequiredService())); } #pragma warning disable CA1822 // Mark members as static @@ -143,7 +165,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { _ = endpoints.MapMetrics(); - Map(HttpMethod.Get, BasicsStationNetworkServer.DiscoveryEndpoint, + Map(HttpMethod.Get, ILnsDiscovery.EndpointName, context => context.Request.Host.Port is BasicsStationNetworkServer.LnsPort or BasicsStationNetworkServer.LnsSecurePort, (ILnsProtocolMessageProcessor processor) => processor.HandleDiscoveryAsync); diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/ClientCertificateValidatorService.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/ClientCertificateValidatorService.cs index c41809788a..f178ad54f5 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/ClientCertificateValidatorService.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/ClientCertificateValidatorService.cs @@ -12,6 +12,7 @@ namespace LoRaWan.NetworkServer.BasicsStation using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; + using LoRaTools; using Microsoft.Extensions.Logging; internal sealed class ClientCertificateValidatorService : IClientCertificateValidatorService diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/CupsEndpoint.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/CupsEndpoint.cs index 1795852181..f401884411 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/CupsEndpoint.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/CupsEndpoint.cs @@ -7,6 +7,7 @@ namespace LoRaWan.NetworkServer.BasicsStation.JsonHandlers { using System; using System.Collections.Immutable; + using LoRaTools; public static class CupsEndpoint { diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/LnsData.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/LnsData.cs index 07a542c3c0..1c9c1cf9f7 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/LnsData.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/LnsData.cs @@ -5,6 +5,7 @@ namespace LoRaWan.NetworkServer.BasicsStation.JsonHandlers { using System; using System.Text.Json; + using LoRaTools; public static class LnsData { diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/LnsDiscovery.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/LnsDiscovery.cs deleted file mode 100644 index 4f7028fc12..0000000000 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/LnsDiscovery.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace LoRaWan.NetworkServer.BasicsStation.JsonHandlers -{ - using System; - using System.Text.Json; - - public static class LnsDiscovery - { - internal static readonly IJsonReader QueryReader = - JsonReader.Object( - JsonReader.Property("router", - JsonReader.Either(from n in JsonReader.UInt64() - select new StationEui(n), - from s in JsonReader.String() - select s.Contains(':', StringComparison.Ordinal) - ? Id6.TryParse(s, out var id6) ? new StationEui(id6) : throw new JsonException() - : Hexadecimal.TryParse(s, out ulong hhd, '-') ? new StationEui(hhd) : throw new JsonException()))); - - /// - /// Writes the response for Discovery endpoint as a JSON string. - /// - /// The write to use for serialization. - /// The of the querying basic station. - /// The identity of the LNS Data endpoint ( formatted). - /// The URI of the LNS Data endpoint. - public static void WriteResponse(Utf8JsonWriter writer, StationEui router, string muxs, Uri url) - { - if (writer == null) throw new ArgumentNullException(nameof(writer)); - if (!Id6.TryParse(muxs, out _)) throw new ArgumentException("Argument should be a string in ID6 format.", nameof(muxs)); - if (url is null) throw new ArgumentNullException(nameof(url)); - - writer.WriteStartObject(); - writer.WriteString("router", Id6.Format(router.AsUInt64, Id6.FormatOptions.Lowercase)); - writer.WriteString("muxs", muxs); - writer.WriteString("uri", url.ToString()); - writer.WriteEndObject(); - } - - public static void WriteResponse(Utf8JsonWriter writer, StationEui router, string error) - { - if (writer == null) throw new ArgumentNullException(nameof(writer)); - - writer.WriteStartObject(); - writer.WriteString("router", Id6.Format(router.AsUInt64, Id6.FormatOptions.Lowercase)); - writer.WriteString("error", error); - writer.WriteEndObject(); - } - } -} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/LnsStationConfiguration.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/LnsStationConfiguration.cs index 34dd6e5478..541c41ab63 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/LnsStationConfiguration.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/LnsStationConfiguration.cs @@ -9,6 +9,7 @@ namespace LoRaWan.NetworkServer.BasicsStation.JsonHandlers using System.Linq; using System.Text; using System.Text.Json; + using LoRaTools; using LoRaTools.Regions; internal static class LnsStationConfiguration diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/LocalLnsDiscovery.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/LocalLnsDiscovery.cs new file mode 100644 index 0000000000..fac105a9f5 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/LocalLnsDiscovery.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.NetworkServer.BasicsStation +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using LoRaTools.NetworkServerDiscovery; + + public sealed class LocalLnsDiscovery : ILnsDiscovery + { + private readonly Uri lnsUri; + + public LocalLnsDiscovery(Uri lnsUri) => this.lnsUri = lnsUri; + + public Task ResolveLnsAsync(StationEui stationEui, CancellationToken cancellationToken) => + Task.FromResult(this.lnsUri); + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/ModuleConnection/ModuleConnectionHost.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/ModuleConnection/ModuleConnectionHost.cs index 7d72d1f43a..e16ed71d1a 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/ModuleConnection/ModuleConnectionHost.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/ModuleConnection/ModuleConnectionHost.cs @@ -4,6 +4,8 @@ namespace LoRaWan.NetworkServer.BasicsStation.ModuleConnection { using LoRaTools.Utils; + using LoRaTools; + using LoRaWan.NetworkServer; using Microsoft.Azure.Devices.Client; using Microsoft.Azure.Devices.Client.Exceptions; using Microsoft.Azure.Devices.Shared; @@ -11,18 +13,15 @@ namespace LoRaWan.NetworkServer.BasicsStation.ModuleConnection using System; using System.Configuration; using System.Diagnostics.Metrics; - using System.Net; - using System.Text.Json; using System.Threading; using System.Threading.Tasks; - public sealed class ModuleConnectionHost : IAsyncDisposable + internal sealed class ModuleConnectionHost : IAsyncDisposable { private const string LnsVersionPropertyName = "LnsVersion"; private readonly NetworkServerConfiguration networkServerConfiguration; - private readonly IClassCDeviceMessageSender classCMessageSender; - private readonly ILoRaDeviceRegistry loRaDeviceRegistry; private readonly LoRaDeviceAPIServiceBase loRaDeviceAPIService; + private readonly ILnsRemoteCallHandler lnsRemoteCallHandler; private readonly ILogger logger; private readonly Counter unhandledExceptionCount; private ILoraModuleClient loRaModuleClient; @@ -30,17 +29,15 @@ public sealed class ModuleConnectionHost : IAsyncDisposable public ModuleConnectionHost( NetworkServerConfiguration networkServerConfiguration, - IClassCDeviceMessageSender defaultClassCDevicesMessageSender, ILoRaModuleClientFactory loRaModuleClientFactory, - ILoRaDeviceRegistry loRaDeviceRegistry, LoRaDeviceAPIServiceBase loRaDeviceAPIService, + ILnsRemoteCallHandler lnsRemoteCallHandler, ILogger logger, Meter meter) { this.networkServerConfiguration = networkServerConfiguration ?? throw new ArgumentNullException(nameof(networkServerConfiguration)); - this.classCMessageSender = defaultClassCDevicesMessageSender ?? throw new ArgumentNullException(nameof(defaultClassCDevicesMessageSender)); - this.loRaDeviceRegistry = loRaDeviceRegistry ?? throw new ArgumentNullException(nameof(loRaDeviceRegistry)); this.loRaDeviceAPIService = loRaDeviceAPIService ?? throw new ArgumentNullException(nameof(loRaDeviceAPIService)); + this.lnsRemoteCallHandler = lnsRemoteCallHandler ?? throw new ArgumentNullException(nameof(lnsRemoteCallHandler)); this.loRaModuleClientFactory = loRaModuleClientFactory ?? throw new ArgumentNullException(nameof(loRaModuleClientFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.unhandledExceptionCount = (meter ?? throw new ArgumentNullException(nameof(meter))).CreateCounter(MetricRegistry.UnhandledExceptions); @@ -86,24 +83,37 @@ internal async Task InitModuleAsync(CancellationToken cancellationToken) await this.loRaModuleClient.SetMethodDefaultHandlerAsync(OnDirectMethodCalled, null); } + // handlers on device -- to be replaced with redis subscriber internal async Task OnDirectMethodCalled(MethodRequest methodRequest, object userContext) { if (methodRequest == null) throw new ArgumentNullException(nameof(methodRequest)); try { - if (string.Equals(Constants.CloudToDeviceClearCache, methodRequest.Name, StringComparison.OrdinalIgnoreCase)) + using var cts = methodRequest.ResponseTimeout is { } someResponseTimeout ? new CancellationTokenSource(someResponseTimeout) : null; + var token = cts?.Token ?? CancellationToken.None; + + // Mapping via the constants for backwards compatibility. + LnsRemoteCall lnsRemoteCall; + if (string.Equals(NetworkServer.Constants.CloudToDeviceClearCache, methodRequest.Name, StringComparison.OrdinalIgnoreCase)) { - return await ClearCache(); + lnsRemoteCall = new LnsRemoteCall(RemoteCallKind.ClearCache, null); } - else if (string.Equals(Constants.CloudToDeviceDecoderElementName, methodRequest.Name, StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(NetworkServer.Constants.CloudToDeviceCloseConnection, methodRequest.Name, StringComparison.OrdinalIgnoreCase)) { - return await SendCloudToDeviceMessageAsync(methodRequest); + lnsRemoteCall = new LnsRemoteCall(RemoteCallKind.CloseConnection, methodRequest.DataAsJson); + } + else if (string.Equals(NetworkServer.Constants.CloudToDeviceDecoderElementName, methodRequest.Name, StringComparison.OrdinalIgnoreCase)) + { + lnsRemoteCall = new LnsRemoteCall(RemoteCallKind.CloudToDeviceMessage, methodRequest.DataAsJson); + } + else + { + throw new LoRaProcessingException($"Unknown direct method called: {methodRequest.Name}"); } - this.logger.LogError($"Unknown direct method called: {methodRequest.Name}"); - - return new MethodResponse((int)HttpStatusCode.BadRequest); + var statusCode = await lnsRemoteCallHandler.ExecuteAsync(lnsRemoteCall, token); + return new MethodResponse((int)statusCode); } catch (Exception ex) when (ExceptionFilterUtility.False(() => this.logger.LogError(ex, $"An exception occurred on a direct method call: {ex}"), () => this.unhandledExceptionCount.Add(1))) @@ -112,43 +122,6 @@ internal async Task OnDirectMethodCalled(MethodRequest methodReq } } - internal async Task SendCloudToDeviceMessageAsync(MethodRequest methodRequest) - { - if (!string.IsNullOrEmpty(methodRequest.DataAsJson)) - { - ReceivedLoRaCloudToDeviceMessage c2d = null; - - try - { - c2d = JsonSerializer.Deserialize(methodRequest.DataAsJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - } - catch (JsonException ex) - { - this.logger.LogError($"Impossible to parse Json for c2d message for device {c2d?.DevEUI}, error: {ex}"); - return new MethodResponse((int)HttpStatusCode.BadRequest); - } - - using var scope = this.logger.BeginDeviceScope(c2d.DevEUI); - this.logger.LogDebug($"received cloud to device message from direct method: {methodRequest.DataAsJson}"); - - using var cts = methodRequest.ResponseTimeout.HasValue ? new CancellationTokenSource(methodRequest.ResponseTimeout.Value) : null; - - if (await this.classCMessageSender.SendAsync(c2d, cts?.Token ?? CancellationToken.None)) - { - return new MethodResponse((int)HttpStatusCode.OK); - } - } - - return new MethodResponse((int)HttpStatusCode.BadRequest); - } - - private Task ClearCache() - { - this.loRaDeviceRegistry.ResetDeviceCache(); - - return Task.FromResult(new MethodResponse((int)HttpStatusCode.OK)); - } - /// /// Method to update the desired properties. /// We only want to update the auth code if the facadeUri was performed. @@ -181,12 +154,27 @@ private bool TryUpdateConfigurationWithDesiredProperties(TwinCollection desiredP } var reader = new TwinCollectionReader(desiredProperties, this.logger); - if (reader.TryRead(Constants.FacadeServerUrlKey, out var faceServerUrl)) + + if (reader.TryRead(NetworkServer.Constants.ProcessingDelayKey, out var processingDelay)) + { + if (processingDelay >= 0) + { + this.logger.LogDebug("Updating processing delay for LNS to {ProcessingDelay} from desired properties of the module twin", processingDelay); + this.networkServerConfiguration.ProcessingDelayInMilliseconds = processingDelay; + } + else + { + this.logger.LogError("Processing delay for LNS was set to an invalid value {ProcessingDelay}, " + + "using default delay of {DefaultDelay} ms", processingDelay, NetworkServer.Constants.DefaultProcessingDelayInMilliseconds); + } + } + + if (reader.TryRead(NetworkServer.Constants.FacadeServerUrlKey, out var faceServerUrl)) { if (Uri.TryCreate(faceServerUrl, UriKind.Absolute, out var url) && (url.Scheme == Uri.UriSchemeHttp || url.Scheme == Uri.UriSchemeHttps)) { this.loRaDeviceAPIService.URL = url; - if (reader.TryRead(Constants.FacadeServerAuthCodeKey, out var authCode)) + if (reader.TryRead(NetworkServer.Constants.FacadeServerAuthCodeKey, out var authCode)) { this.loRaDeviceAPIService.SetAuthCode(authCode); } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/Processors/CupsProtocolMessageProcessor.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/Processors/CupsProtocolMessageProcessor.cs index e3c34925e1..efe2d6296b 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/Processors/CupsProtocolMessageProcessor.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/Processors/CupsProtocolMessageProcessor.cs @@ -14,6 +14,7 @@ namespace LoRaWan.NetworkServer.BasicsStation.Processors using System.Text.Json; using System.Threading; using System.Threading.Tasks; + using LoRaTools; using LoRaTools.CommonAPI; using LoRaWan.NetworkServer.BasicsStation.JsonHandlers; using Microsoft.AspNetCore.Http; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/Processors/LnsProtocolMessageProcessor.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/Processors/LnsProtocolMessageProcessor.cs index 6d108745e0..72d6720660 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/Processors/LnsProtocolMessageProcessor.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/Processors/LnsProtocolMessageProcessor.cs @@ -9,13 +9,13 @@ namespace LoRaWan.NetworkServer.BasicsStation.Processors { using System; using System.Diagnostics.Metrics; - using System.Linq; - using System.Net.NetworkInformation; using System.Net.WebSockets; using System.Text.Json; using System.Threading; using System.Threading.Tasks; + using LoRaTools; using LoRaTools.LoRaMessage; + using LoRaTools.NetworkServerDiscovery; using LoRaWan.NetworkServer.BasicsStation.JsonHandlers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -30,9 +30,10 @@ internal class LnsProtocolMessageProcessor : ILnsProtocolMessageProcessor private readonly WebSocketWriterRegistry socketWriterRegistry; private readonly IDownstreamMessageSender downstreamMessageSender; private readonly IMessageDispatcher messageDispatcher; + private readonly ILoggerFactory loggerFactory; private readonly ILogger logger; private readonly RegistryMetricTagBag registryMetricTagBag; - private readonly Counter joinRequestCounter; + private readonly ITracing tracing; private readonly Counter uplinkMessageCounter; private readonly Counter unhandledExceptionCount; @@ -40,108 +41,47 @@ internal class LnsProtocolMessageProcessor : ILnsProtocolMessageProcessor WebSocketWriterRegistry socketWriterRegistry, IDownstreamMessageSender downstreamMessageSender, IMessageDispatcher messageDispatcher, + ILoggerFactory loggerFactory, ILogger logger, RegistryMetricTagBag registryMetricTagBag, - Meter meter) + Meter meter, + ITracing tracing) { this.basicsStationConfigurationService = basicsStationConfigurationService; this.socketWriterRegistry = socketWriterRegistry; this.downstreamMessageSender = downstreamMessageSender; this.messageDispatcher = messageDispatcher; + this.loggerFactory = loggerFactory; this.logger = logger; this.registryMetricTagBag = registryMetricTagBag; - this.joinRequestCounter = meter?.CreateCounter(MetricRegistry.JoinRequests); + this.tracing = tracing; this.uplinkMessageCounter = meter?.CreateCounter(MetricRegistry.D2CMessagesReceived); this.unhandledExceptionCount = meter?.CreateCounter(MetricRegistry.UnhandledExceptions); } - internal async Task ProcessIncomingRequestAsync(HttpContext httpContext, - Func handler, - CancellationToken cancellationToken) - { - if (httpContext is null) throw new ArgumentNullException(nameof(httpContext)); - - if (!httpContext.WebSockets.IsWebSocketRequest) - { - httpContext.Response.StatusCode = 400; - return httpContext; - } - - try + public Task HandleDiscoveryAsync(HttpContext httpContext, CancellationToken cancellationToken) => + ExecuteWithExceptionHandlingAsync(async () => { - using var socket = await httpContext.WebSockets.AcceptWebSocketAsync(); - this.logger.Log(LogLevel.Debug, "WebSocket connection from {RemoteIpAddress} established", httpContext.Connection.RemoteIpAddress); - - try + var uriBuilder = new UriBuilder { - await handler(httpContext, socket, cancellationToken); - await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Goodbye", cancellationToken); - } - catch (OperationCanceledException ex) -#pragma warning disable CA1508 // Avoid dead conditional code (false positive) - when (ex is { InnerException: WebSocketException { WebSocketErrorCode: WebSocketError.ConnectionClosedPrematurely } }) -#pragma warning restore CA1508 // Avoid dead conditional code - { - // This can happen if the basic station client is losing connectivity - this.logger.LogDebug(ex, ex.Message); - } - } - catch (Exception ex) when (ExceptionFilterUtility.False(() => this.logger.LogError(ex, "An exception occurred while processing requests: {Exception}.", ex), - () => this.unhandledExceptionCount?.Add(1))) - { - throw; - } + Scheme = httpContext.Request.IsHttps ? "wss" : "ws", + Host = httpContext.Request.Host.Host + }; - return httpContext; - } + if (httpContext.Request.Host.Port is { } somePort) + uriBuilder.Port = somePort; - public Task HandleDiscoveryAsync(HttpContext httpContext, CancellationToken cancellationToken) => - ProcessIncomingRequestAsync(httpContext, InternalHandleDiscoveryAsync, cancellationToken); + var discoveryService = new DiscoveryService(new LocalLnsDiscovery(uriBuilder.Uri), this.loggerFactory.CreateLogger()); + await discoveryService.HandleDiscoveryRequestAsync(httpContext, cancellationToken); + return 0; + }); public Task HandleDataAsync(HttpContext httpContext, CancellationToken cancellationToken) => - ProcessIncomingRequestAsync(httpContext, - (httpContext, socket, ct) => InternalHandleDataAsync(httpContext.Request.RouteValues, socket, ct), - cancellationToken); - - /// A boolean stating if more requests are expected on this endpoint. If false, the underlying socket should be closed. - internal async Task InternalHandleDiscoveryAsync(HttpContext httpContext, WebSocket socket, CancellationToken cancellationToken) - { - await using var message = socket.ReadTextMessages(cancellationToken); - if (!await message.MoveNextAsync()) - { - this.logger.LogWarning($"Did not receive discovery request from station."); - } - else + ExecuteWithExceptionHandlingAsync(async () => { - var json = message.Current; - var stationEui = LnsDiscovery.QueryReader.Read(json); - this.logger.LogInformation("Received discovery request from: {StationEui}", stationEui); - - try - { - var scheme = httpContext.Request.IsHttps ? "wss" : "ws"; - var url = new Uri($"{scheme}://{httpContext.Request.Host}{BasicsStationNetworkServer.DataEndpoint}/{stationEui}"); - - var networkInterface = NetworkInterface.GetAllNetworkInterfaces() - .SingleOrDefault(ni => ni.GetIPProperties() - .UnicastAddresses - .Any(info => info.Address.Equals(httpContext.Connection.LocalIpAddress))); - - var muxs = Id6.Format(networkInterface is { } someNetworkInterface - ? someNetworkInterface.GetPhysicalAddress().Convert48To64() : 0, - Id6.FormatOptions.FixedWidth); - - var response = Json.Write(w => LnsDiscovery.WriteResponse(w, stationEui, muxs, url)); - await socket.SendAsync(response, WebSocketMessageType.Text, true, cancellationToken); - } - catch (Exception ex) - { - var response = Json.Write(w => LnsDiscovery.WriteResponse(w, stationEui, ex.Message)); - await socket.SendAsync(response, WebSocketMessageType.Text, true, cancellationToken); - throw; - } - } - } + var webSocketConnection = new WebSocketConnection(httpContext, this.logger); + return await webSocketConnection.HandleAsync((httpContext, socket, ct) => InternalHandleDataAsync(httpContext.Request.RouteValues, socket, ct), cancellationToken); + }); internal async Task InternalHandleDataAsync(RouteValueDictionary routeValues, WebSocket socket, CancellationToken cancellationToken) { @@ -187,6 +127,8 @@ internal async Task InternalHandleDataAsync(RouteValueDictionary routeValues, We string json, CancellationToken cancellationToken) { + using var dataOperation = this.tracing.TrackDataMessage(); + switch (LnsData.MessageTypeReader.Read(json)) { case LnsMessageType.Version: @@ -266,6 +208,19 @@ or LnsMessageType.RunCommand } } + private async Task ExecuteWithExceptionHandlingAsync(Func> action) + { + try + { + return await action(); + } + catch (Exception ex) when (ExceptionFilterUtility.False(() => this.logger.LogError(ex, "An exception occurred while processing requests: {Exception}.", ex), + () => this.unhandledExceptionCount?.Add(1))) + { + throw; + } + } + internal async Task CloseSocketAsync(WebSocket socket, CancellationToken cancellationToken) { if (socket.State is WebSocketState.Open) diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/CloudControlHost.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/CloudControlHost.cs new file mode 100644 index 0000000000..523ecf2a5e --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/CloudControlHost.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.NetworkServer +{ + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + + internal class CloudControlHost : IHostedService + { + private readonly ILnsRemoteCallListener lnsRemoteCallListener; + private readonly ILnsRemoteCallHandler lnsRemoteCallHandler; + private readonly string[] subscriptionChannels; + + public CloudControlHost(ILnsRemoteCallListener lnsRemoteCallListener, + ILnsRemoteCallHandler lnsRemoteCallHandler, + NetworkServerConfiguration networkServerConfiguration) + { + this.lnsRemoteCallListener = lnsRemoteCallListener; + this.lnsRemoteCallHandler = lnsRemoteCallHandler; + this.subscriptionChannels = new string[] { networkServerConfiguration.GatewayID, Constants.CloudToDeviceClearCache }; + } + + + public Task StartAsync(CancellationToken cancellationToken) => + Task.WhenAll(this.subscriptionChannels.Select(c => this.lnsRemoteCallListener.SubscribeAsync(c, + remoteCall => this.lnsRemoteCallHandler.ExecuteAsync(remoteCall, cancellationToken), + cancellationToken))); + + public Task StopAsync(CancellationToken cancellationToken) => + Task.WhenAll(this.subscriptionChannels.Select(c => this.lnsRemoteCallListener.UnsubscribeAsync(c, cancellationToken))); + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ConcentratorDeduplication.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ConcentratorDeduplication.cs index e510c54ba2..026e6baddd 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ConcentratorDeduplication.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ConcentratorDeduplication.cs @@ -16,7 +16,7 @@ public sealed class ConcentratorDeduplication : IConcentratorDeduplication private readonly IMemoryCache cache; private readonly ILogger logger; - private static readonly object cacheLock = new object(); + private static readonly object CacheLock = new object(); internal sealed record DataMessageKey(DevEui DevEui, Mic Mic, ushort FCnt); @@ -71,7 +71,7 @@ private bool EnsureFirstMessageInCache(object key, LoRaRequest loRaRequest, out { var stationEui = loRaRequest.StationEui; - lock (cacheLock) + lock (CacheLock) { if (!this.cache.TryGetValue(key, out previousStation)) { diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Constants.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Constants.cs index 3efb7424ec..f0263b24db 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Constants.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Constants.cs @@ -40,6 +40,11 @@ public static class Constants /// public const string CloudToDeviceClearCache = "clearcache"; + /// + /// Property in decoder json response commanding LNS to close the connection. + /// + public const string CloudToDeviceCloseConnection = "closeconnection"; + /// /// Minimum value for device connection keep alive timeout (1 minute). /// @@ -54,5 +59,17 @@ public static class Constants /// public const string MessageAlreadyEncountered = "because message already encountered"; + /// + /// Delay (in milliseconds) to be used when message processing must be postponed by the LNS + /// which does not own the connection for a given device. + /// + public const string ProcessingDelayKey = "ProcessingDelayInMilliseconds"; + + /// + /// Default delay (in milliseconds) to be used when message processing must be postponed by the LNS + /// which does not own the connection for a given device. + /// + public const int DefaultProcessingDelayInMilliseconds = 400; + } } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DefaultClassCDevicesMessageSender.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DefaultClassCDevicesMessageSender.cs index 4a10625588..44d016358c 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DefaultClassCDevicesMessageSender.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DefaultClassCDevicesMessageSender.cs @@ -96,7 +96,7 @@ public async Task SendAsync(IReceivedLoRaCloudToDeviceMessage message, Can return false; } - var fcntDown = await frameCounterStrategy.NextFcntDown(loRaDevice, 0); + var fcntDown = await frameCounterStrategy.NextFcntDown(loRaDevice, loRaDevice.FCntUp); if (fcntDown <= 0) { this.logger.LogError("[class-c] could not obtain fcnt down for class C device"); diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DefaultLoRaDataRequestHandler.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DefaultLoRaDataRequestHandler.cs index 7af2fd2e3f..72e6bd5451 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DefaultLoRaDataRequestHandler.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DefaultLoRaDataRequestHandler.cs @@ -7,6 +7,7 @@ namespace LoRaWan.NetworkServer using System.Collections.Generic; using System.Diagnostics.Metrics; using System.Linq; + using System.Threading; using System.Threading.Tasks; using LoRaTools; using LoRaTools.ADR; @@ -32,7 +33,6 @@ public class DefaultLoRaDataRequestHandler : ILoRaDataRequestHandler private readonly Counter receiveWindowHits; private readonly Histogram d2cPayloadSizeHistogram; private readonly Counter c2dMessageTooLong; - private readonly Counter unhandledExceptionCount; private IClassCDeviceMessageSender classCDeviceMessageSender; public DefaultLoRaDataRequestHandler( @@ -60,20 +60,75 @@ public class DefaultLoRaDataRequestHandler : ILoRaDataRequestHandler this.receiveWindowHits = meter?.CreateCounter(MetricRegistry.ReceiveWindowHits); this.d2cPayloadSizeHistogram = meter?.CreateHistogram(MetricRegistry.D2CMessageSize); this.c2dMessageTooLong = meter?.CreateCounter(MetricRegistry.C2DMessageTooLong); - this.unhandledExceptionCount = meter?.CreateCounter(MetricRegistry.UnhandledExceptions); + } + + private sealed class ProcessingState + { + private readonly LoRaDevice device; + private List secondaryTasks; + + public ProcessingState(LoRaDevice device) => this.device = device; + + public ICollection SecondaryTasks => + (ICollection)this.secondaryTasks ?? Array.Empty(); + + public void Track(Task task) + { + this.secondaryTasks ??= new List(); + this.secondaryTasks.Add(task); + } + + public IAsyncDisposable DeviceConnectionActivity { get; private set; } + + public bool BeginDeviceClientConnectionActivity() + { + if (DeviceConnectionActivity is not null) + throw new InvalidOperationException(); + var activity = this.device.BeginDeviceClientConnectionActivity(); + if (activity is null) + return false; + DeviceConnectionActivity = activity; + return true; + } } public async Task ProcessRequestAsync(LoRaRequest request, LoRaDevice loRaDevice) { - if (request is null) throw new ArgumentNullException(nameof(request)); - if (loRaDevice is null) throw new ArgumentNullException(nameof(loRaDevice)); + ArgumentNullException.ThrowIfNull(request, nameof(request)); + ArgumentNullException.ThrowIfNull(loRaDevice, nameof(loRaDevice)); - var timeWatcher = request.GetTimeWatcher(); - using var deviceConnectionActivity = loRaDevice.BeginDeviceClientConnectionActivity(); - if (deviceConnectionActivity == null) + var processingState = new ProcessingState(loRaDevice); + + try { - return new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.DeviceClientConnectionFailed); + return await ProcessRequestAsync(request, loRaDevice, processingState); } + finally + { + if (processingState.SecondaryTasks is { Count: > 0 } secondaryTasks) + { + _ = await Task.WhenAny(Task.WhenAll(secondaryTasks)); + + var exception = secondaryTasks.GetExceptions() switch + { + { Count: 1 } exs => exs[0], + { Count: > 1 } exs => new AggregateException(exs), + _ => null + }; + + if (exception is { } someException) + this.logger.LogError(someException, "Processing of secondary tasks failed."); + } + + if (processingState.DeviceConnectionActivity is { } someDeviceConnectionActivity) + await someDeviceConnectionActivity.DisposeAsync(); + } + } + + private async Task ProcessRequestAsync(LoRaRequest request, LoRaDevice loRaDevice, + ProcessingState state) + { + var timeWatcher = request.GetTimeWatcher(); var loraPayload = (LoRaPayloadData)request.Payload; this.d2cPayloadSizeHistogram?.Record(loraPayload.Frmpayload.Length); @@ -123,14 +178,26 @@ public async Task ProcessRequestAsync(LoRaReques } var useMultipleGateways = string.IsNullOrEmpty(loRaDevice.GatewayID); - var stationEuiChanged = false; + var fcntResetSaved = false; try { #region FunctionBundler FunctionBundlerResult bundlerResult = null; - if (concentratorDeduplicationResult is ConcentratorDeduplicationResult.NotDuplicate || concentratorDeduplicationResult is ConcentratorDeduplicationResult.DuplicateDueToResubmission) - bundlerResult = await TryUseBundler(request, loRaDevice, loraPayload, useMultipleGateways); + if (useMultipleGateways + && concentratorDeduplicationResult is ConcentratorDeduplicationResult.NotDuplicate + or ConcentratorDeduplicationResult.DuplicateDueToResubmission) + { + // in the case of resubmissions we need to contact the function to get a valid frame counter down + if (CreateBundler(loraPayload, loRaDevice, request) is { } bundler) + { + if (loRaDevice.IsConnectionOwner is false && IsProcessingDelayEnabled()) + { + await DelayProcessing(); + } + bundlerResult = await TryUseBundler(bundler, loRaDevice); + } + } #endregion loRaADRResult = bundlerResult?.AdrResult; @@ -166,6 +233,11 @@ public async Task ProcessRequestAsync(LoRaReques // applying the correct deduplication if (bundlerResult?.DeduplicationResult != null && !bundlerResult.DeduplicationResult.CanProcess) { + if (IsProcessingDelayEnabled()) + { + loRaDevice.IsConnectionOwner = false; + await loRaDevice.CloseConnectionAsync(CancellationToken.None); + } // duplication strategy is indicating that we do not need to continue processing this message this.logger.LogDebug($"duplication strategy indicated to not process message: {payloadFcnt}"); return new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.DeduplicationDrop); @@ -178,6 +250,19 @@ public async Task ProcessRequestAsync(LoRaReques loRaDevice.UpdateRegion(request.Region.LoRaRegion, acceptChanges: false); } + loRaDevice.IsConnectionOwner = true; + if (!state.BeginDeviceClientConnectionActivity()) + { + return new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.DeviceClientConnectionFailed); + } + + // saving fcnt reset changes + if (isFrameCounterFromNewlyStartedDevice) + { + await SaveChangesToDeviceAsync(loRaDevice, isFrameCounterFromNewlyStartedDevice); + fcntResetSaved = true; + } + #region FrameCounterDown // if deduplication already processed the next framecounter down, use that var fcntDown = loRaADRResult?.FCntDown != null ? loRaADRResult.FCntDown : bundlerResult?.NextFCntDown; @@ -297,7 +382,7 @@ public async Task ProcessRequestAsync(LoRaReques if (!fcntDown.HasValue || fcntDown <= 0) { // We did not get a valid frame count down, therefore we should not process the message - _ = cloudToDeviceMessage.AbandonAsync(); + state.Track(cloudToDeviceMessage.AbandonAsync()); cloudToDeviceMessage = null; } @@ -308,7 +393,10 @@ public async Task ProcessRequestAsync(LoRaReques } else { - SendClassCDeviceMessage(decodePayloadResult.CloudToDeviceMessage); + if (this.classCDeviceMessageSender != null) + { + state.Track(this.classCDeviceMessageSender.SendAsync(decodePayloadResult.CloudToDeviceMessage)); + } } } } @@ -384,7 +472,7 @@ public async Task ProcessRequestAsync(LoRaReques if (downlinkMessageBuilderResp.DownlinkMessage != null) { this.receiveWindowHits?.Add(1, KeyValuePair.Create(MetricRegistry.ReceiveWindowTagName, (object)downlinkMessageBuilderResp.ReceiveWindow)); - _ = request.DownstreamMessageSender.SendDownstreamAsync(downlinkMessageBuilderResp.DownlinkMessage); + await request.DownstreamMessageSender.SendDownstreamAsync(downlinkMessageBuilderResp.DownlinkMessage); if (cloudToDeviceMessage != null) { @@ -420,7 +508,7 @@ public async Task ProcessRequestAsync(LoRaReques if (cloudToDeviceMessage != null && !ValidateCloudToDeviceMessage(loRaDevice, request, cloudToDeviceMessage)) { // Reject cloud to device message based on result from ValidateCloudToDeviceMessage - _ = cloudToDeviceMessage.RejectAsync(); + state.Track(cloudToDeviceMessage.RejectAsync()); cloudToDeviceMessage = null; } @@ -435,7 +523,7 @@ public async Task ProcessRequestAsync(LoRaReques if (!fcntDown.HasValue || fcntDown <= 0) { // We did not get a valid frame count down, therefore we should not process the message - _ = cloudToDeviceMessage.AbandonAsync(); + state.Track(cloudToDeviceMessage.AbandonAsync()); cloudToDeviceMessage = null; } else @@ -456,7 +544,7 @@ public async Task ProcessRequestAsync(LoRaReques { fpending = true; this.logger.LogInformation($"found cloud to device message, setting fpending flag, message id: {additionalMsg.MessageId ?? "undefined"}"); - _ = additionalMsg.AbandonAsync(); + state.Track(additionalMsg.AbandonAsync()); } } } @@ -468,7 +556,6 @@ public async Task ProcessRequestAsync(LoRaReques && loRaDevice.LastProcessingStationEui != request.StationEui) { loRaDevice.SetLastProcessingStationEui(request.StationEui); - stationEuiChanged = true; } // No C2D message and request was not confirmed, return nothing @@ -486,24 +573,24 @@ public async Task ProcessRequestAsync(LoRaReques { this.receiveWindowMissed?.Add(1); this.logger.LogInformation($"out of time for downstream message, will abandon cloud to device message id: {cloudToDeviceMessage.MessageId ?? "undefined"}"); - _ = cloudToDeviceMessage.AbandonAsync(); + state.Track(cloudToDeviceMessage.AbandonAsync()); } else if (confirmDownlinkMessageBuilderResp.IsMessageTooLong) { this.c2dMessageTooLong?.Add(1); this.logger.LogError($"payload will not fit in current receive window, will abandon cloud to device message id: {cloudToDeviceMessage.MessageId ?? "undefined"}"); - _ = cloudToDeviceMessage.AbandonAsync(); + state.Track(cloudToDeviceMessage.AbandonAsync()); } else { - _ = cloudToDeviceMessage.CompleteAsync(); + state.Track(cloudToDeviceMessage.CompleteAsync()); } } if (confirmDownlinkMessageBuilderResp.DownlinkMessage != null) { this.receiveWindowHits?.Add(1, KeyValuePair.Create(MetricRegistry.ReceiveWindowTagName, (object)confirmDownlinkMessageBuilderResp.ReceiveWindow)); - _ = SendMessageDownstreamAsync(request, confirmDownlinkMessageBuilderResp); + await SendMessageDownstreamAsync(request, confirmDownlinkMessageBuilderResp); } return new LoRaDeviceRequestProcessResult(loRaDevice, request, confirmDownlinkMessageBuilderResp.DownlinkMessage); @@ -511,22 +598,12 @@ public async Task ProcessRequestAsync(LoRaReques } finally { - try - { - await SaveChangesToDeviceAsync(loRaDevice, stationEuiChanged); - } - catch (OperationCanceledException saveChangesException) - { - this.logger.LogError($"error updating reported properties. {saveChangesException.Message}"); - } - catch (ArgumentOutOfRangeException ex) - { - this.logger.LogError($"The device properties are out of range. {ex.Message}"); - } + if (loRaDevice.IsConnectionOwner is true) + state.Track(SaveChangesToDeviceAsync(loRaDevice, isFrameCounterFromNewlyStartedDevice && !fcntResetSaved)); } } - protected virtual DownlinkMessageBuilderResponse DownlinkMessageBuilderResponse(LoRaRequest request, LoRaDevice loRaDevice, LoRaOperationTimeWatcher timeWatcher, LoRaADRResult loRaADRResult, IReceivedLoRaCloudToDeviceMessage cloudToDeviceMessage, uint? fcntDown, bool fpending) + internal virtual DownlinkMessageBuilderResponse DownlinkMessageBuilderResponse(LoRaRequest request, LoRaDevice loRaDevice, LoRaOperationTimeWatcher timeWatcher, LoRaADRResult loRaADRResult, IReceivedLoRaCloudToDeviceMessage cloudToDeviceMessage, uint? fcntDown, bool fpending) { _ = loRaDevice ?? throw new ArgumentNullException(nameof(loRaDevice)); _ = request ?? throw new ArgumentNullException(nameof(request)); @@ -552,11 +629,11 @@ protected virtual Task SendMessageDownstreamAsync(LoRaRequest request, DownlinkM return request.DownstreamMessageSender.SendDownstreamAsync(confirmDownlinkMessageBuilderResp.DownlinkMessage); } - protected virtual async Task SaveChangesToDeviceAsync(LoRaDevice loRaDevice, bool stationEuiChanged) + internal virtual async Task SaveChangesToDeviceAsync(LoRaDevice loRaDevice, bool force) { _ = loRaDevice ?? throw new ArgumentNullException(nameof(loRaDevice)); - _ = await loRaDevice.SaveChangesAsync(force: stationEuiChanged); + _ = await loRaDevice.SaveChangesAsync(force: force); } private void HandlePreferredGatewayChanges( @@ -592,17 +669,7 @@ protected virtual async Task SaveChangesToDeviceAsync(LoRaDevice loRaDevice, boo public void SetClassCMessageSender(IClassCDeviceMessageSender classCMessageSender) => this.classCDeviceMessageSender = classCMessageSender; - private void SendClassCDeviceMessage(IReceivedLoRaCloudToDeviceMessage cloudToDeviceMessage) - { - if (this.classCDeviceMessageSender != null) - { - _ = TaskUtil.RunOnThreadPool(() => this.classCDeviceMessageSender.SendAsync(cloudToDeviceMessage), - ex => this.logger.LogError(ex, $"[class-c] error sending class C cloud to device message. {ex.Message}"), - this.unhandledExceptionCount); - } - } - - protected virtual async Task ReceiveCloudToDeviceAsync(LoRaDevice loRaDevice, TimeSpan timeAvailableToCheckCloudToDeviceMessages) + internal virtual async Task ReceiveCloudToDeviceAsync(LoRaDevice loRaDevice, TimeSpan timeAvailableToCheckCloudToDeviceMessages) { _ = loRaDevice ?? throw new ArgumentNullException(nameof(loRaDevice)); @@ -673,7 +740,7 @@ private bool ValidateCloudToDeviceMessage(LoRaDevice loRaDevice, LoRaRequest req return true; } - protected virtual async Task SendDeviceEventAsync(LoRaRequest request, LoRaDevice loRaDevice, LoRaOperationTimeWatcher timeWatcher, object decodedValue, bool isDuplicate, byte[] decryptedPayloadData) + internal virtual async Task SendDeviceEventAsync(LoRaRequest request, LoRaDevice loRaDevice, LoRaOperationTimeWatcher timeWatcher, object decodedValue, bool isDuplicate, byte[] decryptedPayloadData) { _ = loRaDevice ?? throw new ArgumentNullException(nameof(loRaDevice)); _ = timeWatcher ?? throw new ArgumentNullException(nameof(timeWatcher)); @@ -796,25 +863,24 @@ private void LogNotNullFrameCounterDownState(LoRaDevice loRaDevice, uint? newFcn return result; } - protected virtual async Task TryUseBundler(LoRaRequest request, LoRaDevice loRaDevice, LoRaPayloadData loraPayload, bool useMultipleGateways) + protected virtual FunctionBundler CreateBundler(LoRaPayloadData loraPayload, LoRaDevice loRaDevice, LoRaRequest request) + => this.functionBundlerProvider.CreateIfRequired(this.configuration.GatewayID, loraPayload, loRaDevice, this.deduplicationFactory, request); + + internal virtual bool IsProcessingDelayEnabled() => this.configuration.ProcessingDelayInMilliseconds > 0; + + protected virtual async Task DelayProcessing() => await Task.Delay(TimeSpan.FromMilliseconds(this.configuration.ProcessingDelayInMilliseconds)); + + protected virtual async Task TryUseBundler(FunctionBundler bundler, LoRaDevice loRaDevice) { - _ = loRaDevice ?? throw new ArgumentNullException(nameof(loRaDevice)); + ArgumentNullException.ThrowIfNull(bundler, nameof(bundler)); + ArgumentNullException.ThrowIfNull(loRaDevice, nameof(loRaDevice)); - FunctionBundlerResult bundlerResult = null; - if (useMultipleGateways) + var bundlerResult = await bundler.Execute(); + if (bundlerResult.NextFCntDown is { } nextFCntDown) { - // in the case of resubmissions we need to contact the function to get a valid frame counter down - var bundler = this.functionBundlerProvider.CreateIfRequired(this.configuration.GatewayID, loraPayload, loRaDevice, this.deduplicationFactory, request); - if (bundler != null) - { - bundlerResult = await bundler.Execute(); - if (bundlerResult.NextFCntDown.HasValue) - { - // we got a new framecounter down. Make sure this - // gets saved eventually to the twins - loRaDevice.SetFcntDown(bundlerResult.NextFCntDown.Value); - } - } + // we got a new framecounter down. Make sure this + // gets saved eventually to the twins + loRaDevice.SetFcntDown(nextFCntDown); } return bundlerResult; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DeviceLoaderSynchronizer.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DeviceLoaderSynchronizer.cs index 02a82d048a..4a6d8e9d1f 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DeviceLoaderSynchronizer.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DeviceLoaderSynchronizer.cs @@ -7,6 +7,7 @@ namespace LoRaWan.NetworkServer using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; + using LoRaTools; using Microsoft.Extensions.Logging; /// @@ -105,9 +106,10 @@ internal async Task LoadAsync() SetState(LoaderState.Finished); } } - catch (Exception) + catch (Exception ex) { NotifyQueueItemsDueToError(LoRaDeviceRequestFailedReason.ApplicationError); + this.logger.LogError(ex, "Failed to load devices"); throw; } finally @@ -118,13 +120,12 @@ internal async Task LoadAsync() protected async Task CreateDevicesAsync(IReadOnlyList devices) { - List> initTasks = null; - List> refreshTasks = null; - var deviceCreated = 0; - if (devices?.Count > 0) { - initTasks = new List>(devices.Count); + var deviceCreated = 0; + var initTasks = new List>(devices.Count); + List refreshTasks = null; + List deviceInitExceptionList = null; foreach (var foundDevice in devices) { @@ -141,10 +142,37 @@ protected async Task CreateDevicesAsync(IReadOnlyList devices) // device in cache from a previous join that we didn't complete // (lost race with another gw) - refresh the twins now and keep it // in the cache - refreshTasks ??= new List>(); - refreshTasks.Add(cachedDevice.InitializeAsync(this.configuration, CancellationToken.None)); + refreshTasks ??= new List(); + refreshTasks.Add(RefreshDeviceAsync(cachedDevice)); this.logger.LogDebug("refreshing device to fetch DevAddr"); } + else + { + // this case covers a cached device with a potentially outdated DevAddr. + // we want to disconnect it. + // if the device rejoined, a new DevAddr should have been + // generated, therefore we don't need this stale connection anymore. + // If instead it is all up to date, the connection will be re-established + // as soon as the data message is processed. + if (cachedDevice.IsConnectionOwner is true) + { + this.logger.LogDebug("stale connection owner, releasing the connection."); + cachedDevice.IsConnectionOwner = false; + } + await cachedDevice.CloseConnectionAsync(CancellationToken.None); + } + } + } + + async Task RefreshDeviceAsync(LoRaDevice device) + { + try + { + _ = await device.InitializeAsync(this.configuration, CancellationToken.None); + } + finally + { + await device.CloseConnectionAsync(CancellationToken.None); } } @@ -153,7 +181,7 @@ protected async Task CreateDevicesAsync(IReadOnlyList devices) _ = await Task.WhenAll(initTasks); if (refreshTasks != null) { - _ = await Task.WhenAll(refreshTasks); + await Task.WhenAll(refreshTasks); } } catch (LoRaProcessingException ex) when (ex.ErrorCode == LoRaProcessingErrorCode.DeviceInitializationFailed @@ -171,14 +199,37 @@ protected async Task CreateDevicesAsync(IReadOnlyList devices) { var device = await deviceTask; // run initializers - InitializeDevice(device); - deviceCreated++; + try + { + InitializeDevice(device); + deviceCreated++; + } +#pragma warning disable CA1031 // Do not catch general exception types (captured and thrown later) + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { +#pragma warning disable CA1508 // Avoid dead conditional code (false positive) + deviceInitExceptionList ??= new List(); +#pragma warning restore CA1508 // Avoid dead conditional code + deviceInitExceptionList.Add(ex); + } + finally + { + await device.CloseConnectionAsync(CancellationToken.None); + } } } } - } - CreatedDevicesCount = deviceCreated; + CreatedDevicesCount = deviceCreated; + + if (deviceInitExceptionList is { Count: > 0 } someExceptions) + throw new AggregateException(someExceptions); + } + else + { + CreatedDevicesCount = 0; + } } private void NotifyQueueItemsDueToError(LoRaDeviceRequestFailedReason loRaDeviceRequestFailedReason = LoRaDeviceRequestFailedReason.ApplicationError) diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DisposableExtensions.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DisposableExtensions.cs new file mode 100644 index 0000000000..df440c2982 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DisposableExtensions.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.NetworkServer +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + + internal static class DisposableExtensions + { + /// + /// In general implementations are not + /// expected to throw exceptions. If any of the disposable objects throw exceptions then + /// the behaviour of this method is undefined. + /// + public static async ValueTask DisposeAllAsync(this IEnumerable disposables, int concurrency) + { + ArgumentNullException.ThrowIfNull(disposables, nameof(disposables)); + if (concurrency <= 0) throw new ArgumentOutOfRangeException(nameof(concurrency), concurrency, null); + + var capacity = disposables switch + { + ICollection collection => collection.Count, + IReadOnlyCollection collection => collection.Count, + _ => (int?)null, + }; + + if (capacity is 0) // disposables collection is empty so bail out early + return; + + using var semaphore = new SemaphoreSlim(concurrency); + + var tasks = capacity is { } someCapacity ? new List(someCapacity) : new List(); + tasks.AddRange(disposables.Select(DisposeAsync)); + + // NOTE! "IAsyncDisposable.DisposeAsync" implementations are not meant to throw + // and cannot be canceled therefore it is expected that all of the tasks will + // always succeed. + + await Task.WhenAll(tasks).ConfigureAwait(false); + + async Task DisposeAsync(IAsyncDisposable device) + { + try + { + await semaphore.WaitAsync().ConfigureAwait(false); + await device.DisposeAsync(); + } + finally + { + _ = semaphore.Release(); + } + } + } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DownlinkMessageBuilder.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DownlinkMessageBuilder.cs index ee5f90ffa5..08356f9897 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DownlinkMessageBuilder.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/DownlinkMessageBuilder.cs @@ -110,7 +110,7 @@ internal static class DownlinkMessageBuilder FramePort? fport = null; var requiresDeviceAcknowlegement = false; - var macCommandType = Cid.Zero; + Cid? macCommandType = null; byte[] frmPayload = null; @@ -132,6 +132,8 @@ internal static class DownlinkMessageBuilder // Add C2D Mac commands if (macCommandsC2d?.Count > 0) { + macCommandType = macCommandsC2d.First().Cid; + foreach (var macCommand in macCommandsC2d) { macCommands.Add(macCommand); @@ -215,9 +217,6 @@ internal static class DownlinkMessageBuilder loRaDevice.ClassType, radioMetadata.UpInfo.AntennaPreference); - if (logger.IsEnabled(LogLevel.Debug)) - logger.LogDebug($"{ackLoRaMessage.MessageType} {JsonConvert.SerializeObject(downlinkMessage)}"); - return new DownlinkMessageBuilderResponse(downlinkMessage, isMessageTooLong, receiveWindow); } @@ -270,7 +269,7 @@ private static ushort ValidateAndConvert16bitFCnt(uint fcntDown) var fcntDownToSend = ValidateAndConvert16bitFCnt(fcntDown); // default fport - var macCommandType = Cid.Zero; + Cid? macCommandType = null; var rndToken = new byte[2]; RndKeysGenerator.GetBytes(rndToken); @@ -348,8 +347,6 @@ private static ushort ValidateAndConvert16bitFCnt(uint fcntDown) RxDelay0, ackLoRaMessage, LoRaDeviceClassType.C); - if (logger.IsEnabled(LogLevel.Debug)) - logger.LogDebug($"{ackLoRaMessage.MessageType} {JsonConvert.SerializeObject(loraDownLinkMessage)}"); // Class C always uses RX2. return new DownlinkMessageBuilderResponse(loraDownLinkMessage, isMessageTooLong, ReceiveWindow2); @@ -375,8 +372,6 @@ private static ushort ValidateAndConvert16bitFCnt(uint fcntDown) switch (requestedMacCommand.Cid) { case Cid.LinkCheckCmd: - case Cid.Zero: - case Cid.One: case Cid.LinkADRCmd: if (loRaRequest != null) { diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ExclusiveProcessor.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ExclusiveProcessor.cs new file mode 100644 index 0000000000..b0054f8df9 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ExclusiveProcessor.cs @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +#pragma warning disable CA1003 // Use generic event handler instances (suppressed for performance) + +namespace LoRaWan.NetworkServer +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + public sealed class ExclusiveProcessor + { + private readonly IScheduler scheduler; + private readonly IEqualityComparer comparer; + private readonly SemaphoreSlim processingLock = new(1); + private readonly List queue = new(); + + public event EventHandler? Submitted; + public event EventHandler<(T InterruptedProcessor, T InterruptingProcessor)>? Interrupted; + public event EventHandler? Processing; + public event EventHandler<(T Processor, IProcessingOutcome Outcome)>? Processed; + +#pragma warning disable CA1034 // Nested types should not be visible (tightly related) + public interface IScheduler +#pragma warning restore CA1034 // Nested types should not be visible + { + T SelectNext(IReadOnlyList processes); + } + + public static readonly IScheduler DefaultScheduler = new FifoScheduler(); + + private sealed class FifoScheduler : IScheduler + { + public T SelectNext(IReadOnlyList processes) => processes[0]; + } + + public ExclusiveProcessor() : this(DefaultScheduler) { } + + public ExclusiveProcessor(IScheduler scheduler) : this(scheduler, null) { } + + public ExclusiveProcessor(IScheduler scheduler, IEqualityComparer? comparer) + { + this.scheduler = scheduler; + this.comparer = comparer ?? EqualityComparer.Default; + } + +#pragma warning disable CA1034 // Nested types should not be visible (by design) + + public interface IProcessingOutcome + { + Task Task { get; } + DateTimeOffset SubmissionTime { get; } + TimeSpan WaitDuration { get; } + TimeSpan RunDuration { get; } + } + + public record ProcessingOutcome(TResult Result, + DateTimeOffset SubmissionTime, + TimeSpan WaitDuration, + TimeSpan RunDuration) + { +#pragma warning disable CA1062 // Validate arguments of public methods (operators don't usually throw) + public static implicit operator TResult(ProcessingOutcome outcome) => outcome.Result; +#pragma warning restore CA1062 // Validate arguments of public methods + } + +#pragma warning restore CA1034 // Nested types should not be visible + + private sealed record ProcessingTaskOutcome : + ProcessingOutcome>, + IProcessingOutcome + { + public ProcessingTaskOutcome(Task task, + DateTimeOffset submissionTime, + TimeSpan waitDuration, + TimeSpan runDuration) : + base(task, submissionTime, waitDuration, runDuration) + { } + + Task IProcessingOutcome.Task => Result; + } + + public async Task> ProcessAsync(T processor, Func> function) + { + var outcome = await TryProcessAsync(processor, function); + return new ProcessingOutcome(await outcome.Result, outcome.SubmissionTime, outcome.WaitDuration, outcome.RunDuration); + } + + public async Task>> TryProcessAsync(T processor, Func> function) + { + ArgumentNullException.ThrowIfNull(function, nameof(function)); + + var submissionTime = DateTime.UtcNow; + + lock (this.queue) + this.queue.Add(processor); + + Submitted?.Invoke(this, processor); + + while (true) + { + await this.processingLock.WaitAsync(); + + try + { + (bool, T) next = default; + + lock (this.queue) + { + var nextProcessor = this.scheduler.SelectNext(this.queue); + if (!this.comparer.Equals(nextProcessor, processor)) + next = (true, nextProcessor); + else + _ = this.queue.Remove(processor); + } + + if (next is (true, { } someNextProcessor)) + { + Interrupted?.Invoke(this, (processor, someNextProcessor)); + } + else + { + Processing?.Invoke(this, processor); + var startTime = DateTime.UtcNow; + var task = await Task.WhenAny(function()); + var endTime = DateTime.UtcNow; + var outcome = new ProcessingTaskOutcome(task, submissionTime, startTime - submissionTime, endTime - startTime); + Processed?.Invoke(this, (processor, outcome)); + return outcome; + } + } + finally + { + _ = this.processingLock.Release(); + } + } + } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/FunctionBundler/FunctionBundler.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/FunctionBundler/FunctionBundler.cs index be87f6c772..b1c2ce09db 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/FunctionBundler/FunctionBundler.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/FunctionBundler/FunctionBundler.cs @@ -32,6 +32,10 @@ public class FunctionBundler this.logger = logger; } + /// Used only from tests + internal FunctionBundler() + { } + public async Task Execute() { var result = await this.deviceApi.ExecuteFunctionBundlerAsync(this.devEui, this.request); diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/FunctionBundler/FunctionBundlerProvider.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/FunctionBundler/FunctionBundlerProvider.cs index da9c5af59f..be6aeaf757 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/FunctionBundler/FunctionBundlerProvider.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/FunctionBundler/FunctionBundlerProvider.cs @@ -13,7 +13,7 @@ public class FunctionBundlerProvider : IFunctionBundlerProvider private readonly LoRaDeviceAPIServiceBase deviceApi; private readonly ILoggerFactory loggerFactory; private readonly ILogger logger; - private static readonly List functionItems = new List + private static readonly List FunctionItems = new List { new FunctionBundlerDeduplicationExecutionItem(), new FunctionBundlerADRExecutionItem(), @@ -48,10 +48,10 @@ public class FunctionBundlerProvider : IFunctionBundlerProvider var context = new FunctionBundlerExecutionContext(gatewayId, loRaPayload.Fcnt, loRaDevice.FCntDown, loRaPayload, loRaDevice, deduplicationFactory, request); - var qualifyingExecutionItems = new List(functionItems.Count); - for (var i = 0; i < functionItems.Count; i++) + var qualifyingExecutionItems = new List(FunctionItems.Count); + for (var i = 0; i < FunctionItems.Count; i++) { - var itm = functionItems[i]; + var itm = FunctionItems[i]; if (itm.RequiresExecution(context)) { qualifyingExecutionItems.Add(itm); @@ -74,9 +74,10 @@ public class FunctionBundlerProvider : IFunctionBundlerProvider for (var i = 0; i < qualifyingExecutionItems.Count; i++) { qualifyingExecutionItems[i].Prepare(context, bundlerRequest); - this.logger.LogDebug("FunctionBundler request finished preparing."); } + this.logger.LogDebug("Finished preparing {NumberOfExecutionItems} FunctionBundler requests.", qualifyingExecutionItems.Count); + if (this.logger.IsEnabled(LogLevel.Debug)) this.logger.LogDebug($"FunctionBundler request: {JsonSerializer.Serialize(bundlerRequest)}"); diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/FunctionBundler/PreferredGatewayResult.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/FunctionBundler/PreferredGatewayResult.cs index e7e7d80d0e..97b23d95e7 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/FunctionBundler/PreferredGatewayResult.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/FunctionBundler/PreferredGatewayResult.cs @@ -10,16 +10,6 @@ namespace LoRaWan.NetworkServer /// public class PreferredGatewayResult { - [JsonIgnore] - public DevEui DevEUI { get; set; } - - [JsonProperty("DevEUI")] - public string DevEuiString - { - get => DevEUI.ToString(); - set => DevEUI = DevEui.Parse(value); - } - public uint RequestFcntUp { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/IIdentityProvider.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/IIdentityProvider.cs new file mode 100644 index 0000000000..ffad38465c --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/IIdentityProvider.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.NetworkServer +{ + public interface IIdentityProvider + { + T Identity { get; } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ILoRaDeviceClient.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ILoRaDeviceClient.cs index a37f2f9263..6116551e6a 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ILoRaDeviceClient.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ILoRaDeviceClient.cs @@ -14,7 +14,7 @@ namespace LoRaWan.NetworkServer /// LoRa device client contract /// Defines the iteractions between a LoRa device and a IoT service (Azure IoT Hub). /// - public interface ILoRaDeviceClient : IDisposable + public interface ILoRaDeviceClient : IAsyncDisposable { /// /// Gets the twin properties for the device. @@ -51,16 +51,14 @@ public interface ILoRaDeviceClient : IDisposable /// Task RejectAsync(Message cloudToDeviceMessage); - /// - /// Disconnects device client. - /// - bool Disconnect(); - /// /// Ensures the device client is connected. /// bool EnsureConnected(); - bool IsMatchingKey(string primaryKey); + /// + /// Disconnects device client. + /// + Task DisconnectAsync(CancellationToken cancellationToken); } } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ILoRaDeviceClientConnectionManager.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ILoRaDeviceClientConnectionManager.cs index cf41bc9def..13a8123459 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ILoRaDeviceClientConnectionManager.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ILoRaDeviceClientConnectionManager.cs @@ -1,25 +1,21 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +#nullable enable + namespace LoRaWan.NetworkServer { using System; + using System.Threading.Tasks; - public interface ILoRaDeviceClientConnectionManager : IDisposable + public interface ILoRaDeviceClientConnectionManager : IAsyncDisposable { - bool EnsureConnected(LoRaDevice loRaDevice); - ILoRaDeviceClient GetClient(LoRaDevice loRaDevice); - ILoRaDeviceClient GetClient(DevEui devEui); - void Release(LoRaDevice loRaDevice); - void Release(DevEui devEui); + Task ReleaseAsync(LoRaDevice loRaDevice); void Register(LoRaDevice loRaDevice, ILoRaDeviceClient loraDeviceClient); - /// - /// Tries to trigger scanning of expired items. - /// - void TryScanExpiredItems(); + IAsyncDisposable BeginDeviceClientConnectionActivity(LoRaDevice loRaDevice); } } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ILoRaDeviceRegistry.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ILoRaDeviceRegistry.cs index 43a0dd5639..45a07d3530 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ILoRaDeviceRegistry.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ILoRaDeviceRegistry.cs @@ -6,7 +6,7 @@ namespace LoRaWan.NetworkServer using System; using System.Threading.Tasks; - public interface ILoRaDeviceRegistry : IDisposable + public interface ILoRaDeviceRegistry : IAsyncDisposable { /// /// Gets devices that matches an OTAA join request. @@ -26,7 +26,7 @@ public interface ILoRaDeviceRegistry : IDisposable /// /// Resets the device cache. /// - void ResetDeviceCache(); + Task ResetDeviceCacheAsync(); /// /// Gets a where requests can be queued. diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/IServiceFacadeHttpClientProvider.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/IServiceFacadeHttpClientProvider.cs deleted file mode 100644 index 68b8bdba77..0000000000 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/IServiceFacadeHttpClientProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace LoRaWan.NetworkServer -{ - using System.Net.Http; - - /// - /// Provides a to access Service Facade API. - /// - public interface IServiceFacadeHttpClientProvider - { - /// - /// Gets the to access the function. - /// - HttpClient GetHttpClient(); - } -} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/JoinDeviceLoader.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/JoinDeviceLoader.cs index 737b497be8..0501f142f3 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/JoinDeviceLoader.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/JoinDeviceLoader.cs @@ -43,9 +43,8 @@ internal async Task LoadAsync() } return await this.deviceFactory.CreateAndRegisterAsync(this.ioTHubDevice, CancellationToken.None); } - catch (LoRaProcessingException ex) + catch (LoRaProcessingException ex) when (ExceptionFilterUtility.True(() => this.logger.LogError(ex, "join refused: error initializing OTAA device, getting twin failed"))) { - this.logger.LogError(ex, "join refused: error initializing OTAA device, getting twin failed"); this.canCache = false; return null; } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/JoinRequestMessageHandler.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/JoinRequestMessageHandler.cs index a31aef7af6..271fcb8a7f 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/JoinRequestMessageHandler.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/JoinRequestMessageHandler.cs @@ -10,6 +10,7 @@ namespace LoRaWan.NetworkServer using System.Threading; using System.Threading.Tasks; using LoRaTools; + using LoRaTools.CommonAPI; using LoRaTools.LoRaMessage; using LoRaTools.LoRaPhysical; using LoRaTools.Regions; @@ -23,6 +24,7 @@ public class JoinRequestMessageHandler : IJoinRequestMessageHandler private readonly ILoRaDeviceRegistry deviceRegistry; private readonly Counter joinRequestCounter; private readonly ILogger logger; + private readonly LoRaDeviceAPIServiceBase apiService; private readonly Counter receiveWindowHits; private readonly Counter receiveWindowMisses; private readonly Counter unhandledExceptionCount; @@ -33,6 +35,7 @@ public class JoinRequestMessageHandler : IJoinRequestMessageHandler IConcentratorDeduplication concentratorDeduplication, ILoRaDeviceRegistry deviceRegistry, ILogger logger, + LoRaDeviceAPIServiceBase apiService, Meter meter) { this.configuration = configuration; @@ -40,6 +43,7 @@ public class JoinRequestMessageHandler : IJoinRequestMessageHandler this.deviceRegistry = deviceRegistry; this.joinRequestCounter = meter?.CreateCounter(MetricRegistry.JoinRequests); this.logger = logger; + this.apiService = apiService; this.receiveWindowHits = meter?.CreateCounter(MetricRegistry.ReceiveWindowHits); this.receiveWindowMisses = meter?.CreateCounter(MetricRegistry.ReceiveWindowMisses); this.unhandledExceptionCount = meter?.CreateCounter(MetricRegistry.UnhandledExceptions); @@ -53,8 +57,13 @@ public void DispatchRequest(LoRaRequest request) internal async Task ProcessJoinRequestAsync(LoRaRequest request) { + var joinReq = (LoRaPayloadJoinRequest)request.Payload; + + var devEui = joinReq.DevEUI; + + using var scope = this.logger.BeginDeviceScope(devEui); + LoRaDevice loRaDevice = null; - var loraRegion = request.Region; try { @@ -62,17 +71,15 @@ internal async Task ProcessJoinRequestAsync(LoRaRequest request) var processingTimeout = timeWatcher.GetRemainingTimeToJoinAcceptSecondWindow() - TimeSpan.FromMilliseconds(100); using var joinAcceptCancellationToken = new CancellationTokenSource(processingTimeout > TimeSpan.Zero ? processingTimeout : TimeSpan.Zero); - var joinReq = (LoRaPayloadJoinRequest)request.Payload; - - var devEui = joinReq.DevEUI; - - using var scope = this.logger.BeginDeviceScope(devEui); - this.logger.LogInformation("join request received"); - if (this.concentratorDeduplication.CheckDuplicateJoin(request) is ConcentratorDeduplicationResult.Duplicate) + var deduplicationResult = this.concentratorDeduplication.CheckDuplicateJoin(request); + if (deduplicationResult is ConcentratorDeduplicationResult.NotDuplicate) + this.joinRequestCounter?.Add(1); + + if (deduplicationResult is ConcentratorDeduplicationResult.Duplicate) { - request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.DeduplicationDrop); + request.NotifyFailed(devEui.ToString(), LoRaDeviceRequestFailedReason.DeduplicationDrop); // we do not log here as the concentratorDeduplication service already does more detailed logging return; } @@ -94,8 +101,6 @@ internal async Task ProcessJoinRequestAsync(LoRaRequest request) var appKey = loRaDevice.AppKey.Value; - this.joinRequestCounter?.Add(1); - if (loRaDevice.AppEui != joinReq.AppEui) { this.logger.LogError("join refused: AppEUI for OTAA does not match device"); @@ -195,6 +200,8 @@ internal async Task ProcessJoinRequestAsync(LoRaRequest request) return; } + this.deviceRegistry.UpdateDeviceAfterJoin(loRaDevice, oldDevAddr); + var windowToUse = timeWatcher.ResolveJoinAcceptWindowToUse(); if (windowToUse is null) { @@ -204,8 +211,6 @@ internal async Task ProcessJoinRequestAsync(LoRaRequest request) return; } - this.deviceRegistry.UpdateDeviceAfterJoin(loRaDevice, oldDevAddr); - // Build join accept downlink message // Build the DlSettings fields that is a superposition of RX2DR and RX1DROffset field @@ -255,6 +260,7 @@ internal async Task ProcessJoinRequestAsync(LoRaRequest request) loraSpecDesiredRxDelay, null); + var loraRegion = request.Region; if (!loraRegion.TryGetDownstreamChannelFrequency(request.RadioMetadata.Frequency, upstreamDataRate: request.RadioMetadata.DataRate, deviceJoinInfo: deviceJoinInfo, downstreamFrequency: out var freq)) { this.logger.LogError("could not resolve DR and/or frequency for downstream"); @@ -264,8 +270,9 @@ internal async Task ProcessJoinRequestAsync(LoRaRequest request) var joinAcceptBytes = loRaPayloadJoinAccept.Serialize(appKey); + // For join accept messages the RX1DROffsetvalue is ignored as the join accept message carry the settings to the device. var rx1 = windowToUse is not ReceiveWindow2 - ? new ReceiveWindow(loraRegion.GetDownstreamDataRate(request.RadioMetadata.DataRate, loRaDevice.ReportedRX1DROffset), freq) + ? new ReceiveWindow(loraRegion.GetDownstreamDataRate(request.RadioMetadata.DataRate), freq) : (ReceiveWindow?)null; var rx2 = new ReceiveWindow(loraRegion.GetDownstreamRX2DataRate(this.configuration.Rx2DataRate, null, deviceJoinInfo, this.logger), @@ -283,6 +290,14 @@ internal async Task ProcessJoinRequestAsync(LoRaRequest request) this.receiveWindowHits?.Add(1, KeyValuePair.Create(MetricRegistry.ReceiveWindowTagName, (object)windowToUse)); _ = request.DownstreamMessageSender.SendDownstreamAsync(downlinkMessage); + _ = this.apiService.SendJoinNotificationAsync(new DeviceJoinNotification + { + DevAddr = devAddr, + DevEUI = devEui, + GatewayId = loRaDevice.GatewayID, + NwkSKey = nwkSKey + }, joinAcceptCancellationToken.Token); + request.NotifySucceeded(loRaDevice, downlinkMessage); if (this.logger.IsEnabled(LogLevel.Debug)) @@ -294,15 +309,19 @@ internal async Task ProcessJoinRequestAsync(LoRaRequest request) { this.logger.LogInformation("join accepted"); } - } catch (Exception ex) when (ExceptionFilterUtility.True(() => this.logger.LogError(ex, $"failed to handle join request. {ex.Message}", LogLevel.Error), - () => this.unhandledExceptionCount?.Add(1))) + () => this.unhandledExceptionCount?.Add(1))) { request.NotifyFailed(loRaDevice, ex); throw; } + finally + { + if (loRaDevice is { } someLoRaDevice) + await someLoRaDevice.CloseConnectionAsync(CancellationToken.None, true); + } } } } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LnsRemoteCallHandler.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LnsRemoteCallHandler.cs new file mode 100644 index 0000000000..ba4eb87591 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LnsRemoteCallHandler.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.NetworkServer +{ + using System.Diagnostics.Metrics; + using System.Net; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + using LoRaTools; + using Microsoft.Extensions.Logging; + + internal interface ILnsRemoteCallHandler + { + Task ExecuteAsync(LnsRemoteCall lnsRemoteCall, CancellationToken cancellationToken); + } + + internal sealed class LnsRemoteCallHandler : ILnsRemoteCallHandler + { + internal const string ClosedConnectionLog = "Device connection was closed "; + private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + + private readonly NetworkServerConfiguration networkServerConfiguration; + private readonly IClassCDeviceMessageSender classCDeviceMessageSender; + private readonly ILoRaDeviceRegistry loRaDeviceRegistry; + private readonly ILogger logger; + private readonly Counter forceClosedConnections; + + public LnsRemoteCallHandler(NetworkServerConfiguration networkServerConfiguration, + IClassCDeviceMessageSender classCDeviceMessageSender, + ILoRaDeviceRegistry loRaDeviceRegistry, + ILogger logger, + Meter meter) + { + this.networkServerConfiguration = networkServerConfiguration; + this.classCDeviceMessageSender = classCDeviceMessageSender; + this.loRaDeviceRegistry = loRaDeviceRegistry; + this.logger = logger; + this.forceClosedConnections = meter.CreateCounter(MetricRegistry.ForceClosedClientConnections); + } + + public Task ExecuteAsync(LnsRemoteCall lnsRemoteCall, CancellationToken cancellationToken) + { + return lnsRemoteCall.Kind switch + { + RemoteCallKind.CloudToDeviceMessage => SendCloudToDeviceMessageAsync(lnsRemoteCall.JsonData, cancellationToken), + RemoteCallKind.ClearCache => ClearCacheAsync(), + RemoteCallKind.CloseConnection => CloseConnectionAsync(lnsRemoteCall.JsonData, cancellationToken), + _ => throw new System.NotImplementedException(), + }; + } + + private async Task SendCloudToDeviceMessageAsync(string json, CancellationToken cancellationToken) + { + if (!string.IsNullOrEmpty(json)) + { + ReceivedLoRaCloudToDeviceMessage c2d; + + try + { + c2d = JsonSerializer.Deserialize(json, JsonSerializerOptions); + } + catch (JsonException ex) + { + this.logger.LogError(ex, $"Impossible to parse Json for c2d message, error: '{ex}'"); + return HttpStatusCode.BadRequest; + } + + using var scope = this.logger.BeginDeviceScope(c2d.DevEUI); + this.logger.LogDebug($"received cloud to device message from direct method: {json}"); + + if (await this.classCDeviceMessageSender.SendAsync(c2d, cancellationToken)) + return HttpStatusCode.OK; + } + + return HttpStatusCode.BadRequest; + } + + private async Task CloseConnectionAsync(string json, CancellationToken cancellationToken) + { + ReceivedLoRaCloudToDeviceMessage c2d; + + try + { + c2d = JsonSerializer.Deserialize(json, JsonSerializerOptions); + } + catch (JsonException ex) + { + this.logger.LogError(ex, "Unable to parse Json when attempting to close the connection."); + return HttpStatusCode.BadRequest; + } + + if (c2d == null) + { + this.logger.LogError("Missing payload when attempting to close the connection."); + return HttpStatusCode.BadRequest; + } + + if (c2d.DevEUI == null) + { + this.logger.LogError("DevEUI missing, cannot identify device to close connection for; message Id '{MessageId}'", c2d.MessageId); + return HttpStatusCode.BadRequest; + } + + using var scope = this.logger.BeginDeviceScope(c2d.DevEUI); + + var loRaDevice = await this.loRaDeviceRegistry.GetDeviceByDevEUIAsync(c2d.DevEUI.Value); + if (loRaDevice == null) + { + this.logger.LogError("Could not retrieve LoRa device; message id '{MessageId}'", c2d.MessageId); + return HttpStatusCode.NotFound; + } + + loRaDevice.IsConnectionOwner = false; + await loRaDevice.CloseConnectionAsync(cancellationToken, force: true); + + this.logger.LogInformation(ClosedConnectionLog + "from gateway with id '{GatewayId}', message id '{MessageId}'", this.networkServerConfiguration.GatewayID, c2d.MessageId); + this.forceClosedConnections.Add(1); + + return HttpStatusCode.OK; + } + + private async Task ClearCacheAsync() + { + await this.loRaDeviceRegistry.ResetDeviceCacheAsync(); + return HttpStatusCode.OK; + } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LnsRemoteCallListener.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LnsRemoteCallListener.cs new file mode 100644 index 0000000000..5ade6cbcda --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LnsRemoteCallListener.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.NetworkServer +{ + using System; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + using StackExchange.Redis; + using LoRaTools; + using Microsoft.Extensions.Logging; + using System.Diagnostics.Metrics; + + internal interface ILnsRemoteCallListener + { + Task SubscribeAsync(string lns, Func function, CancellationToken cancellationToken); + + Task UnsubscribeAsync(string lns, CancellationToken cancellationToken); + } + + internal sealed class RedisRemoteCallListener : ILnsRemoteCallListener + { + private readonly ConnectionMultiplexer redis; + private readonly ILogger logger; + private readonly Counter unhandledExceptionCount; + + public RedisRemoteCallListener(ConnectionMultiplexer redis, ILogger logger, Meter meter) + { + this.redis = redis; + this.logger = logger; + this.unhandledExceptionCount = meter.CreateCounter(MetricRegistry.UnhandledExceptions); + } + + // Cancellation token to be passed when/if a future update to SubscribeAsync is allowing to use it + public async Task SubscribeAsync(string lns, Func function, CancellationToken cancellationToken) + { + var channelMessage = await this.redis.GetSubscriber().SubscribeAsync(lns); + channelMessage.OnMessage(value => + { + try + { + if (value is { Message: { } m } && !m.IsNullOrEmpty) + { + var lnsRemoteCall = JsonSerializer.Deserialize(m.ToString()) ?? throw new InvalidOperationException("Deserialization produced an empty LnsRemoteCall."); + return function(lnsRemoteCall); + } + else + { + throw new ArgumentNullException(nameof(value)); + } + } + catch (Exception ex) when (ExceptionFilterUtility.False(() => this.logger.LogError(ex, $"An exception occurred when reacting to a Redis message: '{ex}'."), + () => this.unhandledExceptionCount.Add(1))) + { + throw; + } + }); + } + + // Cancellation token to be passed when/if a future update to UnsubscribeAsync is allowing to use it + public async Task UnsubscribeAsync(string lns, CancellationToken cancellationToken) + { + await this.redis.GetSubscriber().UnsubscribeAsync(lns); + } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaApiHttpClient.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaApiHttpClient.cs new file mode 100644 index 0000000000..4c8aeb71c7 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaApiHttpClient.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.NetworkServer +{ + using System; + using System.Net; + using System.Net.Http; + using LoRaTools.CommonAPI; + using LoRaWan.Core; + using Microsoft.Extensions.DependencyInjection; + using Polly; + + public static class LoRaApiHttpClient + { + public const string Name = nameof(LoRaApiHttpClient); + } + + public static class LoRaApiHttpClientExtensions + { + private const int NumberOfAggressiveRetries = 4; + private const int NumberOfRetries = 8; + private static readonly TimeSpan AggressiveRetryInterval = TimeSpan.FromMilliseconds(50); + private static readonly TimeSpan RetryInterval = TimeSpan.FromMilliseconds(120); + + public static IServiceCollection AddApiClient(this IServiceCollection services, + NetworkServerConfiguration configuration, + ApiVersion expectedFunctionVersion) => + AddApiClient(services, () => + { + var handler = new ServiceFacadeHttpClientHandler(expectedFunctionVersion); + + if (!string.IsNullOrEmpty(configuration.HttpsProxy)) + { + var webProxy = new WebProxy( + new Uri(configuration.HttpsProxy), + BypassOnLocal: false); + + handler.Proxy = webProxy; + handler.UseProxy = true; + } + + return handler; + }); + + internal static IServiceCollection AddApiClient(this IServiceCollection services, Func createHttpMessageHandler) + { + // This Http Client retries aggressively first to not miss the receive window if possible. + _ = services.AddHttpClient(LoRaApiHttpClient.Name) + .ConfigurePrimaryHttpMessageHandler(createHttpMessageHandler) + .AddTransientHttpErrorPolicy(policyBuilder => + policyBuilder.WaitAndRetryAsync(NumberOfRetries, i => (i <= NumberOfAggressiveRetries ? AggressiveRetryInterval : RetryInterval) * Math.Pow(2, i))); + + return services; + } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDevice.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDevice.cs index 39a57a5d79..c18e5551bd 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDevice.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDevice.cs @@ -8,6 +8,7 @@ namespace LoRaWan.NetworkServer using System.Diagnostics.Metrics; using System.Threading; using System.Threading.Tasks; + using LoRaTools; using LoRaTools.LoRaMessage; using LoRaTools.Regions; using LoRaTools.Utils; @@ -18,7 +19,7 @@ namespace LoRaWan.NetworkServer using Microsoft.Extensions.Logging.Abstractions; using static ReceiveWindowNumber; - public class LoRaDevice : IDisposable, ILoRaDeviceRequestQueue + public class LoRaDevice : IAsyncDisposable, ILoRaDeviceRequestQueue { /// /// Defines the maximum amount of times an ack resubmit will be sent. @@ -89,6 +90,8 @@ public class LoRaDevice : IDisposable, ILoRaDeviceRequestQueue public bool Supports32BitFCnt { get; set; } + public bool? IsConnectionOwner { get; set; } + private readonly ChangeTrackingProperty dataRate = new(TwinProperty.DataRate); public DataRateIndex DataRate => this.dataRate.Get(); @@ -159,7 +162,9 @@ public LoRaRegionType LoRaRegion /// /// Used to synchronize the async save operation to the twins for this particular device. /// +#pragma warning disable CA2213 // Disposable fields should be disposed (disposed in async) private readonly SemaphoreSlim syncSave = new SemaphoreSlim(1, 1); +#pragma warning restore CA2213 // Disposable fields should be disposed private readonly object processingSyncLock = new object(); private readonly Queue queuedRequests = new Queue(); @@ -189,8 +194,6 @@ public LoRaRegionType LoRaRegion private ILoRaDataRequestHandler dataRequestHandler; - private volatile int deviceClientConnectionActivityCounter; - /// /// Gets or sets a value indicating whether cloud to device messages are enabled for the device /// By default it is enabled. To disable, set the desired property "EnableC2D" to false. @@ -238,7 +241,7 @@ public virtual async Task InitializeAsync(NetworkServerConfiguration confi { _ = configuration ?? throw new ArgumentNullException(nameof(configuration)); - var connection = this.connectionManager.GetClient(this); + var connection = Client; if (connection == null) { throw new LoRaProcessingException("No connection registered.", LoRaProcessingErrorCode.DeviceInitializationFailed); @@ -334,7 +337,7 @@ public virtual async Task InitializeAsync(NetworkServerConfiguration confi DownlinkEnabled = desiredTwin.SafeRead(TwinProperty.DownlinkEnabled, DownlinkEnabled); PreferredWindow = desiredTwin.SafeRead(TwinProperty.PreferredWindow, 1) is 2 ? ReceiveWindow2 : ReceiveWindow1; - Deduplication = desiredTwin.SafeRead(TwinProperty.Deduplication, DeduplicationMode.None); + Deduplication = desiredTwin.SafeRead(TwinProperty.Deduplication, DeduplicationMode.Drop); ClassType = desiredTwin.SafeRead(TwinProperty.ClassType, LoRaDeviceClassType.A); this.preferredGatewayID = reportedTwin.ReadChangeTrackingProperty(TwinProperty.PreferredGatewayID, this.preferredGatewayID); @@ -445,10 +448,7 @@ public async Task SaveChangesAsync(TwinCollection reportedProperties = nul // before checking the current state and update again. await this.syncSave.WaitAsync(); - if (reportedProperties == null) - { - reportedProperties = new TwinCollection(); - } + reportedProperties ??= new TwinCollection(); var savedProperties = new List(); foreach (var prop in GetTrackableProperties()) @@ -481,7 +481,7 @@ public async Task SaveChangesAsync(TwinCollection reportedProperties = nul reportedProperties[TwinProperty.FCntUp] = savedFcntUp; // For class C devices this might be the only moment the connection is established - using var deviceClientActivityScope = BeginDeviceClientConnectionActivity(); + await using var deviceClientActivityScope = BeginDeviceClientConnectionActivity(); if (deviceClientActivityScope == null) { // Logging as information because the real error was logged as error @@ -489,7 +489,7 @@ public async Task SaveChangesAsync(TwinCollection reportedProperties = nul return false; } - var result = await this.connectionManager.GetClient(this).UpdateReportedPropertiesAsync(reportedProperties, default); + var result = await Client.UpdateReportedPropertiesAsync(reportedProperties, default); if (result) { InternalAcceptFrameCountChanges(savedFcntUp, savedFcntDown); @@ -634,27 +634,13 @@ internal void ResetFcnt() /// /// Ensures that the device is connected. Calls the connection manager that keeps track of device connection lifetime. + /// Most devices won't have a connection timeout, + /// in that case check without lock and return a cached disposable /// - internal virtual IDisposable BeginDeviceClientConnectionActivity() - { - // Most devices won't have a connection timeout - // In that case check without lock and return a cached disposable - if (KeepAliveTimeout == 0) - { - return NullDisposable.Instance; - } - - lock (this.processingSyncLock) - { - if (this.connectionManager.EnsureConnected(this)) - { - this.deviceClientConnectionActivityCounter++; - return new ScopedDeviceClientConnection(this); - } - } - - return null; - } + internal virtual IAsyncDisposable BeginDeviceClientConnectionActivity() => + KeepAliveTimeout == 0 + ? AsyncDisposable.Nop + : this.connectionManager.BeginDeviceClientConnectionActivity(this); /// /// Indicates whether or not we can resubmit an ack for the confirmation up message. @@ -683,15 +669,17 @@ public bool ValidateConfirmResubmit(uint payloadFcnt) } } - public Task SendEventAsync(LoRaDeviceTelemetry telemetry, Dictionary properties = null) => this.connectionManager.GetClient(this).SendEventAsync(telemetry, properties); + private ILoRaDeviceClient Client => this.connectionManager.GetClient(this); + + public Task SendEventAsync(LoRaDeviceTelemetry telemetry, Dictionary properties = null) => Client.SendEventAsync(telemetry, properties); - public Task ReceiveCloudToDeviceAsync(TimeSpan timeout) => this.connectionManager.GetClient(this).ReceiveAsync(timeout); + public Task ReceiveCloudToDeviceAsync(TimeSpan timeout) => Client.ReceiveAsync(timeout); - public Task CompleteCloudToDeviceMessageAsync(Message cloudToDeviceMessage) => this.connectionManager.GetClient(this).CompleteAsync(cloudToDeviceMessage); + public Task CompleteCloudToDeviceMessageAsync(Message cloudToDeviceMessage) => Client.CompleteAsync(cloudToDeviceMessage); - public Task AbandonCloudToDeviceMessageAsync(Message cloudToDeviceMessage) => this.connectionManager.GetClient(this).AbandonAsync(cloudToDeviceMessage); + public Task AbandonCloudToDeviceMessageAsync(Message cloudToDeviceMessage) => Client.AbandonAsync(cloudToDeviceMessage); - public Task RejectCloudToDeviceMessageAsync(Message cloudToDeviceMessage) => this.connectionManager.GetClient(this).RejectAsync(cloudToDeviceMessage); + public Task RejectCloudToDeviceMessageAsync(Message cloudToDeviceMessage) => Client.RejectAsync(cloudToDeviceMessage); /// /// Updates device on the server after a join succeeded. @@ -770,7 +758,7 @@ internal virtual async Task UpdateAfterJoinAsync(LoRaDeviceJoinUpdatePrope } } - using var activityScope = BeginDeviceClientConnectionActivity(); + await using var activityScope = BeginDeviceClientConnectionActivity(); if (activityScope == null) { // Logging as information because the real error was logged as error @@ -779,7 +767,7 @@ internal virtual async Task UpdateAfterJoinAsync(LoRaDeviceJoinUpdatePrope } var devAddrBeforeSave = DevAddr; - var succeeded = await this.connectionManager.GetClient(this).UpdateReportedPropertiesAsync(reportedProperties, cancellationToken); + var succeeded = await Client.UpdateReportedPropertiesAsync(reportedProperties, cancellationToken); // Only save if the devAddr remains the same, otherwise ignore the save if (succeeded && devAddrBeforeSave == DevAddr) @@ -938,18 +926,25 @@ async Task CoreAsync() } } - protected virtual void Dispose(bool dispose) + internal virtual async Task CloseConnectionAsync(CancellationToken cancellationToken, bool force = false) { - if (dispose) + if ((force || IsConnectionOwner is null or false) + && this.connectionManager is { } someConnectionManager) { - this.connectionManager?.Release(this); - this.syncSave.Dispose(); + await someConnectionManager.GetClient(this).DisconnectAsync(cancellationToken); } } - public void Dispose() + protected virtual async ValueTask DisposeAsyncCore() { - Dispose(true); + if (this.connectionManager is { } someConnectionManager) + await someConnectionManager.ReleaseAsync(this); + this.syncSave.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore(); GC.SuppressFinalize(this); } @@ -1003,63 +998,6 @@ internal void InternalAcceptChanges() prop.AcceptChanges(); } } - - /// - /// Ends a device client connection activity - /// Called by . - /// - private void EndDeviceClientConnectionActivity() - { - lock (this.processingSyncLock) - { - if (this.deviceClientConnectionActivityCounter == 0) - { - throw new InvalidOperationException("Cannot decrement count, already at zero"); - } - - this.deviceClientConnectionActivityCounter--; - } - } - - /// - /// Disconnects the if there is no pending activity. - /// - internal bool TryDisconnect() - { - lock (this.processingSyncLock) - { - if (this.deviceClientConnectionActivityCounter == 0) - { - return this.connectionManager.GetClient(this).Disconnect(); - } - - return false; - } - } - - /// - /// Defines a scope. - /// While a connection activity is open the connection cannot be closed. - /// - private class ScopedDeviceClientConnection : IDisposable - { - private readonly LoRaDevice loRaDevice; - - internal ScopedDeviceClientConnection(LoRaDevice loRaDevice) - { - if (loRaDevice.KeepAliveTimeout == 0) - { - throw new InvalidOperationException("Scoped device client connection can be created only for devices with a connection timeout"); - } - - this.loRaDevice = loRaDevice; - } - - public void Dispose() - { - this.loRaDevice.EndDeviceClientConnectionActivity(); - } - } } internal static class TwinReaderExtensions diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceAPIService.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceAPIService.cs index 6486319e1d..126bb925cc 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceAPIService.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceAPIService.cs @@ -12,6 +12,7 @@ namespace LoRaWan.NetworkServer using System.Text.Json; using System.Threading; using System.Threading.Tasks; + using LoRaTools; using LoRaTools.CommonAPI; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -22,19 +23,19 @@ namespace LoRaWan.NetworkServer public sealed class LoRaDeviceAPIService : LoRaDeviceAPIServiceBase { private const string PrimaryKeyPropertyName = "PrimaryKey"; - private readonly IServiceFacadeHttpClientProvider serviceFacadeHttpClientProvider; + private readonly IHttpClientFactory httpClientFactory; private readonly ILogger logger; private readonly Counter deviceLoadRequests; public LoRaDeviceAPIService(NetworkServerConfiguration configuration, - IServiceFacadeHttpClientProvider serviceFacadeHttpClientProvider, + IHttpClientFactory httpClientFactory, ILogger logger, Meter meter) : base(configuration) { if (meter is null) throw new ArgumentNullException(nameof(meter)); - this.serviceFacadeHttpClientProvider = serviceFacadeHttpClientProvider; + this.httpClientFactory = httpClientFactory; this.logger = logger; this.deviceLoadRequests = meter.CreateCounter(MetricRegistry.DeviceLoadRequests); } @@ -43,7 +44,7 @@ public override async Task NextFCntDownAsync(DevEui devEUI, uint fcntDown, { this.logger.LogDebug("syncing FCntDown for multigateway"); - var client = this.serviceFacadeHttpClientProvider.GetHttpClient(); + using var client = CreateClient(); var url = GetFullUri($"NextFCntDown?code={AuthCode}&DevEUI={devEUI}&FCntDown={fcntDown}&FCntUp={fcntUp}&GatewayId={gatewayId}"); var response = await client.GetAsync(url); if (!response.IsSuccessStatusCode) @@ -62,7 +63,7 @@ public override async Task NextFCntDownAsync(DevEui devEUI, uint fcntDown, public override async Task ExecuteFunctionBundlerAsync(DevEui devEUI, FunctionBundlerRequest request) { - var client = this.serviceFacadeHttpClientProvider.GetHttpClient(); + using var client = CreateClient(); var url = GetFullUri($"FunctionBundler/{devEUI}?code={AuthCode}"); var requestBody = JsonConvert.SerializeObject(request); @@ -81,7 +82,7 @@ public override async Task ExecuteFunctionBundlerAsync(De public override async Task ABPFcntCacheResetAsync(DevEui devEUI, uint fcntUp, string gatewayId) { - var client = this.serviceFacadeHttpClientProvider.GetHttpClient(); + using var client = CreateClient(); var url = GetFullUri($"NextFCntDown?code={AuthCode}&DevEUI={devEUI}&ABPFcntCacheReset=true&GatewayId={gatewayId}&FCntUp={fcntUp}"); var response = await client.GetAsync(url); if (!response.IsSuccessStatusCode) @@ -108,7 +109,7 @@ private async Task SearchDevicesAsync(string gatewayID = nu { this.deviceLoadRequests?.Add(1); - var client = this.serviceFacadeHttpClientProvider.GetHttpClient(); + using var client = CreateClient(); var url = BuildUri("GetDevice", new Dictionary { @@ -167,7 +168,7 @@ private async Task GetPrimaryKeyByEuiAsync(string eui) { this.deviceLoadRequests?.Add(1); - var client = this.serviceFacadeHttpClientProvider.GetHttpClient(); + using var client = CreateClient(); var url = BuildUri("GetDeviceByDevEUI", new Dictionary { ["code"] = AuthCode, @@ -218,7 +219,7 @@ internal Uri BuildUri(string relativePath, IDictionary queryPara public override async Task FetchStationCredentialsAsync(StationEui eui, ConcentratorCredentialType credentialtype, CancellationToken token) { - var client = this.serviceFacadeHttpClientProvider.GetHttpClient(); + using var client = CreateClient(); var url = BuildUri("FetchConcentratorCredentials", new Dictionary { ["code"] = AuthCode, @@ -242,7 +243,7 @@ public override async Task FetchStationCredentialsAsync(StationEui eui, public override async Task FetchStationFirmwareAsync(StationEui eui, CancellationToken token) { - var client = this.serviceFacadeHttpClientProvider.GetHttpClient(); + using var client = CreateClient(); var url = BuildUri("FetchConcentratorFirmware", new Dictionary { ["code"] = AuthCode, @@ -257,5 +258,26 @@ public override async Task FetchStationFirmwareAsync(StationEui eui return response.Content; } + + private HttpClient CreateClient() => this.httpClientFactory.CreateClient(LoRaApiHttpClient.Name); + + public override async Task SendJoinNotificationAsync(DeviceJoinNotification deviceJoinNotification, CancellationToken token) + { + using var client = CreateClient(); + const string FunctionName = "DeviceJoinNotification"; + var url = BuildUri(FunctionName, new Dictionary + { + ["code"] = AuthCode + }); + + var requestBody = JsonConvert.SerializeObject(deviceJoinNotification); + + using var content = PreparePostContent(requestBody); + using var response = await client.PostAsync(url, content, token); + if (!response.IsSuccessStatusCode) + { + this.logger.LogError($"error calling the {FunctionName} function, check the function log. {response.ReasonPhrase}"); + } + } } } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceAPIServiceBase.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceAPIServiceBase.cs index f1e643de39..dfd46a9fe9 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceAPIServiceBase.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceAPIServiceBase.cs @@ -70,6 +70,8 @@ public abstract class LoRaDeviceAPIServiceBase /// public void SetAuthCode(string value) => AuthCode = value; + public abstract Task SendJoinNotificationAsync(DeviceJoinNotification deviceJoinNotification, CancellationToken token); + public abstract Task ExecuteFunctionBundlerAsync(DevEui devEUI, FunctionBundlerRequest request); protected LoRaDeviceAPIServiceBase() diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceCache.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceCache.cs index 892468ba95..c366f49a29 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceCache.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceCache.cs @@ -16,7 +16,7 @@ namespace LoRaWan.NetworkServer using System.Threading; using System.Threading.Tasks; - public class LoRaDeviceCache : IDisposable + public class LoRaDeviceCache : IAsyncDisposable { private readonly LoRaDeviceCacheOptions options; private readonly ConcurrentDictionary> devAddrCache = new(); @@ -24,7 +24,9 @@ public class LoRaDeviceCache : IDisposable private readonly object syncLock = new object(); private readonly NetworkServerConfiguration configuration; private readonly ILogger logger; +#pragma warning disable CA2213 // Disposable fields should be disposed (false positive) private CancellationTokenSource? ctsDispose; +#pragma warning restore CA2213 // Disposable fields should be disposed private readonly StatisticsTracker statisticsTracker = new StatisticsTracker(); private readonly Counter? deviceCacheHits; @@ -46,73 +48,75 @@ public LoRaDeviceCache(LoRaDeviceCacheOptions options, NetworkServerConfiguratio private async Task RefreshCacheAsync(CancellationToken cancellationToken) { - while (!cancellationToken.IsCancellationRequested) + try { - try + while (!cancellationToken.IsCancellationRequested) { - await Task.Delay(this.options.ValidationInterval, cancellationToken); - } - catch (TaskCanceledException) - { - break; - } + try + { + await Task.Delay(this.options.ValidationInterval, cancellationToken); + } + catch (TaskCanceledException) + { + break; + } - OnRefresh(); + OnRefresh(); - // remove any devices that were not seen for the configured amount of time + // remove any devices that were not seen for the configured amount of time - var now = DateTimeOffset.UtcNow; + var now = DateTimeOffset.UtcNow; - lock (this.syncLock) - { var itemsToRemove = this.euiCache.Values.Where(x => now - x.LastSeen > this.options.MaxUnobservedLifetime); foreach (var expiredDevice in itemsToRemove) { - _ = Remove(expiredDevice); + _ = await RemoveAsync(expiredDevice); } - } - - // refresh the devices that were not refreshed within the configured time window - var itemsToRefresh = this.euiCache.Values.Where(x => now - x.LastUpdate > this.options.RefreshInterval).ToList(); - var tasks = new List(itemsToRefresh.Count); + // refresh the devices that were not refreshed within the configured time window - foreach (var item in itemsToRefresh) - { - tasks.Add(RefreshDeviceAsync(item, cancellationToken)); - } - - while (tasks.Count > 0) - { - var t = await Task.WhenAny(tasks); - _ = tasks.Remove(t); + var itemsToRefresh = this.euiCache.Values.Where(x => now - x.LastUpdate > this.options.RefreshInterval).ToList(); + var tasks = new List(itemsToRefresh.Count); - try - { - await t; - } - catch (TaskCanceledException) + foreach (var item in itemsToRefresh) { - // ignore canceled task for now (so all remaining tasks can be awaited), - // but outer loop will eventually break due to cancellation + tasks.Add(RefreshDeviceAsync(item, cancellationToken)); } - catch (LoRaProcessingException ex) + + while (tasks.Count > 0) { - // retry on next iteration - this.logger.LogError(ex, "Failed to refresh device."); + var t = await Task.WhenAny(tasks); + _ = tasks.Remove(t); + + try + { + await t; + } + catch (TaskCanceledException) + { + // ignore canceled task for now (so all remaining tasks can be awaited), + // but outer loop will eventually break due to cancellation + } + catch (LoRaProcessingException ex) + { + // retry on next iteration + this.logger.LogError(ex, "Failed to refresh device."); + } } } } + catch (Exception ex) when (ExceptionFilterUtility.False(() => this.logger.LogError(ex, "Exception when refreshing cache: {Exception}.", ex))) + { } } protected virtual async Task RefreshDeviceAsync(LoRaDevice device, CancellationToken cancellationToken) { _ = device ?? throw new ArgumentNullException(nameof(device)); - using (device.BeginDeviceClientConnectionActivity()) + await using (device.BeginDeviceClientConnectionActivity()) _ = await device.InitializeAsync(this.configuration, cancellationToken); } - public virtual bool Remove(LoRaDevice device, bool dispose = true) + public virtual async Task RemoveAsync(LoRaDevice device) { _ = device ?? throw new ArgumentNullException(nameof(device)); @@ -131,12 +135,10 @@ public virtual bool Remove(LoRaDevice device, bool dispose = true) result &= this.devAddrCache.Remove(someDevAddr, out _); } } - - if (dispose) - { - device.Dispose(); - } } + + await device.DisposeAsync(); + return result; } @@ -213,9 +215,9 @@ public void Register(LoRaDevice device) } } - public void Reset() + public Task ResetAsync() { - CleanupAllDevices(); + return CleanupAllDevicesAsync(); } public virtual bool TryGetForPayload(LoRaPayload payload, [MaybeNullWhen(returnValue: false)] out LoRaDevice device) @@ -270,18 +272,19 @@ private void TrackCacheStats(LoRaDevice? device) } } - private void CleanupAllDevices() + private async Task CleanupAllDevicesAsync() { + var devices = this.euiCache.Values; + lock (this.syncLock) { - foreach (var device in this.euiCache.Values) - { - device.Dispose(); - } this.euiCache.Clear(); this.devAddrCache.Clear(); - this.logger.LogInformation($"{nameof(LoRaDeviceCache)} cleared."); } + + this.logger.LogInformation($"{nameof(LoRaDeviceCache)} cleared."); + + await devices.DisposeAllAsync(20); } private class StatisticsTracker @@ -296,13 +299,13 @@ private class StatisticsTracker internal void IncrementMiss() => Interlocked.Increment(ref miss); } - public void Dispose() + public async ValueTask DisposeAsync() { - Dispose(true); + await DisposeAsync(true); GC.SuppressFinalize(this); } - protected virtual void Dispose(bool dispose) + protected virtual async ValueTask DisposeAsync(bool dispose) { if (dispose) { @@ -311,8 +314,9 @@ protected virtual void Dispose(bool dispose) this.ctsDispose?.Cancel(); this.ctsDispose?.Dispose(); this.ctsDispose = null; - CleanupAllDevices(); } + + await CleanupAllDevicesAsync(); } } } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceClient.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceClient.cs index 975a072e3d..bb65182a40 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceClient.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceClient.cs @@ -18,34 +18,47 @@ namespace LoRaWan.NetworkServer /// /// Interface between IoT Hub and device. /// - public sealed class LoRaDeviceClient : ILoRaDeviceClient + public sealed class LoRaDeviceClient : ILoRaDeviceClient, IIdentityProvider { - private static readonly TimeSpan twinUpdateTimeout = TimeSpan.FromSeconds(10); + private const string CompleteOperationName = "Complete"; + private const string AbandonOperationName = "Abandon"; + private const string RejectOperationName = "Reject"; + private static readonly string GetTwinDependencyName = GetSdkDependencyName("GetTwin"); + private static readonly string UpdateReportedPropertiesDependencyName = GetSdkDependencyName("UpdateReportedProperties"); + private static readonly string SendEventDependencyName = GetSdkDependencyName("SendEvent"); + private static readonly string ReceiveDependencyName = GetSdkDependencyName("Receive"); + private static string GetSdkDependencyName(string dependencyName) => $"SDK {dependencyName}"; + private static readonly TimeSpan TwinUpdateTimeout = TimeSpan.FromSeconds(10); + private static int activeDeviceConnections; + + private readonly string deviceIdTracingData; private readonly string connectionString; private readonly ITransportSettings[] transportSettings; private readonly ILogger logger; + private readonly ITracing tracing; private readonly Counter twinLoadRequests; private DeviceClient deviceClient; - private readonly string primaryKey; - - public LoRaDeviceClient(string connectionString, ITransportSettings[] transportSettings, string primaryKey, ILogger logger, Meter meter) + public LoRaDeviceClient(string deviceId, + string connectionString, + ITransportSettings[] transportSettings, + ILogger logger, + Meter meter, + ITracing tracing) { if (string.IsNullOrEmpty(connectionString)) throw new ArgumentException($"'{nameof(connectionString)}' cannot be null or empty.", nameof(connectionString)); - if (string.IsNullOrEmpty(primaryKey)) throw new ArgumentException($"'{nameof(primaryKey)}' cannot be null or empty.", nameof(primaryKey)); if (meter is null) throw new ArgumentNullException(nameof(meter)); this.transportSettings = transportSettings ?? throw new ArgumentNullException(nameof(transportSettings)); - + this.deviceIdTracingData = $"id={deviceId}"; this.connectionString = connectionString; - this.primaryKey = primaryKey; this.logger = logger; + this.tracing = tracing; this.twinLoadRequests = meter.CreateCounter(MetricRegistry.TwinLoadRequests); + _ = meter.CreateObservableGauge(MetricRegistry.ActiveClientConnections, () => activeDeviceConnections); this.deviceClient = CreateDeviceClient(); } - public bool IsMatchingKey(string primaryKey) => this.primaryKey == primaryKey; - public async Task GetTwinAsync(CancellationToken cancellationToken = default) { this.twinLoadRequests.Add(1); @@ -53,6 +66,7 @@ public async Task GetTwinAsync(CancellationToken cancellationToken = defau try { this.logger.LogDebug("getting device twin"); + using var getTwinOperation = this.tracing.TrackIotHubDependency(GetTwinDependencyName, this.deviceIdTracingData); var twins = await this.deviceClient.GetTwinAsync(cancellationToken); @@ -62,7 +76,7 @@ public async Task GetTwinAsync(CancellationToken cancellationToken = defau } catch (OperationCanceledException ex) { - this.logger.LogError($"could not retrieve device twin with error: {ex.Message}"); + this.logger.LogError(ex, $"could not retrieve device twin with error: {ex.Message}"); return null; } catch (IotHubCommunicationException ex) @@ -82,11 +96,12 @@ public async Task UpdateReportedPropertiesAsync(TwinCollection reportedPro { if (cancellationToken == default) { - cts = new CancellationTokenSource(twinUpdateTimeout); + cts = new CancellationTokenSource(TwinUpdateTimeout); cancellationToken = cts.Token; } this.logger.LogDebug("updating twin"); + using var updateReportedPropertiesOperation = this.tracing.TrackIotHubDependency(UpdateReportedPropertiesDependencyName, this.deviceIdTracingData); await this.deviceClient.UpdateReportedPropertiesAsync(reportedProperties, cancellationToken); @@ -95,7 +110,7 @@ public async Task UpdateReportedPropertiesAsync(TwinCollection reportedPro return true; } catch (IotHubCommunicationException ex) when (ex.InnerException is OperationCanceledException && - ExceptionFilterUtility.True(() => this.logger.LogError($"could not update twin with error: {ex.Message}"))) + ExceptionFilterUtility.True(() => this.logger.LogError(ex, $"could not update twin with error: {ex.Message}"))) { return false; } @@ -125,11 +140,12 @@ public async Task SendEventAsync(LoRaDeviceTelemetry telemetry, Dictionary message.Properties.Add(prop); } + using var sendEventOperation = this.tracing.TrackIotHubDependency(SendEventDependencyName, this.deviceIdTracingData); await this.deviceClient.SendEventAsync(message); return true; } - catch (OperationCanceledException ex) when (ExceptionFilterUtility.True(() => this.logger.LogError($"could not send message to IoTHub/Edge with error: {ex.Message}"))) + catch (OperationCanceledException ex) when (ExceptionFilterUtility.True(() => this.logger.LogError(ex, "could not send message to IoTHub/Edge due to timeout."))) { // continue } @@ -144,6 +160,7 @@ public async Task ReceiveAsync(TimeSpan timeout) { this.logger.LogDebug($"checking cloud to device message for {timeout}"); + using var receiveOperation = this.tracing.TrackIotHubDependency(ReceiveDependencyName, this.deviceIdTracingData); var msg = await this.deviceClient.ReceiveAsync(timeout); if (this.logger.IsEnabled(LogLevel.Debug)) @@ -156,91 +173,71 @@ public async Task ReceiveAsync(TimeSpan timeout) return msg; } - catch (OperationCanceledException ex) when (ExceptionFilterUtility.True(() => this.logger.LogError($"could not retrieve cloud to device message with error: {ex.Message}"))) + catch (OperationCanceledException ex) when (ExceptionFilterUtility.True(() => this.logger.LogError(ex, "could not retrieve cloud to device message due to timeout."))) { return null; } } - public async Task CompleteAsync(Message cloudToDeviceMessage) - { - if (cloudToDeviceMessage is null) throw new ArgumentNullException(nameof(cloudToDeviceMessage)); - - try - { - this.logger.LogDebug($"completing cloud to device message, id: {cloudToDeviceMessage.MessageId ?? "undefined"}"); - - await this.deviceClient.CompleteAsync(cloudToDeviceMessage); + public Task CompleteAsync(Message cloudToDeviceMessage) => + ExecuteC2DOperationAsync(cloudToDeviceMessage, static (client, message) => client.CompleteAsync(message), CompleteOperationName); - this.logger.LogDebug($"done completing cloud to device message, id: {cloudToDeviceMessage.MessageId ?? "undefined"}"); + public Task AbandonAsync(Message cloudToDeviceMessage) => + ExecuteC2DOperationAsync(cloudToDeviceMessage, static (client, message) => client.AbandonAsync(message), AbandonOperationName); - return true; - } - catch (OperationCanceledException ex) when (ExceptionFilterUtility.True(() => this.logger.LogError($"could not complete cloud to device message (id: {cloudToDeviceMessage.MessageId ?? "undefined"}) with error: {ex.Message}"))) - { - return false; - } - } + public Task RejectAsync(Message cloudToDeviceMessage) => + ExecuteC2DOperationAsync(cloudToDeviceMessage, static (client, message) => client.RejectAsync(message), RejectOperationName); - public async Task AbandonAsync(Message cloudToDeviceMessage) + private async Task ExecuteC2DOperationAsync(Message cloudToDeviceMessage, Func executeAsync, string operationName) { if (cloudToDeviceMessage is null) throw new ArgumentNullException(nameof(cloudToDeviceMessage)); + var messageId = cloudToDeviceMessage.MessageId ?? "undefined"; try { - this.logger.LogDebug($"abandoning cloud to device message, id: {cloudToDeviceMessage.MessageId ?? "undefined"}"); + this.logger.LogDebug("'{OperationName}' cloud to device message, id: '{MessageId}'.", operationName, messageId); + using var dependencyOperation = this.tracing.TrackIotHubDependency(GetSdkDependencyName(operationName), $"{this.deviceIdTracingData}&messageId={messageId}"); - await this.deviceClient.AbandonAsync(cloudToDeviceMessage); - - this.logger.LogDebug($"done abandoning cloud to device message, id: {cloudToDeviceMessage.MessageId ?? "undefined"}"); + await executeAsync(this.deviceClient, cloudToDeviceMessage); + this.logger.LogDebug("done processing '{OperationName}' on cloud to device message, id: '{MessageId}'.", operationName, messageId); return true; } - catch (OperationCanceledException ex) when (ExceptionFilterUtility.True(() => this.logger.LogError($"could not abandon cloud to device message (id: {cloudToDeviceMessage.MessageId ?? "undefined"}) with error: {ex.Message}"))) + catch (OperationCanceledException ex) when (ExceptionFilterUtility.True(() => this.logger.LogError(ex, "'{OperationName}' timed out on cloud to device message (id: {MessageId}).", operationName, messageId))) { return false; } } - public async Task RejectAsync(Message cloudToDeviceMessage) + public async Task DisconnectAsync(CancellationToken cancellationToken) { - if (cloudToDeviceMessage is null) throw new ArgumentNullException(nameof(cloudToDeviceMessage)); - - try + if (this.deviceClient != null) { - this.logger.LogDebug($"rejecting cloud to device message, id: {cloudToDeviceMessage.MessageId ?? "undefined"}"); - - await this.deviceClient.RejectAsync(cloudToDeviceMessage); + _ = Interlocked.Decrement(ref activeDeviceConnections); - this.logger.LogDebug($"done rejecting cloud to device message, id: {cloudToDeviceMessage.MessageId ?? "undefined"}"); + try + { + await this.deviceClient.CloseAsync(cancellationToken); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) when (ExceptionFilterUtility.True(() => this.logger.LogError(ex, "failed to close device client."))) + { + } + finally + { +#pragma warning disable CA1849 // Calling DisposeAsync after CloseAsync throws an error + this.deviceClient.Dispose(); +#pragma warning restore CA1849 // Call async methods when in an async method + this.deviceClient = null; - return true; - } - catch (OperationCanceledException ex) when (ExceptionFilterUtility.True(() => this.logger.LogError($"could not reject cloud to device message (id: {cloudToDeviceMessage.MessageId ?? "undefined"}) with error: {ex.Message}"))) - { - return false; + this.logger.LogDebug("device client disconnected"); + } } } - /// - /// Disconnects device client. - /// - public bool Disconnect() - { - if (this.deviceClient != null) - { - this.deviceClient.Dispose(); - this.deviceClient = null; - - this.logger.LogDebug("device client disconnected"); - } - else - { - this.logger.LogDebug("device client was already disconnected"); - } - - return true; - } /// /// Ensures that the connection is open. @@ -254,7 +251,7 @@ public bool EnsureConnected() this.deviceClient = CreateDeviceClient(); this.logger.LogDebug("device client reconnected"); } - catch (ArgumentException ex) when (ExceptionFilterUtility.True(() => this.logger.LogError($"could not connect device client with error: {ex.Message}"))) + catch (ArgumentException ex) when (ExceptionFilterUtility.True(() => this.logger.LogError(ex, $"could not connect device client with error: {ex.Message}"))) { return false; } @@ -265,6 +262,7 @@ public bool EnsureConnected() private DeviceClient CreateDeviceClient() { + _ = Interlocked.Increment(ref activeDeviceConnections); var dc = DeviceClient.CreateFromConnectionString(this.connectionString, this.transportSettings); dc.SetRetryPolicy(new ExponentialBackoff(int.MaxValue, minBackoff: TimeSpan.FromMilliseconds(100), @@ -273,12 +271,11 @@ private DeviceClient CreateDeviceClient() return dc; } - public void Dispose() + public async ValueTask DisposeAsync() { - this.deviceClient?.Dispose(); - this.deviceClient = null; - - GC.SuppressFinalize(this); + await DisconnectAsync(CancellationToken.None); } + + ILoRaDeviceClient IIdentityProvider.Identity => this; } } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceClientConnectionManager.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceClientConnectionManager.cs index 995a6818e0..805fc0cb74 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceClientConnectionManager.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceClientConnectionManager.cs @@ -1,171 +1,407 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +#nullable enable + namespace LoRaWan.NetworkServer { using System; using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Azure.Devices.Client; + using Microsoft.Azure.Devices.Shared; using Microsoft.Extensions.Caching.Memory; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; + using LoRaTools; /// /// Manages connections for . /// public sealed class LoRaDeviceClientConnectionManager : ILoRaDeviceClientConnectionManager { - internal sealed class ManagedConnection : IDisposable + private readonly IMemoryCache cache; + private readonly ILoggerFactory? loggerFactory; + private readonly ILogger logger; + private readonly ConcurrentDictionary clientByDevEui = new(); + + /// + /// This record could be a "record struct" but since it is used as a key in a + /// , which treats all keys as objects, it will always get boxed. + /// As a result, it might as well be kept as a reference to avoid superfluous allocations + /// (via unboxing and re-boxing). + /// + private record ScheduleKey(DevEui DevEui); + + public LoRaDeviceClientConnectionManager(IMemoryCache cache, + ILogger logger) : + this(cache, null, logger) + { } + + [ActivatorUtilitiesConstructor] + public LoRaDeviceClientConnectionManager(IMemoryCache cache, + ILoggerFactory? loggerFactory, + ILogger logger) { - public ManagedConnection(LoRaDevice loRaDevice, ILoRaDeviceClient deviceClient) - { - LoRaDevice = loRaDevice; - DeviceClient = deviceClient; - } + this.cache = cache ?? throw new ArgumentNullException(nameof(cache)); + this.loggerFactory = loggerFactory; + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } - public LoRaDevice LoRaDevice { get; } + public ILoRaDeviceClient GetClient(LoRaDevice loRaDevice) + { + if (loRaDevice is null) throw new ArgumentNullException(nameof(loRaDevice)); + + return this.clientByDevEui.TryGetValue(loRaDevice.DevEUI, out var client) + ? client + : throw new ManagedConnectionException($"Connection for device {loRaDevice.DevEUI} was not found"); + } + + public void Register(LoRaDevice loRaDevice, ILoRaDeviceClient loraDeviceClient) + { + if (loRaDevice is null) throw new ArgumentNullException(nameof(loRaDevice)); - public ILoRaDeviceClient DeviceClient { get; } + var loRaDeviceClient = new SynchronizedLoRaDeviceClient(loraDeviceClient, loRaDevice, + this.loggerFactory?.CreateLogger()); - public void Dispose() + loRaDeviceClient.EnsureConnectedSucceeded += (sender, args) => { - DeviceClient.Dispose(); + var client = (SynchronizedLoRaDeviceClient)sender!; + + if (loRaDeviceClient.KeepAliveTimeout <= TimeSpan.Zero) + return; - // Disposing the Connection Manager should only happen on application shutdown - // (which in turn triggers the disposal of all managed connections). - // In that specific case disposing the LoRaDevice will cause the LoRa device to unregister itself again, - // which causes DeviceClient.Dispose() to be called twice. We do not optimize this case, since the Dispose logic is idempotent. - LoRaDevice.Dispose(); + // Set the schedule for a device client disconnect. + // Touching an existing item will update the last access item + // and creating will start the expiration count. + + _ = this.cache.GetOrCreate(new ScheduleKey(client.DevEui), ce => + { + var keepAliveTimeout = client.KeepAliveTimeout; + ce.SlidingExpiration = keepAliveTimeout; + // NOTE! Use of an async void, while generally discouraged, is used here + // intentionally since the eviction callback is synchronous and using async + // void logically equates to "Task.Run". The risk of any exception going + // unnoticed (in either case) is mitigated by logging it. + _ = ce.RegisterPostEvictionCallback(state: this.logger, callback: static async (_, value, _, state) => + { + var client = (SynchronizedLoRaDeviceClient)value; + var logger = (ILogger)state; + try + { + using var scope = logger.BeginDeviceScope(client.DevEui); + await client.DisconnectAsync(CancellationToken.None); + } + catch (Exception ex) when (ExceptionFilterUtility.False(() => logger.LogError(ex, "Error while disconnecting client ({DevEUI}) due to cache eviction.", client.DevEui))) + { + } + }); + + this.logger.LogDebug($"client connection timeout set to {keepAliveTimeout.TotalSeconds} seconds (sliding expiration)"); + return client; + }); + }; + + var key = loRaDevice.DevEUI; + if (!this.clientByDevEui.TryAdd(key, loRaDeviceClient)) + throw new InvalidOperationException($"Connection already registered for device {key}"); + } + + public async Task ReleaseAsync(LoRaDevice loRaDevice) + { + _ = loRaDevice ?? throw new ArgumentNullException(nameof(loRaDevice)); + + if (this.clientByDevEui.TryRemove(loRaDevice.DevEUI, out var removedItem)) + { + await removedItem.DisposeAsync(); } } - private readonly IMemoryCache cache; - private readonly ILogger logger; - private readonly ConcurrentDictionary managedConnections = new ConcurrentDictionary(); + /// + /// Tries to trigger scanning of expired items + /// For tests only. + /// + public void TryScanExpiredItems() + { + _ = this.cache.TryGetValue(string.Empty, out _); + } - public LoRaDeviceClientConnectionManager(IMemoryCache cache, ILogger logger) + public async ValueTask DisposeAsync() { - this.cache = cache ?? throw new ArgumentNullException(nameof(cache)); - this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + await this.clientByDevEui.Values.DisposeAllAsync(20); } - public bool EnsureConnected(LoRaDevice loRaDevice) + public IAsyncDisposable BeginDeviceClientConnectionActivity(LoRaDevice loRaDevice) { - if (loRaDevice is null) throw new ArgumentNullException(nameof(loRaDevice)); + ArgumentNullException.ThrowIfNull(loRaDevice, nameof(loRaDevice)); + return this.clientByDevEui[loRaDevice.DevEUI].BeginDeviceClientConnectionActivity(); + } - if (this.managedConnections.TryGetValue(GetConnectionCacheKey(loRaDevice.DevEUI), out var managedConnection)) + private sealed class SynchronizedLoRaDeviceClient : ILoRaDeviceClient, ILoRaDeviceClientSynchronizedOperationEventSource, IIdentityProvider + { + private readonly ILoRaDeviceClient client; + private readonly LoRaDevice device; + private readonly ILogger? logger; + private int operationSequenceNumber; + private readonly ExclusiveProcessor exclusiveProcessor = new(); + private bool disconnectedDuringActivity; + private int activities; + + private record struct Process(int Id, string Name); + + public SynchronizedLoRaDeviceClient(ILoRaDeviceClient client, LoRaDevice device, ILogger? logger) { - if (loRaDevice.KeepAliveTimeout > 0) + this.client = client; + this.device = device; + this.logger = logger; + + if (logger is { } someLogger && someLogger.IsEnabled(LogLevel.Debug)) { - if (!managedConnection.DeviceClient.EnsureConnected()) - return false; + this.exclusiveProcessor.Submitted += (_, p) => + { + var (id, name) = p; + someLogger.LogDebug(@"Queued ""{Name}"" ({Id})", name, id); + }; + + this.exclusiveProcessor.Processing += (_, p) => + { + var (id, name) = p; + someLogger.LogDebug(@"Invoking ""{Name}"" ({Id})", name, id); + }; - SetupSchedule(managedConnection); + this.exclusiveProcessor.Processed += (_, args) => + { + var ((id, name), outcome) = args; + someLogger.LogDebug(@"Invoked ""{Name}"" ({Id}); status = {Status}, run-time = {RunTime}, wait-time = {WaitTime}", + name, id, outcome.Task.Status, outcome.RunDuration, outcome.WaitDuration); + }; + + this.exclusiveProcessor.Interrupted += (_, p) => + { + var (interrupted, interrupting) = p; + someLogger.LogDebug(@"Interrupted ""{Name}"" ({Id}) by {InterruptingName} ({InterruptingId})", + interrupted.Name, interrupted.Id, + interrupting.Name, interrupting.Id); + }; } + } + + public DevEui DevEui => this.device.DevEUI; + public TimeSpan KeepAliveTimeout => TimeSpan.FromSeconds(this.device.KeepAliveTimeout); + + ILoRaDeviceClient IIdentityProvider.Identity => this.client; + + public ValueTask DisposeAsync() => this.client.DisposeAsync(); + + public event EventHandler? EnsureConnectedSucceeded; + + private Task InvokeExclusivelyAsync(Func> processor, + [CallerMemberName] string? callerName = null) => + InvokeExclusivelyAsync(doesNotRequireOpenConnection: false, processor, callerName); - return true; + private async Task InvokeExclusivelyAsync(bool doesNotRequireOpenConnection, + Func> processor, + [CallerMemberName] string? callerName = null) + { + _ = Interlocked.Increment(ref this.operationSequenceNumber); + return await this.exclusiveProcessor.ProcessAsync(new Process(this.operationSequenceNumber, callerName!), async () => + { + if (!doesNotRequireOpenConnection) + _ = EnsureConnected(); + + return await processor(this.client); + }); } - throw new ManagedConnectionException($"Connection for device {loRaDevice.DevEUI} was not found"); - } + public Task GetTwinAsync(CancellationToken cancellationToken) => + InvokeExclusivelyAsync(client => client.GetTwinAsync(cancellationToken)); - /// - /// Sets the schedule for a device client disconnect. - /// - private void SetupSchedule(ManagedConnection managedConnection) - { - var key = GetScheduleCacheKey(managedConnection.LoRaDevice.DevEUI); - // Touching an existing item will update the last access item - // Creating will start the expiration count - _ = this.cache.GetOrCreate( - key, - (ce) => - { - ce.SlidingExpiration = TimeSpan.FromSeconds(managedConnection.LoRaDevice.KeepAliveTimeout); - _ = ce.RegisterPostEvictionCallback(OnScheduledDisconnect); + public Task SendEventAsync(LoRaDeviceTelemetry telemetry, Dictionary properties) => + InvokeExclusivelyAsync(client => client.SendEventAsync(telemetry, properties)); - this.logger.LogDebug($"client connection timeout set to {managedConnection.LoRaDevice.KeepAliveTimeout} seconds (sliding expiration)"); + public Task UpdateReportedPropertiesAsync(TwinCollection reportedProperties, CancellationToken cancellationToken) => + InvokeExclusivelyAsync(client => client.UpdateReportedPropertiesAsync(reportedProperties, cancellationToken)); - return managedConnection; - }); - } + public Task ReceiveAsync(TimeSpan timeout) => + InvokeExclusivelyAsync(client => client.ReceiveAsync(timeout)); - private void OnScheduledDisconnect(object key, object value, EvictionReason reason, object state) - { - var managedConnection = (ManagedConnection)value; + public Task CompleteAsync(Message cloudToDeviceMessage) => + InvokeExclusivelyAsync(client => client.CompleteAsync(cloudToDeviceMessage)); - using var scope = this.logger.BeginDeviceScope(managedConnection.LoRaDevice.DevEUI); + public Task AbandonAsync(Message cloudToDeviceMessage) => + InvokeExclusivelyAsync(client => client.AbandonAsync(cloudToDeviceMessage)); - if (!managedConnection.LoRaDevice.TryDisconnect()) + public Task RejectAsync(Message cloudToDeviceMessage) => + InvokeExclusivelyAsync(client => client.RejectAsync(cloudToDeviceMessage)); + + public bool EnsureConnected() { - this.logger.LogInformation("scheduled device disconnection has been postponed. Device client connection is active"); - SetupSchedule(managedConnection); + var connected = this.client.EnsureConnected(); + + if (this.logger?.IsEnabled(LogLevel.Debug) ?? false) + { + const string message = $"{nameof(EnsureConnected)} = {{Connected}}"; + this.logger.LogDebug(message, connected); + } + + if (connected) + EnsureConnectedSucceeded?.Invoke(this, EventArgs.Empty); + + return connected; } - } - private static string GetConnectionCacheKey(DevEui devEui) => string.Concat("connection:", devEui); + public Task DisconnectAsync(CancellationToken cancellationToken) => + DisconnectAsync(isActivityEnding: false, cancellationToken); - private static string GetScheduleCacheKey(DevEui devEui) => string.Concat("connection:schedule:", devEui); + private enum DisconnectionResult { Disconnected, Deferred, NotDisconnected } - public ILoRaDeviceClient GetClient(LoRaDevice loRaDevice) - { - if (loRaDevice is null) throw new ArgumentNullException(nameof(loRaDevice)); - return GetClient(loRaDevice.DevEUI); - } + /// + /// This method is either called by an explicit call to + /// or when an activity is ending (when is true) + /// to issue a disconnection if it was deferred and no other activities are + /// outstanding. + /// + private Task DisconnectAsync(bool isActivityEnding, CancellationToken cancellationToken) => + InvokeExclusivelyAsync(doesNotRequireOpenConnection: true, async client => + { + DisconnectionResult result; - public ILoRaDeviceClient GetClient(DevEui devEui) - { - if (this.managedConnections.TryGetValue(GetConnectionCacheKey(devEui), out var managedConnection)) - return managedConnection.DeviceClient; + if (Interlocked.Add(ref this.activities, 0) > 0) + { + this.disconnectedDuringActivity = true; + result = DisconnectionResult.Deferred; + } + else if (isActivityEnding && !this.disconnectedDuringActivity) + { + result = DisconnectionResult.NotDisconnected; + } + else + { + this.disconnectedDuringActivity = false; + await client.DisconnectAsync(cancellationToken); + result = DisconnectionResult.Disconnected; + } - throw new ManagedConnectionException($"Connection for device {devEui} was not found"); - } + if (this.logger is { } logger && logger.IsEnabled(LogLevel.Debug)) + { + const string message = $"{nameof(DisconnectAsync)} = {{DisconnectResult}}"; + logger.LogDebug(message, result); + } - public void Register(LoRaDevice loRaDevice, ILoRaDeviceClient loraDeviceClient) - { - if (loRaDevice is null) throw new ArgumentNullException(nameof(loRaDevice)); + return result; + }); - var key = GetConnectionCacheKey(loRaDevice.DevEUI); - lock (this.managedConnections) + public IAsyncDisposable BeginDeviceClientConnectionActivity() { - if (this.managedConnections.ContainsKey(key)) + var activities = Interlocked.Increment(ref this.activities); + + if (this.logger is { } logger && logger.IsEnabled(LogLevel.Debug)) { - throw new InvalidOperationException($"Connection already registered for device {loRaDevice.DevEUI}"); + const string message = $"{nameof(BeginDeviceClientConnectionActivity)} = {{Activities}}"; + logger.LogDebug(message, activities); } - this.managedConnections[key] = new ManagedConnection(loRaDevice, loraDeviceClient); + return new AsyncDisposable(async cancellationToken => + { + var activities = Interlocked.Decrement(ref this.activities); + + if (this.logger is { } logger && logger.IsEnabled(LogLevel.Debug)) + { + const string message = $"{nameof(BeginDeviceClientConnectionActivity)} (disposal) = {{Activities}}"; + logger.LogDebug(message, activities); + } + + if (activities > 0) + return; + + _ = await DisconnectAsync(isActivityEnding: true, cancellationToken); + }); } - } - public void Release(LoRaDevice loRaDevice) - { - _ = loRaDevice ?? throw new ArgumentNullException(nameof(loRaDevice)); - Release(loRaDevice.DevEUI); - } + private OperationEvent? submittedEvent; + private OperationEvent? processingEvent; + private OperationEvent<(Process, ExclusiveProcessor.IProcessingOutcome)>? processedEvent; - public void Release(DevEui devEui) - { - if (this.managedConnections.TryRemove(GetConnectionCacheKey(devEui), out var removedItem)) + private OperationEvent SubmittedEvent => + this.submittedEvent ??= new(this, p => p, static (ep, h) => ep.Submitted += h, static (ep, h) => ep.Submitted -= h); + + private OperationEvent ProcessingEvent => + this.processingEvent ??= new(this, p => p, static (ep, h) => ep.Processing += h, static (ep, h) => ep.Processing -= h); + + private OperationEvent<(Process, ExclusiveProcessor.IProcessingOutcome)> ProcessedEvent => + this.processedEvent ??= new(this, args => args.Item1, static (ep, h) => ep.Processed += h, static (ep, h) => ep.Processed -= h); + + event EventHandler? ILoRaDeviceClientSynchronizedOperationEventSource.Queued { - removedItem.Dispose(); + add => SubmittedEvent.Add(value); + remove => SubmittedEvent.Remove(value); } - } - /// - /// Tries to trigger scanning of expired items - /// For tests only. - /// - public void TryScanExpiredItems() - { - _ = this.cache.TryGetValue(string.Empty, out _); - } + event EventHandler? ILoRaDeviceClientSynchronizedOperationEventSource.Processing + { + add => ProcessingEvent.Add(value); + remove => ProcessingEvent.Remove(value); + } - public void Dispose() - { - // The LoRaDeviceClientConnectionManager does not own the cache, but it owns all the managed connections. + event EventHandler? ILoRaDeviceClientSynchronizedOperationEventSource.Processed + { + add => ProcessedEvent.Add(value); + remove => ProcessedEvent.Remove(value); + } - foreach (var it in this.managedConnections) + private sealed class OperationEvent { - it.Value.Dispose(); + private EventHandler? field; + private readonly SynchronizedLoRaDeviceClient client; + private readonly EventHandler handler; + private readonly Action, EventHandler> adder; + private readonly Action, EventHandler> remover; + + public OperationEvent(SynchronizedLoRaDeviceClient client, + Func argsMapper, + Action, EventHandler> adder, + Action, EventHandler> remover) + { + this.client = client; + this.handler = (_, args) => + { + var (id, name) = argsMapper(args); + this.field?.Invoke(this.client, new LoRaDeviceClientSynchronizedOperationEventArgs(id, name)); + }; + this.adder = adder; + this.remover = remover; + } + + public void Add(EventHandler? value) + { + if (this.field is not null) + this.remover(this.client.exclusiveProcessor, this.handler); + this.adder(this.client.exclusiveProcessor, this.handler); + this.field = value; + } + + public void Remove(EventHandler? value) + { + if (value != this.field) + return; + this.field = null; + this.remover(this.client.exclusiveProcessor, this.handler); + } } } } + +#pragma warning disable CA1711 // Identifiers should not have incorrect suffix + public sealed record LoRaDeviceClientSynchronizedOperationEventArgs(int Id, string Name); +#pragma warning restore CA1711 // Identifiers should not have incorrect suffix + + public interface ILoRaDeviceClientSynchronizedOperationEventSource + { + event EventHandler Queued; + event EventHandler Processing; + event EventHandler Processed; + } } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceClientExtensions.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceClientExtensions.cs new file mode 100644 index 0000000000..4b46b014ce --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceClientExtensions.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.NetworkServer +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Reflection; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Azure.Devices.Client; + using Microsoft.Azure.Devices.Shared; + using Microsoft.Extensions.Logging; + + public static class LoRaDeviceClientExtensions + { + public static ILoRaDeviceClient AddResiliency(this ILoRaDeviceClient client, ILoggerFactory? loggerFactory) + { + ArgumentNullException.ThrowIfNull(client, nameof(client)); + return client is ResilientClient ? client : new ResilientClient(client, loggerFactory?.CreateLogger()); + } + + private sealed class ResilientClient : ILoRaDeviceClient, IIdentityProvider + { + private const int MaxAttempts = 3; + + private readonly ILoRaDeviceClient client; + private readonly ILogger? logger; + + public ResilientClient(ILoRaDeviceClient client, ILogger? logger) + { + this.client = client; + this.logger = logger; + } + + public bool EnsureConnected() => this.client.EnsureConnected(); + public Task DisconnectAsync(CancellationToken cancellationToken) => this.client.DisconnectAsync(cancellationToken); + ValueTask IAsyncDisposable.DisposeAsync() => this.client.DisposeAsync(); + + private async Task InvokeAsync(T1 arg1, T2 arg2, Func> function, + [CallerMemberName] string? operationName = null) + { + for (var attempt = 1; ; attempt++) + { + try + { + _ = this.client.EnsureConnected(); + return await function(this.client, arg1, arg2); + } + catch (Exception ex) + when ((ex is ObjectDisposedException + || (ex is InvalidOperationException ioe + && ioe.Message.StartsWith("This operation is only allowed using a successfully authenticated context.", StringComparison.OrdinalIgnoreCase))) + && ExceptionFilterUtility.True(() => + this.logger?.LogDebug(ex, @"Device client operation ""{Operation}"" (attempt {Attempt}/" + + MaxAttempts.ToString(CultureInfo.InvariantCulture) + + @") failed due to error: {Error}", + operationName, attempt, ex.GetBaseException().Message))) + { + // disconnect, re-connect and then retry... + await this.client.DisconnectAsync(CancellationToken.None); + if (attempt == MaxAttempts) + throw; + } + } + } + + public Task GetTwinAsync(CancellationToken cancellationToken) => + InvokeAsync(cancellationToken, Missing.Value, static (client, cancellationToken, _) => client.GetTwinAsync(cancellationToken)); + + public Task SendEventAsync(LoRaDeviceTelemetry telemetry, Dictionary properties) => + InvokeAsync(telemetry, properties, static (client, telemetry, properties) => client.SendEventAsync(telemetry, properties)); + + public Task UpdateReportedPropertiesAsync(TwinCollection reportedProperties, CancellationToken cancellationToken) => + InvokeAsync(reportedProperties, cancellationToken, static (client, reportedProperties, cancellationToken) => client.UpdateReportedPropertiesAsync(reportedProperties, cancellationToken)); + + public Task ReceiveAsync(TimeSpan timeout) => + InvokeAsync(timeout, Missing.Value, static (client, timeout, _) => client.ReceiveAsync(timeout)); + + public Task CompleteAsync(Message cloudToDeviceMessage) => + InvokeAsync(cloudToDeviceMessage, Missing.Value, static (client, message, _) => client.CompleteAsync(message)); + + public Task AbandonAsync(Message cloudToDeviceMessage) => + InvokeAsync(cloudToDeviceMessage, Missing.Value, static (client, message, _) => client.AbandonAsync(message)); + + public Task RejectAsync(Message cloudToDeviceMessage) => + InvokeAsync(cloudToDeviceMessage, Missing.Value, static (client, message, _) => client.RejectAsync(message)); + + ILoRaDeviceClient IIdentityProvider.Identity => this.client; + } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceFactory.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceFactory.cs index 9be66695be..1915246e6b 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceFactory.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceFactory.cs @@ -19,6 +19,7 @@ public class LoRaDeviceFactory : ILoRaDeviceFactory private readonly ILoggerFactory loggerFactory; private readonly ILogger logger; private readonly Meter meter; + private readonly ITracing tracing; public LoRaDeviceFactory(NetworkServerConfiguration configuration, ILoRaDataRequestHandler dataRequestHandler, @@ -26,7 +27,8 @@ public class LoRaDeviceFactory : ILoRaDeviceFactory LoRaDeviceCache loRaDeviceCache, ILoggerFactory loggerFactory, ILogger logger, - Meter meter) + Meter meter, + ITracing tracing) { this.configuration = configuration; this.dataRequestHandler = dataRequestHandler; @@ -34,6 +36,7 @@ public class LoRaDeviceFactory : ILoRaDeviceFactory this.loggerFactory = loggerFactory; this.logger = logger; this.meter = meter; + this.tracing = tracing; this.loRaDeviceCache = loRaDeviceCache; } @@ -53,6 +56,7 @@ public Task CreateAndRegisterAsync(IoTHubDeviceInfo deviceInfo, Canc private async Task RegisterCoreAsync(IoTHubDeviceInfo deviceInfo, CancellationToken cancellationToken) { var loRaDevice = CreateDevice(deviceInfo); + var loRaDeviceClient = CreateDeviceClient(deviceInfo.DevEUI.ToString(), deviceInfo.PrimaryKey); try { // we always want to register the connection if we have a key. @@ -61,7 +65,7 @@ private async Task RegisterCoreAsync(IoTHubDeviceInfo deviceInfo, Ca // even though, we don't own it, to detect ownership // changes. // Ownership is transferred to connection manager. - this.connectionManager.Register(loRaDevice, CreateDeviceClient(deviceInfo.DevEUI.ToString(), deviceInfo.PrimaryKey)); + this.connectionManager.Register(loRaDevice, loRaDeviceClient); loRaDevice.SetRequestHandler(this.dataRequestHandler); @@ -70,13 +74,31 @@ private async Task RegisterCoreAsync(IoTHubDeviceInfo deviceInfo, Ca { throw new LoRaProcessingException("Failed to initialize device twins.", LoRaProcessingErrorCode.DeviceInitializationFailed); } + this.loRaDeviceCache.Register(loRaDevice); + return loRaDevice; } catch { - this.connectionManager.Release(loRaDevice); - loRaDevice.Dispose(); + // release the loradevice client explicitly. If we were unable to register, or there was already + // a connection registered, we will leak this client. + await loRaDeviceClient.DisposeAsync(); + + if (this.logger.IsEnabled(LogLevel.Debug)) + { + try + { + // if the created client is registered, release it + if (!ReferenceEquals(loRaDeviceClient, ((IIdentityProvider)this.connectionManager.GetClient(loRaDevice)).Identity)) + { + this.logger.LogDebug("leaked connection found"); + } + } + catch (ManagedConnectionException) { } + } + + await loRaDevice.DisposeAsync(); throw; } } @@ -145,7 +167,11 @@ public virtual ILoRaDeviceClient CreateDeviceClient(string deviceId, string prim } }; - return new LoRaDeviceClient(deviceConnectionStr, transportSettings, primaryKey, this.loggerFactory.CreateLogger(), this.meter); + var client = new LoRaDeviceClient(deviceId, deviceConnectionStr, transportSettings, + this.loggerFactory.CreateLogger(), this.meter, + this.tracing); + + return client.AddResiliency(this.loggerFactory); } catch (Exception ex) { diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceRegistry.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceRegistry.cs index 946474aa2d..6a78b918c6 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceRegistry.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaDeviceRegistry.cs @@ -173,7 +173,7 @@ public async Task GetDeviceByDevEUIAsync(DevEui devEUI) // Creates cache key for join device loader: "joinloader:{devEUI}" private static string GetJoinDeviceLoaderCacheKey(DevEui devEui) => string.Concat("joinloader:", devEui); - private static string GetDevLoaderCacheKey(DevAddr devAddr) => string.Concat("devaddrloader:", devAddr); + internal static string GetDevLoaderCacheKey(DevAddr devAddr) => string.Concat("devaddrloader:", devAddr); // Removes join device loader from cache private void RemoveJoinDeviceLoader(DevEui devEui) => this.cache.Remove(GetJoinDeviceLoaderCacheKey(devEui)); @@ -210,9 +210,9 @@ public async Task GetDeviceForJoinRequestAsync(DevEui devEUI, DevNon // another gateway processed the join request. If we have it in the cache // with existing session keys, we need to invalidate that entry, to ensure // it gets re-fetched on the next message - if (this.deviceCache.TryGetByDevEui(devEUI, out var someDevice) && someDevice.AppSKey != null) + if (this.deviceCache.TryGetByDevEui(devEUI, out var someDevice)) { - _ = this.deviceCache.Remove(someDevice); + _ = await this.deviceCache.RemoveAsync(someDevice); this.logger.LogDebug("Device was removed from cache."); } @@ -273,11 +273,8 @@ private void CleanupOldDevAddr(LoRaDevice loRaDevice, DevAddr? oldDevAddr) /// /// /// - public void ResetDeviceCache() - { - this.deviceCache.Reset(); - } + public Task ResetDeviceCacheAsync() => this.deviceCache.ResetAsync(); - public void Dispose() => this.deviceCache.Dispose(); + public ValueTask DisposeAsync() => this.deviceCache.DisposeAsync(); } } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaPayloadDecoder.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaPayloadDecoder.cs index 41fdb090ea..ea737e534a 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaPayloadDecoder.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaPayloadDecoder.cs @@ -11,6 +11,7 @@ namespace LoRaWan.NetworkServer using System.Text; using System.Threading.Tasks; using System.Web; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -18,36 +19,17 @@ namespace LoRaWan.NetworkServer /// /// LoRa payload decoder. /// - public class LoRaPayloadDecoder : ILoRaPayloadDecoder + public sealed class LoRaPayloadDecoder : ILoRaPayloadDecoder { - private readonly HttpClient httpClient; - - // Http client used by decoders - // Decoder calls don't need proxy since they will never leave the IoT Edge device - private readonly Lazy decodersHttpClient; + private readonly IHttpClientFactory httpClientFactory; private readonly ILogger logger; - public LoRaPayloadDecoder(ILogger logger) + public LoRaPayloadDecoder(IHttpClientFactory httpClientFactory, ILogger logger) { - this.decodersHttpClient = new Lazy(() => - { - var client = new HttpClient(); - client.DefaultRequestHeaders.Add("Connection", "Keep-Alive"); - client.DefaultRequestHeaders.Add("Keep-Alive", "timeout=86400"); - return client; - }); + this.httpClientFactory = httpClientFactory; this.logger = logger; } - /// - /// Initializes a new instance of the class. - /// Constructor for unit testing. - /// - public LoRaPayloadDecoder(HttpClient httpClient) - { - this.httpClient = httpClient; - } - public async ValueTask DecodeMessageAsync(DevEui devEui, byte[] payload, FramePort fport, string sensorDecoder) { sensorDecoder ??= string.Empty; @@ -95,8 +77,7 @@ private async Task CallSensorDecoderModule(Uri sensorDecode { try { - var httpClientToUse = this.httpClient ?? this.decodersHttpClient.Value; - var response = await httpClientToUse.GetAsync(sensorDecoderModuleUrl); + var response = await this.httpClientFactory.CreateClient(PayloadDecoderHttpClient.ClientName).GetAsync(sensorDecoderModuleUrl); if (!response.IsSuccessStatusCode) { @@ -202,4 +183,22 @@ public static object DecoderHexSensor(DevEui devEui, byte[] payload, FramePort f return new DecodedPayloadValue(payloadHex); } } + + internal static class PayloadDecoderHttpClient + { + public const string ClientName = nameof(PayloadDecoderHttpClient); + + public static IServiceCollection AddPayloadDecoderHttpClient(this IServiceCollection services) + { + // Decoder calls don't need proxy since they will never leave the IoT Edge device + _ = services.AddHttpClient(ClientName) + .ConfigureHttpClient(client => + { + client.DefaultRequestHeaders.Add("Connection", "Keep-Alive"); + client.DefaultRequestHeaders.Add("Keep-Alive", "timeout=86400"); + }); + + return services; + } + } } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaWan.NetworkServer.csproj b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaWan.NetworkServer.csproj index 6833ffad2c..62b2620eb7 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaWan.NetworkServer.csproj +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/LoRaWan.NetworkServer.csproj @@ -12,7 +12,6 @@ - @@ -20,26 +19,14 @@ + + + - - - JsonReader.g.cs - TextTemplatingFileGenerator - - - - - - True - True - JsonReader.g.tt - - - diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/IotHubLogger.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/IotHubLogger.cs similarity index 86% rename from LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/IotHubLogger.cs rename to LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/IotHubLogger.cs index e4a0f16b00..c71afd0594 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/IotHubLogger.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/IotHubLogger.cs @@ -3,13 +3,13 @@ #nullable enable -namespace Logger +namespace LoRaWan.NetworkServer.Logger { using System; using System.Collections.Concurrent; using System.Text; using System.Threading.Tasks; - using LoRaWan; + using LoRaTools; using Microsoft.Azure.Devices.Client; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -21,41 +21,48 @@ internal sealed class IotHubLoggerProvider : ILoggerProvider { private readonly ConcurrentDictionary loggers = new(); private readonly Lazy> moduleClientFactory; + private readonly ITracing tracing; internal LoggerConfigurationMonitor LoggerConfigurationMonitor { get; } - public IotHubLoggerProvider(IOptionsMonitor configuration) - : this(configuration, new Lazy>(ModuleClient.CreateFromEnvironmentAsync(new[] { new AmqpTransportSettings(TransportType.Amqp_Tcp_Only) }))) + public IotHubLoggerProvider(IOptionsMonitor configuration, ITracing tracing) + : this(configuration, new Lazy>(ModuleClient.CreateFromEnvironmentAsync(new[] { new AmqpTransportSettings(TransportType.Amqp_Tcp_Only) })), tracing) { } - internal IotHubLoggerProvider(IOptionsMonitor configuration, Lazy> moduleClientFactory) + internal IotHubLoggerProvider(IOptionsMonitor configuration, Lazy> moduleClientFactory, ITracing tracing) { LoggerConfigurationMonitor = new LoggerConfigurationMonitor(configuration); this.moduleClientFactory = moduleClientFactory; + this.tracing = tracing; } public ILogger CreateLogger(string categoryName) => - this.loggers.GetOrAdd(categoryName, n => new IotHubLogger(this, this.moduleClientFactory)); + this.loggers.GetOrAdd(categoryName, n => new IotHubLogger(this, this.moduleClientFactory, this.tracing)); public void Dispose() { this.loggers.Clear(); - this.LoggerConfigurationMonitor.Dispose(); + LoggerConfigurationMonitor.Dispose(); } } internal class IotHubLogger : ILogger { + private const string SendOperationName = "SDK SendEvent"; + private const string LogTraceData = "log"; private readonly IotHubLoggerProvider iotHubLoggerProvider; private readonly Lazy> moduleClientFactory; + private readonly ITracing tracing; internal bool hasError; public IotHubLogger(IotHubLoggerProvider iotHubLoggerProvider, - Lazy> moduleClientFactory) + Lazy> moduleClientFactory, + ITracing tracing) { this.iotHubLoggerProvider = iotHubLoggerProvider; this.moduleClientFactory = moduleClientFactory; + this.tracing = tracing; } public IDisposable BeginScope(TState state) => @@ -90,6 +97,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except throw; } + using var sendOperation = this.tracing.TrackIotHubDependency(SendOperationName, LogTraceData); await SendAsync(moduleClient, formattedMessage); } catch (Exception ex) diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/LoRaConsoleLogger.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/LoRaConsoleLogger.cs similarity index 98% rename from LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/LoRaConsoleLogger.cs rename to LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/LoRaConsoleLogger.cs index 999f4c5af7..d29b5b5260 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/LoRaConsoleLogger.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/LoRaConsoleLogger.cs @@ -2,11 +2,11 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. #nullable enable -namespace LoRaWan +namespace LoRaWan.NetworkServer.Logger { using System; using System.Collections.Concurrent; - using Logger; + using LoRaTools; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/LoRaLoggerConfiguration.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/LoRaLoggerConfiguration.cs similarity index 91% rename from LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/LoRaLoggerConfiguration.cs rename to LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/LoRaLoggerConfiguration.cs index a1319786b7..62fa69d86c 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/LoRaLoggerConfiguration.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/LoRaLoggerConfiguration.cs @@ -3,7 +3,7 @@ #nullable enable -namespace Logger +namespace LoRaWan.NetworkServer.Logger { using Microsoft.Extensions.Logging; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/LoggerConfigurationMonitor.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/LoggerConfigurationMonitor.cs similarity index 97% rename from LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/LoggerConfigurationMonitor.cs rename to LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/LoggerConfigurationMonitor.cs index 431b9a8bbc..ccbd4afe71 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/LoggerConfigurationMonitor.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/LoggerConfigurationMonitor.cs @@ -3,7 +3,7 @@ #nullable enable -namespace Logger +namespace LoRaWan.NetworkServer.Logger { using System; using System.Diagnostics.CodeAnalysis; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/LoggerHelper.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/LoggerHelper.cs similarity index 97% rename from LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/LoggerHelper.cs rename to LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/LoggerHelper.cs index a5c8180709..2480974ff0 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/LoggerHelper.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/LoggerHelper.cs @@ -3,11 +3,11 @@ #nullable enable -namespace Logger +namespace LoRaWan.NetworkServer.Logger { using System; using System.Collections.Generic; - using LoRaWan; + using LoRaTools; using Microsoft.Extensions.Logging; internal static class LoggerHelper diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/TcpLogger.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/TcpLogger.cs similarity index 99% rename from LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/TcpLogger.cs rename to LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/TcpLogger.cs index ec2ccaab02..2a0946c0f8 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/TcpLogger.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/TcpLogger.cs @@ -3,7 +3,7 @@ #nullable enable -namespace Logger +namespace LoRaWan.NetworkServer.Logger { using System; using System.Buffers; @@ -18,7 +18,7 @@ namespace Logger using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; - using LoRaWan; + using LoRaTools; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/TcpLoggerConfiguration.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/TcpLoggerConfiguration.cs similarity index 96% rename from LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/TcpLoggerConfiguration.cs rename to LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/TcpLoggerConfiguration.cs index 6fb55c22ea..25431ede69 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/TcpLoggerConfiguration.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/Logger/TcpLoggerConfiguration.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace LoRaWan +namespace LoRaWan.NetworkServer.Logger { using Microsoft.Extensions.Logging; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/MessageDispatcher.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/MessageDispatcher.cs index 5a7d7858a8..3a2bdbcde6 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/MessageDispatcher.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/MessageDispatcher.cs @@ -5,15 +5,15 @@ namespace LoRaWan.NetworkServer { using System; using System.Diagnostics.Metrics; + using System.Threading.Tasks; + using LoRaTools; using LoRaTools.LoRaMessage; - using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; - using Microsoft.Extensions.Logging.Abstractions; /// /// Message dispatcher. /// - public sealed class MessageDispatcher : IDisposable, IMessageDispatcher + public sealed class MessageDispatcher : IAsyncDisposable, IMessageDispatcher { private readonly NetworkServerConfiguration configuration; private readonly ILoRaDeviceRegistry deviceRegistry; @@ -23,9 +23,6 @@ public sealed class MessageDispatcher : IDisposable, IMessageDispatcher private readonly ILogger logger; private readonly Histogram d2cMessageDeliveryLatencyHistogram; - private static readonly IMemoryCache testMemoryCache = new MemoryCache(new MemoryCacheOptions()); - private static readonly IConcentratorDeduplication concentratorDeduplication = new ConcentratorDeduplication(testMemoryCache, NullLogger.Instance); - public MessageDispatcher( NetworkServerConfiguration configuration, ILoRaDeviceRegistry deviceRegistry, @@ -49,19 +46,6 @@ public sealed class MessageDispatcher : IDisposable, IMessageDispatcher this.d2cMessageDeliveryLatencyHistogram = meter?.CreateHistogram(MetricRegistry.D2CMessageDeliveryLatency); } - /// - /// Use this constructor only for tests. - /// - internal MessageDispatcher(NetworkServerConfiguration configuration, - ILoRaDeviceRegistry deviceRegistry, - ILoRaDeviceFrameCounterUpdateStrategyProvider frameCounterUpdateStrategyProvider) - : this(configuration, deviceRegistry, frameCounterUpdateStrategyProvider, - new JoinRequestMessageHandler(configuration, concentratorDeduplication, deviceRegistry, NullLogger.Instance, null), - NullLoggerFactory.Instance, - NullLogger.Instance, - null) - { } - /// /// Dispatches a request. /// @@ -129,6 +113,6 @@ private bool IsValidNetId(DevAddr devAddr) return false; } - public void Dispose() => this.deviceRegistry.Dispose(); + public ValueTask DisposeAsync() => this.deviceRegistry.DisposeAsync(); } } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/MetricRegistry.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/MetricRegistry.cs index 9465f26a33..b96daf9d6b 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/MetricRegistry.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/MetricRegistry.cs @@ -35,6 +35,8 @@ internal static class MetricRegistry public static readonly CustomMetric DeviceCacheHits = new CustomMetric("DeviceCacheHits", "Number of device cache hits", MetricType.Counter, new[] { GatewayIdTagName }); public static readonly CustomMetric DeviceLoadRequests = new CustomMetric("DeviceLoadRequests", "Number of device load requests issued against an API service", MetricType.Counter, new[] { GatewayIdTagName }); public static readonly CustomMetric TwinLoadRequests = new CustomMetric("TwinLoadRequests", "Number of device twin load requests issued against RegistryManager and DeviceClient", MetricType.Counter, new[] { GatewayIdTagName }); + public static readonly CustomMetric ActiveClientConnections = new CustomMetric("ActiveClientConnections", "Number of active client connections", MetricType.ObservableGauge, new[] { GatewayIdTagName }); + public static readonly CustomMetric ForceClosedClientConnections = new CustomMetric("ForceClosedClientConnections", "Number of device client connections requested to be closed by the FunctionBundler", MetricType.Counter, new[] { GatewayIdTagName }); private static readonly ICollection Registry = new[] { @@ -50,20 +52,24 @@ internal static class MetricRegistry C2DMessageTooLong, DeviceCacheHits, DeviceLoadRequests, - TwinLoadRequests + TwinLoadRequests, + ActiveClientConnections, + ForceClosedClientConnections }; public static readonly IDictionary RegistryLookup = new Dictionary(Registry.ToDictionary(m => m.Name, m => m), StringComparer.OrdinalIgnoreCase); } - internal record CustomMetric(string Name, string Description, MetricType Type, string[] Tags); +#pragma warning disable CA1819 // Properties should not return arrays (prometheus-net only accepts arrays in their API, done to avoid unnecessary conversion) + public record CustomMetric(string Name, string Description, MetricType Type, string[] Tags); +#pragma warning restore CA1819 // Properties should not return arrays internal record CustomHistogram(string Name, string Description, MetricType Type, string[] Tags, double BucketStart, double BucketWidth, int BucketCount) : CustomMetric(Name, Description, Type, Tags); - internal enum MetricType + public enum MetricType { Counter, Histogram, @@ -144,15 +150,12 @@ public static string[] GetTagsInOrder(IReadOnlyList tagNames, ReadOnlySp } // fall back to tag bag lookup - if (tagValue == null) + tagValue ??= tagName switch { - tagValue = tagName switch - { - MetricRegistry.ConcentratorIdTagName when metricTagBag.StationEui.Value is { } stationEui => stationEui.ToString(), - MetricRegistry.GatewayIdTagName => metricTagBag.GatewayId, - _ => null - }; - } + MetricRegistry.ConcentratorIdTagName when metricTagBag.StationEui.Value is { } stationEui => stationEui.ToString(), + MetricRegistry.GatewayIdTagName => metricTagBag.GatewayId, + _ => null + }; if (string.IsNullOrEmpty(tagValue)) throw new LoRaProcessingException($"Tag '{tagName}' is not defined.", LoRaProcessingErrorCode.TagNotSet); @@ -172,16 +175,16 @@ internal static class MetricsExtensions public static Counter CreateCounter(this Meter meter, CustomMetric customMetric) where T : struct => customMetric.Type == MetricType.Counter ? meter.CreateCounter(customMetric.Name, description: customMetric.Description) - : throw new ArgumentException("Custom metric must of type Counter", nameof(customMetric)); + : throw new ArgumentException("Custom metric must be of type Counter", nameof(customMetric)); public static Histogram CreateHistogram(this Meter meter, CustomMetric customMetric) where T : struct => customMetric.Type == MetricType.Histogram ? meter.CreateHistogram(customMetric.Name, description: customMetric.Description) - : throw new ArgumentException("Custom metric must of type Histogram", nameof(customMetric)); + : throw new ArgumentException("Custom metric must be of type Histogram", nameof(customMetric)); public static ObservableGauge CreateObservableGauge(this Meter meter, CustomMetric customMetric, Func observeValue) where T : struct => customMetric.Type == MetricType.ObservableGauge ? meter.CreateObservableGauge(customMetric.Name, observeValue, description: customMetric.Description) - : throw new ArgumentException("Custom metric must of type Histogram", nameof(customMetric)); + : throw new ArgumentException("Custom metric must be of type Histogram", nameof(customMetric)); } } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/MultiGatewayFrameCounterUpdateStrategy.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/MultiGatewayFrameCounterUpdateStrategy.cs index 6944d91571..1545a53f29 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/MultiGatewayFrameCounterUpdateStrategy.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/MultiGatewayFrameCounterUpdateStrategy.cs @@ -24,12 +24,7 @@ public async Task ResetAsync(LoRaDevice loRaDevice, uint fcntUp, string ga loRaDevice.ResetFcnt(); - if (await InternalSaveChangesAsync(loRaDevice, force: true)) - { - return await this.loRaDeviceAPIService.ABPFcntCacheResetAsync(loRaDevice.DevEUI, fcntUp, gatewayId); - } - - return false; + return await this.loRaDeviceAPIService.ABPFcntCacheResetAsync(loRaDevice.DevEUI, fcntUp, gatewayId); } public async ValueTask NextFcntDown(LoRaDevice loRaDevice, uint messageFcnt) diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/NetworkServerConfiguration.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/NetworkServerConfiguration.cs index 8e6cb0873f..c3789468be 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/NetworkServerConfiguration.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/NetworkServerConfiguration.cs @@ -131,10 +131,27 @@ public class NetworkServerConfiguration /// public string LnsVersion { get; private set; } + /// + /// Gets the connection string of Redis server for Pub/Sub functionality in Cloud only deployments. + /// + public string RedisConnectionString { get; private set; } + + /// /// Specifies the pool size for upstream AMQP connection /// public uint IotHubConnectionPoolSize { get; internal set; } = 1; + /// + /// Specificies wether we are running in local development mode. + /// + public bool IsLocalDevelopment { get; set; } + + + /// + /// Specifies the Processing Delay in Milliseconds + /// + public int ProcessingDelayInMilliseconds { get; set; } = Constants.DefaultProcessingDelayInMilliseconds; + // Creates a new instance of NetworkServerConfiguration by reading values from environment variables public static NetworkServerConfiguration CreateFromEnvironmentVariables() { @@ -142,12 +159,23 @@ public static NetworkServerConfiguration CreateFromEnvironmentVariables() // Create case insensitive dictionary from environment variables var envVars = new CaseInsensitiveEnvironmentVariables(Environment.GetEnvironmentVariables()); + config.ProcessingDelayInMilliseconds = envVars.GetEnvVar("PROCESSING_DELAY_IN_MS", config.ProcessingDelayInMilliseconds); + config.IsLocalDevelopment = envVars.GetEnvVar("LOCAL_DEVELOPMENT", false); + // We disable IoT Edge runtime either when we run in the cloud or during local development. + config.RunningAsIoTEdgeModule = !(envVars.GetEnvVar("CLOUD_DEPLOYMENT", false) || config.IsLocalDevelopment); + var iotHubHostName = envVars.GetEnvVar("IOTEDGE_IOTHUBHOSTNAME", envVars.GetEnvVar("IOTHUBHOSTNAME", string.Empty)); + config.IoTHubHostName = !string.IsNullOrEmpty(iotHubHostName) ? iotHubHostName : throw new InvalidOperationException("Either 'IOTEDGE_IOTHUBHOSTNAME' or 'IOTHUBHOSTNAME' environment variable should be populated"); - config.RunningAsIoTEdgeModule = !string.IsNullOrEmpty(envVars.GetEnvVar("IOTEDGE_APIVERSION", string.Empty)); - config.IoTHubHostName = envVars.GetEnvVar("IOTEDGE_IOTHUBHOSTNAME", string.Empty); config.GatewayHostName = envVars.GetEnvVar("IOTEDGE_GATEWAYHOSTNAME", string.Empty); - config.EnableGateway = envVars.GetEnvVar("ENABLE_GATEWAY", config.EnableGateway); - config.GatewayID = envVars.GetEnvVar("IOTEDGE_DEVICEID", string.Empty); + config.EnableGateway = envVars.GetEnvVar("ENABLE_GATEWAY", true); + if (!config.RunningAsIoTEdgeModule && config.EnableGateway) + { + throw new NotSupportedException("ENABLE_GATEWAY cannot be true if RunningAsIoTEdgeModule is false."); + } + + var gatewayId = envVars.GetEnvVar("IOTEDGE_DEVICEID", envVars.GetEnvVar("HOSTNAME", string.Empty)); + config.GatewayID = !string.IsNullOrEmpty(gatewayId) ? gatewayId : throw new InvalidOperationException("Either 'IOTEDGE_DEVICEID' or 'HOSTNAME' environment variable should be populated"); + config.HttpsProxy = envVars.GetEnvVar("HTTPS_PROXY", string.Empty); config.Rx2DataRate = envVars.GetEnvVar("RX2_DATR", -1) is var datrNum && (DataRateIndex)datrNum is var datr && Enum.IsDefined(datr) ? datr : null; config.Rx2Frequency = envVars.GetEnvVar("RX2_FREQ") is { } someFreq ? Hertz.Mega(someFreq) : null; @@ -182,6 +210,10 @@ public static NetworkServerConfiguration CreateFromEnvironmentVariables() ? size : throw new NotSupportedException($"'IOTHUB_CONNECTION_POOL_SIZE' needs to be between 1 and {AmqpConnectionPoolSettings.AbsoluteMaxPoolSize}."); + config.RedisConnectionString = envVars.GetEnvVar("REDIS_CONNECTION_STRING", string.Empty); + if (!config.RunningAsIoTEdgeModule && !config.IsLocalDevelopment && string.IsNullOrEmpty(config.RedisConnectionString)) + throw new InvalidOperationException("'REDIS_CONNECTION_STRING' can't be empty if running network server as part of a cloud only deployment."); + return config; } } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/NullDisposable.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/NullDisposable.cs deleted file mode 100644 index 90162b27c5..0000000000 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/NullDisposable.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace LoRaWan.NetworkServer -{ - using System; - - /// - /// Helper for a reusable . - /// - internal class NullDisposable : IDisposable - { - private static readonly NullDisposable instance = new NullDisposable(); - - internal static IDisposable Instance => instance; - - private NullDisposable() - { - } - - void IDisposable.Dispose() - { - // do nothing - } - } -} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/PrometheusMetricExporter.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/PrometheusMetricExporter.cs index 8a1519ce86..25f47119f0 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/PrometheusMetricExporter.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/PrometheusMetricExporter.cs @@ -42,9 +42,9 @@ internal PrometheusMetricExporter(IDictionary registryLook this.gauges = GetMetricsFromRegistry(MetricType.ObservableGauge, m => Metrics.CreateGauge(m.Name, m.Description, m.Tags)); IDictionary GetMetricsFromRegistry(MetricType metricType, Func factory) => - this.registryLookup.Values.Where(m => m.Type == metricType) - .Select(m => KeyValuePair.Create(m.Name, factory(m))) - .ToDictionary(m => m.Key, m => m.Value); + RegistryLookup.Values.Where(m => m.Type == metricType) + .Select(m => KeyValuePair.Create(m.Name, factory(m))) + .ToDictionary(m => m.Key, m => m.Value); this.metricTagBag = metricTagBag; } @@ -69,7 +69,7 @@ protected override void TrackValue(Instrument instrument, double measurement, Re }; #pragma warning restore format - var inOrderTags = MetricExporterHelper.GetTagsInOrder(this.registryLookup[instrument.Name].Tags, tags, this.metricTagBag); + var inOrderTags = MetricExporterHelper.GetTagsInOrder(RegistryLookup[instrument.Name].Tags, tags, this.metricTagBag); trackMetric(instrument.Name, inOrderTags, measurement); } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/RegistryMetricExporter.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/RegistryMetricExporter.cs index e1dcfdfe52..89ecbf505e 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/RegistryMetricExporter.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/RegistryMetricExporter.cs @@ -12,19 +12,27 @@ namespace LoRaWan.NetworkServer using System.Threading.Tasks; using Microsoft.Extensions.Logging; - internal abstract class RegistryMetricExporter : IMetricExporter + public abstract class RegistryMetricExporter : IMetricExporter { private static readonly TimeSpan ObserveInterval = TimeSpan.FromSeconds(30); private readonly CancellationTokenSource cancellationTokenSource; - protected readonly IDictionary registryLookup; + private readonly string registryNamespace; + protected IDictionary RegistryLookup { get; private set; } private readonly ILogger logger; private MeterListener? listener; private bool disposedValue; - public RegistryMetricExporter(IDictionary registryLookup, ILogger logger) + protected RegistryMetricExporter(IDictionary registryLookup, ILogger logger) + : this(MetricRegistry.Namespace, registryLookup, logger) + { } + + protected RegistryMetricExporter(string registryNamespace, + IDictionary registryLookup, + ILogger logger) { - this.registryLookup = registryLookup; + this.registryNamespace = registryNamespace; + RegistryLookup = registryLookup; this.logger = logger; this.cancellationTokenSource = new CancellationTokenSource(); } @@ -35,7 +43,7 @@ public void Start() { InstrumentPublished = (instrument, meterListener) => { - if (instrument.Meter.Name == MetricRegistry.Namespace && this.registryLookup.ContainsKey(instrument.Name)) + if (instrument.Meter.Name == this.registryNamespace && RegistryLookup.ContainsKey(instrument.Name)) { meterListener.EnableMeasurementEvents(instrument); } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ServiceFacadeHttpClientProvider.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ServiceFacadeHttpClientProvider.cs deleted file mode 100644 index 78d0f318ad..0000000000 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/ServiceFacadeHttpClientProvider.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace LoRaWan.NetworkServer -{ - using System; - using System.Net; - using System.Net.Http; - using LoRaTools.CommonAPI; - using LoRaWan.Core; - - /// - /// Default implementation for . - /// - public class ServiceFacadeHttpClientProvider : IServiceFacadeHttpClientProvider - { - private readonly NetworkServerConfiguration configuration; - private readonly ApiVersion expectedFunctionVersion; - private readonly Lazy httpClient; - - public ServiceFacadeHttpClientProvider(NetworkServerConfiguration configuration, - ApiVersion expectedFunctionVersion) - { - this.configuration = configuration; - this.expectedFunctionVersion = expectedFunctionVersion; - this.httpClient = new Lazy(CreateHttpClient); - } - - public HttpClient GetHttpClient() => this.httpClient.Value; - - private HttpClient CreateHttpClient() - { -#pragma warning disable CA2000 // Dispose objects before losing scope - // Will be handled once we migrate to DI. - // https://github.com/Azure/iotedge-lorawan-starterkit/issues/534 - var handler = new ServiceFacadeHttpClientHandler(this.expectedFunctionVersion); -#pragma warning restore CA2000 // Dispose objects before losing scope - - if (!string.IsNullOrEmpty(this.configuration.HttpsProxy)) - { - var webProxy = new WebProxy( - new Uri(this.configuration.HttpsProxy), - BypassOnLocal: false); - - handler.Proxy = webProxy; - handler.UseProxy = true; - } - -#pragma warning disable CA5399 // Definitely disable HttpClient certificate revocation list check - // Related to: https://github.com/Azure/iotedge-lorawan-starterkit/issues/534 - // Will be resolved once we migrate to DI - return new HttpClient(handler); -#pragma warning restore CA5399 // Definitely disable HttpClient certificate revocation list check - } - } -} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/SingleGatewayFrameCounterUpdateStrategy.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/SingleGatewayFrameCounterUpdateStrategy.cs index 11fc65013d..0833b05edf 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/SingleGatewayFrameCounterUpdateStrategy.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/SingleGatewayFrameCounterUpdateStrategy.cs @@ -8,12 +8,12 @@ namespace LoRaWan.NetworkServer public sealed class SingleGatewayFrameCounterUpdateStrategy : ILoRaDeviceFrameCounterUpdateStrategy, ILoRaDeviceInitializer { - public async Task ResetAsync(LoRaDevice loRaDevice, uint fcntUp, string gatewayId) + public Task ResetAsync(LoRaDevice loRaDevice, uint fcntUp, string gatewayId) { if (loRaDevice is null) throw new ArgumentNullException(nameof(loRaDevice)); loRaDevice.ResetFcnt(); - return await InternalSaveChangesAsync(loRaDevice, force: true); + return Task.FromResult(true); // always able to reset locally } public ValueTask NextFcntDown(LoRaDevice loRaDevice, uint messageFcnt) diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/TaskExtensions.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/TaskExtensions.cs new file mode 100644 index 0000000000..6b72fda984 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/TaskExtensions.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.NetworkServer +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Threading.Tasks; + + internal static class TaskExtensions + { + public static IList GetExceptions(this ICollection tasks) + { + if (tasks.Any(static t => !t.IsCompleted)) throw new ArgumentException("All tasks must have completed.", nameof(tasks)); + + var result = new List(); + + foreach (var task in tasks) + { + if (task.IsCompletedSuccessfully) + continue; + + if (task.IsCanceled && task.TryGetCanceledException(out var ex)) + result.Add(ex); + + if (task is { IsFaulted: true, Exception: { } aggregateException }) + result.Add(aggregateException.InnerExceptions is { Count: 1 } exceptions ? exceptions[0] : aggregateException); + } + + return result; + } + + public static bool TryGetCanceledException(this Task task, [NotNullWhen(true)] out OperationCanceledException? exception) + { + exception = null; + + if (!task.IsCanceled) + return false; + + try + { + task.GetAwaiter().GetResult(); + return false; + } + catch (OperationCanceledException ex) + { + exception = ex; + return true; + } + } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/TwinProperty.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/TwinProperty.cs index 51e5b1f581..233490168b 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/TwinProperty.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/TwinProperty.cs @@ -23,7 +23,7 @@ public static class TwinProperty public const string NetId = "NetId"; // Must be NetId to be backward compatible public const string DownlinkEnabled = "Downlink"; public const string PreferredWindow = "PreferredWindow"; // (1 or 2) - public const string Deduplication = "Deduplication"; // None (default), Drop, Mark + public const string Deduplication = "Deduplication"; // None, Drop (default), Mark public const string ClassType = "ClassType"; public const string Supports32BitFCnt = "Supports32BitFCnt"; public const string FCntResetCounter = "FCntResetCounter"; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/DataRate.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/DataRate.cs index 02f4929a18..cae015ea74 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/DataRate.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/DataRate.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -#nullable enable - namespace LoRaWan { using System; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/DataRateIndex.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/DataRateIndex.cs index 8d4182250a..a3c066fcbd 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/DataRateIndex.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/DataRateIndex.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -#nullable enable - namespace LoRaWan { /// diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/LoRaProcessingException.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/LoRaProcessingException.cs index 0c6fa1e6bc..edd4a95e25 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/LoRaProcessingException.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/LoRaProcessingException.cs @@ -65,6 +65,7 @@ public enum LoRaProcessingErrorCode InvalidFrequency, InvalidFormat, PayloadNotSet, - RegionNotSet + RegionNotSet, + LnsDiscoveryFailed } } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/Metric.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/Metric.cs index 21a230511d..6616ccbc55 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/Metric.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan/Metric.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -#nullable enable - namespace LoRaWan { using System; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/ADR/LoRaADRManagerBase.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/ADR/LoRaADRManagerBase.cs index ddb356523c..c00e3b628f 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/ADR/LoRaADRManagerBase.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/ADR/LoRaADRManagerBase.cs @@ -73,12 +73,7 @@ public virtual async Task CalculateADRResultAndAddEntryAsync(DevE if (result.CanConfirmToDevice) { - if (table == null) - { - // in a reset case, we may not have a table, but still want to store the default - // values that we sent to the client - table = new LoRaADRTable(); - } + table ??= new LoRaADRTable(); table.CurrentNbRep = result.NbRepetition; table.CurrentTxPower = result.TxPower; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/CommonAPI/ApiVersion.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/CommonAPI/ApiVersion.cs index 78016140da..c149fe38d8 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/CommonAPI/ApiVersion.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/CommonAPI/ApiVersion.cs @@ -28,7 +28,7 @@ public sealed class ApiVersion /// Gets the latest version released. /// Update this once a new API version is released. /// - public static ApiVersion LatestVersion => Version_2020_10_09; + public static ApiVersion LatestVersion => Version_2022_03_04; /// /// Gets the Version from 0.1 and 0.2 had not versioning information. @@ -109,6 +109,15 @@ public sealed class ApiVersion /// public static ApiVersion Version_2020_10_09 { get; } + public static ApiVersion Version_2022_03_04 { get; } + + /// + /// Gets2022_09_01 version + /// Enable outside of Edge support + /// Not backward compatible. + /// + public static ApiVersion Version_2022_09_01 { get; } + /// /// Gets the version that is assumed in case none is specified. /// @@ -133,6 +142,8 @@ public static IEnumerable ApiVersions yield return Version_2019_07_16; yield return Version_2020_08_11; yield return Version_2020_10_09; + yield return Version_2022_03_04; + yield return Version_2022_09_01; } } @@ -199,6 +210,12 @@ static ApiVersion() Version_2020_10_09 = new ApiVersion("2020-10-09"); Version_2020_10_09.MinCompatibleVersion = Version_2020_10_09; + + Version_2022_03_04 = new ApiVersion("2022-03-04"); + Version_2022_03_04.MinCompatibleVersion = Version_2022_03_04; + + Version_2022_09_01 = new ApiVersion("2022-09-01"); + Version_2022_09_01.MinCompatibleVersion = Version_2022_09_01; } /// diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/CommonAPI/DeviceJoinNotification.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/CommonAPI/DeviceJoinNotification.cs new file mode 100644 index 0000000000..e853679017 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/CommonAPI/DeviceJoinNotification.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaTools.CommonAPI +{ + using LoRaWan; + using Newtonsoft.Json; + + public class DeviceJoinNotification + { + public string GatewayId { get; set; } + + [JsonIgnore] + public DevAddr DevAddr { get; set; } + + [JsonIgnore] + public DevEui? DevEUI { get; set; } + + [JsonIgnore] + public NetworkSessionKey? NwkSKey { get; set; } + + [JsonProperty("DevAddr")] + public string DevAddrString + { + get => DevAddr.ToString(); + set => DevAddr = DevAddr.Parse(value); + } + + [JsonProperty("DevEUI")] + public string DevEuiString + { + get => DevEUI?.ToString(); + set => DevEUI = value is null ? null : DevEui.Parse(value); + } + + [JsonProperty("NwkSKey")] + public string NwkSKeyString + { + get => NwkSKey?.ToString(); + set => NwkSKey = value is null ? null : NetworkSessionKey.Parse(value); + } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Constants.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Constants.cs new file mode 100644 index 0000000000..da9840d8bf --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Constants.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaTools +{ + public static class Constants + { + public const string NetworkServerModuleId = "LoRaWanNetworkSrvModule"; + public const string RoundTripDateTimeStringFormat = "o"; + + public const string AbpDeviceId = "46AAC86800430028"; + public const string OtaaDeviceId = "47AAC86800430028"; + + public const string NetworkTagName = "network"; + public const string NetworkId = "quickstartnetwork"; + + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IDeviceRegistryManager.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IDeviceRegistryManager.cs new file mode 100644 index 0000000000..ce5cffedba --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IDeviceRegistryManager.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaTools +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using LoRaWan; + + public interface IDeviceRegistryManager + { + Task GetTwinAsync(string deviceId, CancellationToken? cancellationToken = null); + Task GetLoRaDeviceTwinAsync(string deviceId, CancellationToken? cancellationToken = null); + Task GetDevicePrimaryKeyAsync(string deviceId); + Task UpdateTwinAsync(string deviceId, string moduleId, IDeviceTwin deviceTwin, string eTag, CancellationToken cancellationToken); + Task AddDeviceAsync(IDeviceTwin twin); + IRegistryPageResult GetEdgeDevices(); + IRegistryPageResult GetAllLoRaDevices(); + IRegistryPageResult GetLastUpdatedLoRaDevices(DateTime lastUpdateDateTime); + IRegistryPageResult FindLoRaDeviceByDevAddr(DevAddr someDevAddr); + IRegistryPageResult FindLnsByNetworkId(string networkId); + IRegistryPageResult FindDeviceByDevEUI(DevEui devEUI); + Task UpdateTwinAsync(string deviceName, IDeviceTwin twin, string eTag); + Task RemoveDeviceAsync(string deviceId); + Task DeployEdgeDeviceAsync( + string deviceId, + string resetPin, + string spiSpeed, + string spiDev, + string publishingUserName, + string publishingPassword, + string networkId = Constants.NetworkId, + string lnsHostAddress = "ws://mylns:5000"); + Task DeployConcentratorAsync(string stationEuiString, string region, string networkId = Constants.NetworkId); + Task DeployEndDevicesAsync(); + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IDeviceTwin.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IDeviceTwin.cs new file mode 100644 index 0000000000..8c442ce0cf --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IDeviceTwin.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaTools +{ + using Microsoft.Azure.Devices.Shared; + + public interface IDeviceTwin + { + string ETag { get; } + + TwinProperties Properties { get; } + + TwinCollection Tags { get; } + + string DeviceId { get; } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/ILoRaDeviceTwin.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/ILoRaDeviceTwin.cs new file mode 100644 index 0000000000..1ce580821a --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/ILoRaDeviceTwin.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaTools +{ + public interface ILoRaDeviceTwin : IDeviceTwin + { + string GetGatewayID(); + + string GetNwkSKey(); + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/ILoggerExtensions.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/ILoggerExtensions.cs similarity index 85% rename from LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/ILoggerExtensions.cs rename to LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/ILoggerExtensions.cs index 5e89244ad0..7dc9504ca9 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/ILoggerExtensions.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/ILoggerExtensions.cs @@ -1,10 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace LoRaWan +namespace LoRaTools { using System; using System.Collections.Generic; + using LoRaWan; using Microsoft.Extensions.Logging; public static class ILoggerExtensions @@ -18,8 +19,8 @@ public static class ILoggerExtensions ? logger?.BeginScope(new Dictionary { [DevEUIKey] = someDevEui.ToString() }) : NoopDisposable.Instance; - public static IDisposable BeginDeviceAddressScope(this ILogger logger, DevAddr devAddr) => - logger?.BeginDeviceAddressScope(devAddr.ToString()); + public static IDisposable BeginDeviceAddressScope(this ILogger logger, DevAddr? devAddr) => + devAddr is { } someDevAddr ? logger?.BeginDeviceAddressScope(someDevAddr.ToString()) : NoopDisposable.Instance; public static IDisposable BeginDeviceAddressScope(this ILogger logger, string deviceAddress) => logger?.BeginScope(new Dictionary { [DeviceAddressKey] = deviceAddress }); diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IRegistryPageResult.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IRegistryPageResult.cs new file mode 100644 index 0000000000..8845cb5c97 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IRegistryPageResult.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaTools +{ + using System.Collections.Generic; + using System.Threading.Tasks; + + public interface IRegistryPageResult + { + Task> GetNextPageAsync(); + + bool HasMoreResults { get; } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/DeviceTwinExtensions.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/DeviceTwinExtensions.cs new file mode 100644 index 0000000000..a111bff3e7 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/DeviceTwinExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaTools.IoTHubImpl +{ + using System; + using Microsoft.Azure.Devices.Shared; + + internal static class DeviceTwinExtensions + { + internal static Twin ToIoTHubDeviceTwin(this IDeviceTwin twin) + { + ArgumentNullException.ThrowIfNull(twin, nameof(twin)); + + if (twin is not IoTHubDeviceTwin iotHubDeviceTwin) + { + throw new ArgumentException($"Cannot convert {twin.GetType().Name} to IoTHubDeviceTwin instance."); + } + + return iotHubDeviceTwin.TwinInstance; + } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubDeviceTwin.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubDeviceTwin.cs new file mode 100644 index 0000000000..9bcb7bb539 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubDeviceTwin.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaTools.IoTHubImpl +{ + using System; + using LoRaTools.Utils; + using Microsoft.Azure.Devices.Shared; + + public class IoTHubDeviceTwin : IDeviceTwin + { + internal Twin TwinInstance { get; } + + public TwinProperties Properties => this.TwinInstance.Properties; + + public TwinCollection Tags => this.TwinInstance.Tags; + + public IoTHubDeviceTwin(Twin twin) + { + this.TwinInstance = twin; + } + + public string ETag => this.TwinInstance.ETag; + + public string DeviceId => this.TwinInstance.DeviceId; + + public override bool Equals(object obj) + { + ArgumentNullException.ThrowIfNull(obj, nameof(obj)); + + if (obj is not IoTHubDeviceTwin) + { + return false; + } + + return (obj as IoTHubDeviceTwin)?.TwinInstance == this.TwinInstance; + } + + public override int GetHashCode() + { + return TwinInstance.GetHashCode(); + } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubDeviceTwinPageResult.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubDeviceTwinPageResult.cs new file mode 100644 index 0000000000..6468a77743 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubDeviceTwinPageResult.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaTools.IoTHubImpl +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.Azure.Devices; + + public class IoTHubDeviceTwinPageResult : IoTHubRegistryPageResult + { + public IoTHubDeviceTwinPageResult(IQuery originalQuery) + : base(originalQuery) + { + } + + public override async Task> GetNextPageAsync() + { + var page = await this.OriginalQuery.GetNextAsTwinAsync(); + + return page.Select(c => new IoTHubDeviceTwin(c)); + } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubLoRaDeviceTwin.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubLoRaDeviceTwin.cs new file mode 100644 index 0000000000..ccbc93c814 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubLoRaDeviceTwin.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaTools.IoTHubImpl +{ + using LoRaTools.Utils; + using Microsoft.Azure.Devices.Shared; + + internal class IoTHubLoRaDeviceTwin : IoTHubDeviceTwin, ILoRaDeviceTwin + { + public IoTHubLoRaDeviceTwin(Twin twin) : base(twin) + { + } + + public string GetGatewayID() + => TwinInstance.Properties.Desired.TryRead(TwinPropertiesConstants.GatewayID, null, out var someGatewayId) + ? someGatewayId + : string.Empty; + + public string GetNwkSKey() + { + return TwinInstance.Properties.Desired.TryRead(TwinPropertiesConstants.NwkSKey, null, out string nwkSKey) + ? nwkSKey + : TwinInstance.Properties.Reported.TryRead(TwinPropertiesConstants.NwkSKey, null, out nwkSKey) + ? nwkSKey + : null; + } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubLoRaDeviceTwinPageResult.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubLoRaDeviceTwinPageResult.cs new file mode 100644 index 0000000000..b598863d96 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubLoRaDeviceTwinPageResult.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaTools.IoTHubImpl +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.Azure.Devices; + + public class IoTHubLoRaDeviceTwinPageResult : IoTHubRegistryPageResult + { + public IoTHubLoRaDeviceTwinPageResult(IQuery originalQuery) + : base(originalQuery) + { + } + + public override async Task> GetNextPageAsync() + { + var page = await this.OriginalQuery.GetNextAsTwinAsync(); + + return page.Select(c => new IoTHubLoRaDeviceTwin(c)); + } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubRegistryManager.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubRegistryManager.cs new file mode 100644 index 0000000000..e3928d20e7 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubRegistryManager.cs @@ -0,0 +1,245 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaTools.IoTHubImpl +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Net.Http; + using System.Runtime.CompilerServices; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using LoRaWan; + using Microsoft.Azure.Devices; + using Microsoft.Azure.Devices.Shared; + using Microsoft.Extensions.Logging; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + public sealed class IoTHubRegistryManager : IDeviceRegistryManager, IDisposable + { + private readonly RegistryManager instance; + private readonly IHttpClientFactory httpClientFactory; + private readonly ILogger logger; + + public static IDeviceRegistryManager CreateWithProvider( + Func registryManagerProvider, + IHttpClientFactory httpClientFactory, + ILogger logger) + { + return registryManagerProvider == null + ? throw new ArgumentNullException(nameof(registryManagerProvider)) + : (IDeviceRegistryManager)new IoTHubRegistryManager(registryManagerProvider, httpClientFactory, logger); + } + + internal IoTHubRegistryManager(Func registryManagerProvider, IHttpClientFactory httpClientFactory, ILogger logger) + { + this.instance = registryManagerProvider() ?? throw new InvalidOperationException("RegistryManager provider provided a null RegistryManager."); + this.httpClientFactory = httpClientFactory; + this.logger = logger; + } + + public async Task AddDeviceAsync(IDeviceTwin twin) + { + var result = await this.instance.AddDeviceWithTwinAsync(new Device(twin?.DeviceId), twin.ToIoTHubDeviceTwin()); + + if (result.IsSuccessful) + return true; + + this.logger.LogWarning($"Failed to add Device with twin: \n{result.Errors}"); + + return false; + } + + public void Dispose() => this.instance?.Dispose(); + + public async Task GetDevicePrimaryKeyAsync(string deviceId) + { + var device = await this.instance.GetDeviceAsync(deviceId); + + return device?.Authentication?.SymmetricKey?.PrimaryKey; + } + + public async Task UpdateTwinAsync(string deviceName, IDeviceTwin twin, string eTag) + => new IoTHubDeviceTwin(await this.instance.UpdateTwinAsync(deviceName, twin.ToIoTHubDeviceTwin(), eTag)); + + public async Task UpdateTwinAsync(string deviceId, string moduleId, IDeviceTwin deviceTwin, string eTag, CancellationToken cancellationToken) + => new IoTHubDeviceTwin(await this.instance.UpdateTwinAsync(deviceId, moduleId, deviceTwin.ToIoTHubDeviceTwin(), eTag, cancellationToken)); + + public Task RemoveDeviceAsync(string deviceId) + => this.instance.RemoveDeviceAsync(deviceId); + + public IRegistryPageResult GetEdgeDevices() + { + var q = this.instance.CreateQuery($"SELECT * FROM devices.modules where moduleId = '{Constants.NetworkServerModuleId}'"); + return new IoTHubDeviceTwinPageResult(q); + } + + public IRegistryPageResult GetAllLoRaDevices() + { + var q = this.instance.CreateQuery("SELECT * FROM devices WHERE is_defined(properties.desired.AppKey) OR is_defined(properties.desired.AppSKey) OR is_defined(properties.desired.NwkSKey)"); + return new IoTHubLoRaDeviceTwinPageResult(q); + } + + public IRegistryPageResult GetLastUpdatedLoRaDevices(DateTime lastUpdateDateTime) + { + var formattedDateTime = lastUpdateDateTime.ToString(Constants.RoundTripDateTimeStringFormat, CultureInfo.InvariantCulture); + var q = this.instance.CreateQuery($"SELECT * FROM devices where properties.desired.$metadata.$lastUpdated >= '{formattedDateTime}' OR properties.reported.$metadata.DevAddr.$lastUpdated >= '{formattedDateTime}'"); + return new IoTHubLoRaDeviceTwinPageResult(q); + } + + public IRegistryPageResult FindLoRaDeviceByDevAddr(DevAddr someDevAddr) + { + var q = this.instance.CreateQuery($"SELECT * FROM devices WHERE properties.desired.DevAddr = '{someDevAddr}' OR properties.reported.DevAddr ='{someDevAddr}'", 100); + return new IoTHubLoRaDeviceTwinPageResult(q); + } + + public IRegistryPageResult FindLnsByNetworkId(string networkId) + { + var q = this.instance.CreateQuery($"SELECT properties.desired.hostAddress, deviceId FROM devices.modules WHERE tags.network = '{networkId}'"); + return new JsonPageResult(q); + } + + public IRegistryPageResult FindDeviceByDevEUI(DevEui devEUI) + { + var q = this.instance.CreateQuery($"SELECT * FROM devices WHERE deviceId = '{devEUI}'", 1); + return new IoTHubLoRaDeviceTwinPageResult(q); + } + + public async Task GetLoRaDeviceTwinAsync(string deviceId, CancellationToken? cancellationToken = null) + => await this.instance.GetTwinAsync(deviceId, cancellationToken ?? CancellationToken.None) is { } twin ? new IoTHubLoRaDeviceTwin(twin) : null; + + public async Task GetTwinAsync(string deviceId, CancellationToken? cancellationToken = null) + => await this.instance.GetTwinAsync(deviceId, cancellationToken ?? CancellationToken.None) is { } twin ? new IoTHubDeviceTwin(twin) : null; + + public async Task DeployEdgeDeviceAsync( + string deviceId, + string resetPin, + string spiSpeed, + string spiDev, + string publishingUserName, + string publishingPassword, + string networkId = Constants.NetworkId, + string lnsHostAddress = "ws://mylns:5000") + { + // Get function facade key + var base64Auth = Convert.ToBase64String(Encoding.Default.GetBytes($"{publishingUserName}:{publishingPassword}")); + var apiUrl = new Uri($"https://{Environment.GetEnvironmentVariable("WEBSITE_CONTENTSHARE")}.scm.azurewebsites.net"); + var siteUrl = new Uri($"https://{Environment.GetEnvironmentVariable("WEBSITE_CONTENTSHARE")}.azurewebsites.net"); + string jwt; + using (var client = this.httpClientFactory.CreateClient()) + { + client.DefaultRequestHeaders.Add("Authorization", $"Basic {base64Auth}"); + var result = await client.GetAsync(new Uri(apiUrl, "/api/functions/admin/token")); + jwt = (await result.Content.ReadAsStringAsync()).Trim('"'); // get JWT for call funtion key + } + + var facadeKey = string.Empty; + using (var client = this.httpClientFactory.CreateClient()) + { + client.DefaultRequestHeaders.Add("Authorization", "Bearer " + jwt); + var response = await client.GetAsync(new Uri(siteUrl, "/admin/host/keys")); + var jsonResult = await response.Content.ReadAsStringAsync(); + dynamic resObject = JsonConvert.DeserializeObject(jsonResult); + facadeKey = resObject.keys[0].value; + } + + var edgeGatewayDevice = new Device(deviceId) + { + Capabilities = new DeviceCapabilities() + { + IotEdge = true + } + }; + + _ = await this.instance.AddDeviceAsync(edgeGatewayDevice); + _ = await this.instance.AddModuleAsync(new Module(deviceId, "LoRaWanNetworkSrvModule")); + + async Task GetConfigurationContentAsync(Uri configLocation, IDictionary tokenReplacements) + { + using var httpClient = this.httpClientFactory.CreateClient(); + var json = await httpClient.GetStringAsync(configLocation); + foreach (var r in tokenReplacements) + json = json.Replace(r.Key, r.Value, StringComparison.Ordinal); + return JsonConvert.DeserializeObject(json); + } + + var deviceConfigurationContent = await GetConfigurationContentAsync(new Uri(Environment.GetEnvironmentVariable("DEVICE_CONFIG_LOCATION")), new Dictionary + { + ["[$reset_pin]"] = resetPin, + ["[$spi_speed]"] = string.IsNullOrEmpty(spiSpeed) || string.Equals(spiSpeed, "8", StringComparison.OrdinalIgnoreCase) ? string.Empty : ",'SPI_SPEED':{'value':'2'}", + ["[$spi_dev]"] = string.IsNullOrEmpty(spiDev) || string.Equals(spiDev, "0", StringComparison.OrdinalIgnoreCase) ? string.Empty : $",'SPI_DEV':{{'value':'{spiDev}'}}" + }); + + await this.instance.ApplyConfigurationContentOnDeviceAsync(deviceId, deviceConfigurationContent); + + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("LOG_ANALYTICS_WORKSPACE_ID"))) + { + this.logger.LogDebug("Opted-in to use Azure Monitor on the edge. Deploying the observability layer."); + // If Appinsights Key is set this means that user opted in to use Azure Monitor. + _ = await this.instance.AddModuleAsync(new Module(deviceId, "IotHubMetricsCollectorModule")); + var observabilityConfigurationContent = await GetConfigurationContentAsync(new Uri(Environment.GetEnvironmentVariable("OBSERVABILITY_CONFIG_LOCATION")), new Dictionary + { + ["[$iot_hub_resource_id]"] = Environment.GetEnvironmentVariable("IOT_HUB_RESOURCE_ID"), + ["[$log_analytics_workspace_id]"] = Environment.GetEnvironmentVariable("LOG_ANALYTICS_WORKSPACE_ID"), + ["[$log_analytics_shared_key]"] = Environment.GetEnvironmentVariable("LOG_ANALYTICS_WORKSPACE_KEY") + }); + + _ = await this.instance.AddConfigurationAsync(new Configuration($"obs-{Guid.NewGuid()}") + { + Content = observabilityConfigurationContent, + TargetCondition = $"deviceId='{deviceId}'" + }); + } + + var twin = new Twin(); + twin.Properties.Desired = new TwinCollection($"{{FacadeServerUrl:'https://{Environment.GetEnvironmentVariable("FACADE_HOST_NAME", EnvironmentVariableTarget.Process)}.azurewebsites.net/api/',FacadeAuthCode: '{facadeKey}'}}"); + twin.Properties.Desired["hostAddress"] = new Uri(lnsHostAddress); + twin.Tags[Constants.NetworkTagName] = networkId; + var remoteTwin = await this.instance.GetTwinAsync(deviceId); + + _ = await this.instance.UpdateTwinAsync(deviceId, "LoRaWanNetworkSrvModule", twin, remoteTwin.ETag); + } + + public async Task DeployConcentratorAsync(string stationEuiString, string region, string networkId = Constants.NetworkId) + { + // Deploy concentrator + using var httpClient = this.httpClientFactory.CreateClient(); + var regionalConfiguration = region switch + { + var s when string.Equals("EU", s, StringComparison.OrdinalIgnoreCase) => await httpClient.GetStringAsync(new Uri(Environment.GetEnvironmentVariable("EU863_CONFIG_LOCATION", EnvironmentVariableTarget.Process))), + var s when string.Equals("US", s, StringComparison.OrdinalIgnoreCase) => await httpClient.GetStringAsync(new Uri(Environment.GetEnvironmentVariable("US902_CONFIG_LOCATION", EnvironmentVariableTarget.Process))), + _ => throw new SwitchExpressionException("Region should be either 'EU' or 'US'") + }; + + var concentratorDevice = new Device(stationEuiString); + _ = await this.instance.AddDeviceAsync(concentratorDevice); + var concentratorTwin = await this.instance.GetTwinAsync(stationEuiString); + concentratorTwin.Properties.Desired["routerConfig"] = JsonConvert.DeserializeObject(regionalConfiguration); + concentratorTwin.Tags[Constants.NetworkTagName] = networkId; + _ = await this.instance.UpdateTwinAsync(stationEuiString, concentratorTwin, concentratorTwin.ETag); + } + + public async Task DeployEndDevicesAsync() + { + var otaaDevice = await this.instance.GetDeviceAsync(Constants.OtaaDeviceId) + ?? await this.instance.AddDeviceAsync(new Device(Constants.OtaaDeviceId)); + + var otaaEndTwin = new Twin(); + otaaEndTwin.Properties.Desired = new TwinCollection(/*lang=json*/ @"{AppEUI:'BE7A0000000014E2',AppKey:'8AFE71A145B253E49C3031AD068277A1',GatewayID:'',SensorDecoder:'DecoderValueSensor'}"); + var otaaRemoteTwin = _ = await this.instance.GetTwinAsync(Constants.OtaaDeviceId); + _ = await this.instance.UpdateTwinAsync(Constants.OtaaDeviceId, otaaEndTwin, otaaRemoteTwin.ETag); + + var abpDevice = await this.instance.GetDeviceAsync(Constants.AbpDeviceId) + ?? await this.instance.AddDeviceAsync(new Device(Constants.AbpDeviceId)); + var abpTwin = new Twin(); + abpTwin.Properties.Desired = new TwinCollection(/*lang=json*/ @"{AppSKey:'2B7E151628AED2A6ABF7158809CF4F3C',NwkSKey:'3B7E151628AED2A6ABF7158809CF4F3C',GatewayID:'',DevAddr:'0228B1B1',SensorDecoder:'DecoderValueSensor'}"); + var abpRemoteTwin = await this.instance.GetTwinAsync(Constants.AbpDeviceId); + _ = await this.instance.UpdateTwinAsync(Constants.AbpDeviceId, abpTwin, abpRemoteTwin.ETag); + + return abpDevice != null && otaaDevice != null; + } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubRegistryPageResult.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubRegistryPageResult.cs new file mode 100644 index 0000000000..321938246d --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubRegistryPageResult.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaTools.IoTHubImpl +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using Microsoft.Azure.Devices; + + public abstract class IoTHubRegistryPageResult : IRegistryPageResult + where TResultType : class + { + protected IQuery OriginalQuery { get; } + + public bool HasMoreResults => this.OriginalQuery.HasMoreResults; + + protected IoTHubRegistryPageResult(IQuery originalQuery) + { + this.OriginalQuery = originalQuery; + } + + public abstract Task> GetNextPageAsync(); + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/JsonPageResult.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/JsonPageResult.cs new file mode 100644 index 0000000000..23a1c6c337 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/JsonPageResult.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaTools.IoTHubImpl +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using Microsoft.Azure.Devices; + + internal class JsonPageResult : IoTHubRegistryPageResult + { + public JsonPageResult(IQuery originalQuery) + : base(originalQuery) + { + } + + public override Task> GetNextPageAsync() + { + return this.OriginalQuery.GetNextAsJsonAsync(); + } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/JsonReader.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/JsonReader.cs similarity index 98% rename from LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/JsonReader.cs rename to LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/JsonReader.cs index 2a61613fd9..c6c77df78b 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/JsonReader.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/JsonReader.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace LoRaWan.NetworkServer +namespace LoRaTools { using System; using System.Collections.Generic; @@ -322,10 +322,10 @@ static void DefaultUnassigned(IJsonProperty property, ref (bool, T) v) private sealed class DelegatingJsonReader : IJsonReader { - private readonly ReadHandler _func; + private readonly ReadHandler func; - public DelegatingJsonReader(ReadHandler func) => _func = func; - public T Read(ref Utf8JsonReader reader) => _func(ref reader); + public DelegatingJsonReader(ReadHandler func) => this.func = func; + public T Read(ref Utf8JsonReader reader) => this.func(ref reader); } } } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/JsonReader.g.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/JsonReader.g.cs similarity index 99% rename from LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/JsonReader.g.cs rename to LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/JsonReader.g.cs index 9a2ea4853a..7985018390 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/JsonReader.g.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/JsonReader.g.cs @@ -6,7 +6,7 @@ // Changes to this file will be lost if the code is re-generated. //------------------------------------------------------------------------------ -namespace LoRaWan.NetworkServer +namespace LoRaTools { using System; using System.Text.Json; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/JsonReader.g.tt b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/JsonReader.g.tt similarity index 98% rename from LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/JsonReader.g.tt rename to LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/JsonReader.g.tt index 90c6b2c5e6..623d806d12 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/JsonReader.g.tt +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/JsonReader.g.tt @@ -11,7 +11,7 @@ // Changes to this file will be lost if the code is re-generated. //------------------------------------------------------------------------------ -namespace LoRaWan.NetworkServer +namespace LoRaTools { using System; using System.Text.Json; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/LnsRemoteCall.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/LnsRemoteCall.cs new file mode 100644 index 0000000000..bec17ab930 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/LnsRemoteCall.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaTools +{ + public sealed record LnsRemoteCall(RemoteCallKind Kind, string? JsonData); + + public enum RemoteCallKind + { + CloudToDeviceMessage, + ClearCache, + CloseConnection + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/LoRaTools.csproj b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/LoRaTools.csproj index d6897c79e1..a433528e34 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/LoRaTools.csproj +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/LoRaTools.csproj @@ -5,15 +5,38 @@ + + + + + - + + + + JsonReader.g.tt + True + True + + + + + + JsonReader.g.cs + TextTemplatingFileGenerator + + + + + + diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/Cid.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/Cid.cs index b7198d2e5f..aa3f4bafcc 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/Cid.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/Cid.cs @@ -3,17 +3,17 @@ namespace LoRaTools { +#pragma warning disable CA1008 // Enums should have zero value (but values reflect CID from spec which has none) public enum Cid +#pragma warning restore CA1008 // Enums should have zero value { - Zero, - One, - LinkCheckCmd, - LinkADRCmd, - DutyCycleCmd, - RXParamCmd, - DevStatusCmd, - NewChannelCmd, - RXTimingCmd, - TxParamSetupCmd = 0x09 + LinkCheckCmd = 2, + LinkADRCmd = 3, + DutyCycleCmd = 4, + RXParamCmd = 5, + DevStatusCmd = 6, + NewChannelCmd = 7, + RXTimingCmd = 8, + TxParamSetupCmd = 9 } } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/DevStatusAnswer.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/DevStatusAnswer.cs index 9436da7416..8f0543bcf6 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/DevStatusAnswer.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/DevStatusAnswer.cs @@ -16,7 +16,7 @@ public class DevStatusAnswer : MacCommand public byte Battery { get; set; } [JsonProperty("margin")] - private byte Margin { get; set; } + public byte Margin { get; set; } public override int Length => 3; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/DutyCycleRequest.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/DutyCycleRequest.cs index f8151a63f6..d59a9fcb3d 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/DutyCycleRequest.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/DutyCycleRequest.cs @@ -20,7 +20,7 @@ public DutyCycleRequest() { } - // Downstream message˙ + // Downstream message public DutyCycleRequest(byte dutyCyclePL) { Cid = Cid.DutyCycleCmd; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/LinkADRAnswer.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/LinkADRAnswer.cs index 41e584606d..e105c90cd7 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/LinkADRAnswer.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/LinkADRAnswer.cs @@ -32,8 +32,9 @@ public class LinkADRAnswer : MacCommand public LinkADRAnswer(byte powerAck, bool dataRateAck, bool channelMaskAck) { Cid = Cid.LinkADRCmd; - Status |= (byte)((byte)(powerAck & 0b00000011) << 2); - Status |= (byte)((byte)(dataRateAck ? 1 << 1 : 0 << 1) | (byte)(channelMaskAck ? 1 : 0)); + Status = unchecked((byte)(((powerAck & 0b00000011) << 2) + | (dataRateAck ? 2 : 0) + | (channelMaskAck ? 1 : 0))); } /// diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/LinkADRRequest.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/LinkADRRequest.cs index 27d69e4740..b9a1801645 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/LinkADRRequest.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/LinkADRRequest.cs @@ -48,7 +48,7 @@ public LinkADRRequest(ushort datarate, ushort txPower, ushort chMask, ushort chM /// /// Initializes a new instance of the class. For tests to serialize from byte. /// - public LinkADRRequest(byte[] input) + internal LinkADRRequest(byte[] input) { if (input is null) throw new ArgumentNullException(nameof(input)); @@ -59,7 +59,7 @@ public LinkADRRequest(byte[] input) Cid = Cid.LinkADRCmd; DataRateTXPower = input[1]; - ChMask = BinaryPrimitives.ReadUInt16LittleEndian(input); + ChMask = BinaryPrimitives.ReadUInt16LittleEndian(input.AsSpan(2)); Redundancy = input[4]; } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/LinkCheckAnswer.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/LinkCheckAnswer.cs index 5712e0b0c9..322e968032 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/LinkCheckAnswer.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/LinkCheckAnswer.cs @@ -33,7 +33,6 @@ public LinkCheckAnswer(byte margin, byte gwCnt) /// /// Initializes a new instance of the class. - /// Test Constructor. /// public LinkCheckAnswer(ReadOnlySpan input) { diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/MACCommand.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/MACCommand.cs index 10123a12ea..47300825cf 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/MACCommand.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/MACCommand.cs @@ -19,6 +19,7 @@ public abstract class MacCommand [JsonProperty("cid")] public Cid Cid { get; set; } + [JsonIgnore] public abstract int Length { get; } public abstract override string ToString(); @@ -107,8 +108,6 @@ public static IList CreateMacCommandFromBytes(ReadOnlyMemory i pointer += txParamSetupAnswer.Length; macCommands.Add(txParamSetupAnswer); break; - case Cid.Zero: - case Cid.One: default: logger?.LogError($"a transmitted Mac Command value ${input.Span[pointer]} was not from a supported type. Aborting Mac Command processing"); return null; @@ -126,13 +125,11 @@ public static IList CreateMacCommandFromBytes(ReadOnlyMemory i /// /// Create a List of Mac commands from server based on a sequence of bytes. /// - public static IList CreateServerMacCommandFromBytes(DevEui deviceId, ReadOnlyMemory input, ILogger logger = null) + public static IList CreateServerMacCommandFromBytes(ReadOnlyMemory input, ILogger logger = null) { var pointer = 0; var macCommands = new List(3); - using var scope = logger?.BeginDeviceScope(deviceId); - while (pointer < input.Length) { try @@ -150,8 +147,6 @@ public static IList CreateServerMacCommandFromBytes(DevEui deviceId, pointer += devStatusRequest.Length; macCommands.Add(devStatusRequest); break; - case Cid.Zero: - case Cid.One: case Cid.LinkADRCmd: case Cid.DutyCycleCmd: case Cid.RXParamCmd: diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/MACCommandJsonConverter.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/MACCommandJsonConverter.cs index 7571a2e48d..df96c9b124 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/MACCommandJsonConverter.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/MACCommandJsonConverter.cs @@ -19,7 +19,7 @@ public override bool CanConvert(Type objectType) return typeof(MacCommand).IsAssignableFrom(objectType); } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (serializer is null) throw new ArgumentNullException(nameof(serializer)); @@ -68,9 +68,6 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist serializer.Populate(item.CreateReader(), cmd); return cmd; } - - case Cid.Zero: - case Cid.One: case Cid.LinkCheckCmd: case Cid.LinkADRCmd: { diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/RXParamSetupRequest.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/RXParamSetupRequest.cs index 40aa4d59d8..4548e4aeb8 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/RXParamSetupRequest.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Mac/RXParamSetupRequest.cs @@ -42,6 +42,7 @@ public RXParamSetupRequest() public RXParamSetupRequest(byte rx1DROffset, byte rx2DataRateOffset, int frequency) { + Cid = Cid.RXParamCmd; DlSettings = (byte)(((rx1DROffset << 4) | rx2DataRateOffset) & 0b01111111); Frequency = frequency; } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/NetworkServerDiscovery/DiscoveryService.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/NetworkServerDiscovery/DiscoveryService.cs new file mode 100644 index 0000000000..2dcba35223 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/NetworkServerDiscovery/DiscoveryService.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaTools.NetworkServerDiscovery +{ + using System; + using System.IO; + using System.Linq; + using System.Net.NetworkInformation; + using System.Net.WebSockets; + using System.Text.Json; + using System.Text.RegularExpressions; + using System.Threading; + using System.Threading.Tasks; + using LoRaTools; + using LoRaWan; + using Microsoft.AspNetCore.Http; + using Microsoft.Extensions.Logging; + + public sealed class DiscoveryService + { + private const string DataEndpointPath = "router-data"; + private readonly ILnsDiscovery lnsDiscovery; + private readonly ILogger logger; + + internal static readonly IJsonReader QueryReader = + JsonReader.Object( + JsonReader.Property("router", + JsonReader.Either(from n in JsonReader.UInt64() + select new StationEui(n), + from s in JsonReader.String() + select s.Contains(':', StringComparison.Ordinal) + ? Id6.TryParse(s, out var id6) ? new StationEui(id6) : throw new JsonException() + : Hexadecimal.TryParse(s, out ulong hhd, '-') ? new StationEui(hhd) : throw new JsonException()))); + + public DiscoveryService(ILnsDiscovery lnsDiscovery, ILogger logger) + { + this.lnsDiscovery = lnsDiscovery; + this.logger = logger; + } + + public async Task HandleDiscoveryRequestAsync(HttpContext httpContext, CancellationToken cancellationToken) + { + if (httpContext is null) throw new ArgumentNullException(nameof(httpContext)); + + var webSocketConnection = new WebSocketConnection(httpContext, this.logger); + _ = await webSocketConnection.HandleAsync(async (ctx, s, ct) => + { + await using var message = s.ReadTextMessages(cancellationToken); + if (!await message.MoveNextAsync()) + { + this.logger.LogWarning("Did not receive discovery request from station."); + } + else + { + var json = message.Current; + var stationEui = QueryReader.Read(json); + + using var scope = this.logger.BeginEuiScope(stationEui); + this.logger.LogInformation("Received discovery request from: {StationEui}", stationEui); + + try + { + var networkInterface = + NetworkInterface.GetAllNetworkInterfaces() + .SingleOrDefault(ni => ni.GetIPProperties() + .UnicastAddresses + .Any(info => info.Address.Equals(ctx.Connection.LocalIpAddress))); + + var muxs = Id6.Format(networkInterface is { } someNetworkInterface + ? someNetworkInterface.GetPhysicalAddress().Convert48To64() : 0, + Id6.FormatOptions.FixedWidth); + + var lnsUri = await this.lnsDiscovery.ResolveLnsAsync(stationEui, cancellationToken); + + // Ensure resilience against duplicate specification of `router-data` and make sure that LNS host address ends with slash + // to make sure that URI composes as expected. + var lnsUriSanitized = Regex.Replace(lnsUri.AbsoluteUri, @"/router-data/?$", string.Empty, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + lnsUriSanitized = lnsUriSanitized.EndsWith('/') ? lnsUriSanitized : $"{lnsUriSanitized}/"; + + var url = new Uri(new Uri(lnsUriSanitized), $"{DataEndpointPath}/{stationEui}"); + var response = Write(w => WriteResponse(w, stationEui, muxs, url)); + await s.SendAsync(response, WebSocketMessageType.Text, + true, cancellationToken); + } + catch (Exception ex) + { + var response = Write(w => WriteResponse(w, stationEui, ex.Message)); + await s.SendAsync(response, WebSocketMessageType.Text, + true, cancellationToken); + throw; + } + } + }, cancellationToken); + } + + /// + /// Writes the response for Discovery endpoint as a JSON string. + /// + /// The write to use for serialization. + /// The of the querying basic station. + /// The identity of the LNS Data endpoint ( formatted). + /// The URI of the LNS Data endpoint. + internal static void WriteResponse(Utf8JsonWriter writer, StationEui router, string muxs, Uri url) + { + if (writer == null) throw new ArgumentNullException(nameof(writer)); + if (!Id6.TryParse(muxs, out _)) throw new ArgumentException("Argument should be a string in ID6 format.", nameof(muxs)); + if (url is null) throw new ArgumentNullException(nameof(url)); + + writer.WriteStartObject(); + writer.WriteString("router", Id6.Format(router.AsUInt64, Id6.FormatOptions.Lowercase)); + writer.WriteString("muxs", muxs); + writer.WriteString("uri", url.ToString()); + writer.WriteEndObject(); + } + + internal static void WriteResponse(Utf8JsonWriter writer, StationEui router, string error) + { + if (writer == null) throw new ArgumentNullException(nameof(writer)); + + writer.WriteStartObject(); + writer.WriteString("router", Id6.Format(router.AsUInt64, Id6.FormatOptions.Lowercase)); + writer.WriteString("error", error); + writer.WriteEndObject(); + } + + internal static byte[] Write(Action writer) + { + using var ms = new MemoryStream(); + using var jsonWriter = new Utf8JsonWriter(ms); + writer(jsonWriter); + jsonWriter.Flush(); + return ms.ToArray(); + } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/NetworkServerDiscovery/ILnsDiscovery.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/NetworkServerDiscovery/ILnsDiscovery.cs new file mode 100644 index 0000000000..2da72059cc --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/NetworkServerDiscovery/ILnsDiscovery.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaTools.NetworkServerDiscovery +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using LoRaWan; + + public interface ILnsDiscovery + { + public const string EndpointName = "/router-info"; + + Task ResolveLnsAsync(StationEui stationEui, CancellationToken cancellationToken); + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/PhysicalAddressExtensions.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/NetworkServerDiscovery/PhysicalAddressExtensions.cs similarity index 97% rename from LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/PhysicalAddressExtensions.cs rename to LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/NetworkServerDiscovery/PhysicalAddressExtensions.cs index cce9d63550..89a84ad610 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/PhysicalAddressExtensions.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/NetworkServerDiscovery/PhysicalAddressExtensions.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace LoRaWan.NetworkServer +namespace LoRaTools.NetworkServerDiscovery { using System; using System.Buffers.Binary; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/NoopDisposable.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/NoopDisposable.cs similarity index 91% rename from LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/NoopDisposable.cs rename to LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/NoopDisposable.cs index 8b1d11bad0..60ad9dbf16 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/Logger/NoopDisposable.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/NoopDisposable.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace LoRaWan +#nullable enable + +namespace LoRaTools { using System; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Regions/RegionManager.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Regions/RegionManager.cs index cdc5210065..269794edeb 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Regions/RegionManager.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/Regions/RegionManager.cs @@ -47,10 +47,7 @@ public static Region EU868 { get { - if (eu868 == null) - { - eu868 = new RegionEU868(); - } + eu868 ??= new RegionEU868(); return eu868; } @@ -62,10 +59,7 @@ public static Region US915 { get { - if (us915 == null) - { - us915 = new RegionUS915(); - } + us915 ??= new RegionUS915(); return us915; } @@ -77,10 +71,7 @@ public static Region CN470RP1 { get { - if (cn470RP1 == null) - { - cn470RP1 = new RegionCN470RP1(); - } + cn470RP1 ??= new RegionCN470RP1(); return cn470RP1; } @@ -92,10 +83,7 @@ public static Region CN470RP2 { get { - if (cn470RP2 == null) - { - cn470RP2 = new RegionCN470RP2(); - } + cn470RP2 ??= new RegionCN470RP2(); return cn470RP2; } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/TwinPropertiesConstants.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/TwinPropertiesConstants.cs new file mode 100644 index 0000000000..b919979659 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/TwinPropertiesConstants.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaTools +{ + public static class TwinPropertiesConstants + { + public const string GatewayID = "GatewayID"; + public const string ClassType = "ClassType"; + public const string PreferredGatewayID = "PreferredGatewayID"; + public const string DevAddr = "DevAddr"; + public const string NwkSKey = "NwkSKey"; + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/WebSocketConnection.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/WebSocketConnection.cs new file mode 100644 index 0000000000..f8e95b49e8 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/WebSocketConnection.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaTools +{ + using System; + using System.Net.WebSockets; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Http; + using Microsoft.Extensions.Logging; + + public sealed class WebSocketConnection + { + private readonly HttpContext httpContext; + private readonly ILogger? logger; + + public WebSocketConnection(HttpContext httpContext, ILogger? logger) + { + this.httpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); + this.logger = logger; + } + + public async Task HandleAsync(Func handler, CancellationToken cancellationToken) + { + if (handler is null) throw new ArgumentNullException(nameof(handler)); + + if (!this.httpContext.WebSockets.IsWebSocketRequest) + { + this.httpContext.Response.StatusCode = 400; + return this.httpContext; + } + + using var socket = await this.httpContext.WebSockets.AcceptWebSocketAsync(); + this.logger?.LogDebug("WebSocket connection from {RemoteIpAddress} established", this.httpContext.Connection.RemoteIpAddress); + + try + { + await handler(this.httpContext, socket, cancellationToken); + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Goodbye", cancellationToken); + } + catch (OperationCanceledException ex) +#pragma warning disable CA1508 // Avoid dead conditional code (false positive) + when (ex is { InnerException: WebSocketException { WebSocketErrorCode: WebSocketError.ConnectionClosedPrematurely } }) +#pragma warning restore CA1508 // Avoid dead conditional code + { + // Client lost connectivity + this.logger?.LogDebug(ex, "Client lost connectivity: {Exception}", ex.Message); + } + + return this.httpContext; + } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/WebSocketExtensions.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/WebSocketExtensions.cs similarity index 99% rename from LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/WebSocketExtensions.cs rename to LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/WebSocketExtensions.cs index c3be07f251..b8ce24236b 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/WebSocketExtensions.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/WebSocketExtensions.cs @@ -3,7 +3,7 @@ #nullable enable -namespace LoRaWan.NetworkServer +namespace LoRaTools { using System; using System.Buffers; diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/NOTICE.txt b/LoRaEngine/modules/LoRaWanNetworkSrvModule/NOTICE.txt index 142096a1c0..29e2b292a9 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/NOTICE.txt +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/NOTICE.txt @@ -20,7 +20,6 @@ required to debug changes to any libraries licensed under the GNU Lesser General Microsoft.AspNetCore.Hosting 2.1.1 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Apache License @@ -128,7 +127,6 @@ limitations under the License. Microsoft.AspNetCore.Hosting.Abstractions 2.1.1 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Apache License @@ -236,7 +234,6 @@ limitations under the License. Microsoft.AspNetCore.Hosting.Server.Abstractions 2.1.1 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Apache License @@ -344,7 +341,6 @@ limitations under the License. Microsoft.AspNetCore.Http 2.1.22 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Copyright (c) .NET Foundation and Contributors @@ -450,10 +446,9 @@ limitations under the License. --------------------------------------------------------- -Microsoft.AspNetCore.Http.Abstractions 2.1.1 - Apache-2.0 +Microsoft.AspNetCore.Http.Abstractions 2.2.0 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Apache License @@ -561,7 +556,6 @@ limitations under the License. Microsoft.AspNetCore.Http.Extensions 2.1.1 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Apache License @@ -666,10 +660,9 @@ limitations under the License. --------------------------------------------------------- -Microsoft.AspNetCore.Http.Features 2.1.1 - Apache-2.0 +Microsoft.AspNetCore.Http.Features 2.2.0 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Apache License @@ -777,7 +770,6 @@ limitations under the License. Microsoft.AspNetCore.WebUtilities 2.1.1 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Apache License @@ -882,10 +874,9 @@ limitations under the License. --------------------------------------------------------- -Microsoft.Extensions.Configuration.EnvironmentVariables 2.1.1 - Apache-2.0 +Microsoft.Extensions.Configuration 3.1.0 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Apache License @@ -990,10 +981,9 @@ limitations under the License. --------------------------------------------------------- -Microsoft.Extensions.Configuration.FileExtensions 2.1.1 - Apache-2.0 +Microsoft.Extensions.Configuration.Abstractions 3.1.0 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Apache License @@ -1098,10 +1088,9 @@ limitations under the License. --------------------------------------------------------- -Microsoft.Extensions.Configuration.Json 2.1.0 - Apache-2.0 +Microsoft.Extensions.Configuration.EnvironmentVariables 2.1.1 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Apache License @@ -1206,10 +1195,9 @@ limitations under the License. --------------------------------------------------------- -Microsoft.Extensions.FileProviders.Abstractions 2.1.1 - Apache-2.0 +Microsoft.Extensions.Configuration.FileExtensions 3.1.0 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Apache License @@ -1314,10 +1302,9 @@ limitations under the License. --------------------------------------------------------- -Microsoft.Extensions.FileProviders.Physical 2.1.1 - Apache-2.0 +Microsoft.Extensions.Configuration.Json 3.1.0 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Apache License @@ -1422,10 +1409,9 @@ limitations under the License. --------------------------------------------------------- -Microsoft.Extensions.FileSystemGlobbing 2.1.1 - Apache-2.0 +Microsoft.Extensions.FileProviders.Abstractions 3.1.0 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Apache License @@ -1530,10 +1516,116 @@ limitations under the License. --------------------------------------------------------- -Microsoft.Extensions.Hosting.Abstractions 2.1.1 - Apache-2.0 +Microsoft.Extensions.FileProviders.Physical 3.1.0 - Apache-2.0 + + +(c) Microsoft Corporation. + +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + + + "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + + + + "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + + + + "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + + + "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + + + + "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + + + + "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + + + + "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + + + + "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + + + + "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + + + + "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +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 + +http://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. + +--------------------------------------------------------- + +--------------------------------------------------------- + +Microsoft.Extensions.FileSystemGlobbing 3.1.0 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Apache License @@ -1638,10 +1730,9 @@ limitations under the License. --------------------------------------------------------- -Microsoft.Extensions.Http 3.1.9 - Apache-2.0 +Microsoft.Extensions.Hosting.Abstractions 2.1.1 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Apache License @@ -1749,7 +1840,6 @@ limitations under the License. Microsoft.Extensions.ObjectPool 2.1.1 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Apache License @@ -1857,7 +1947,6 @@ limitations under the License. Microsoft.Net.Http.Headers 2.1.1 - Apache-2.0 -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Apache License @@ -2068,10 +2157,89 @@ limitations under the License. --------------------------------------------------------- +Polly 7.2.3 - BSD-3-Clause + + + +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--------------------------------------------------------- + +--------------------------------------------------------- + +Polly.Extensions.Http 3.0.0 - BSD-3-Clause + + +Copyright (c) 2019, App +Copyright (c) 2019, App vNext HttpClient HttpClientFactory Exception Handling Resilience Transient Fault + +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--------------------------------------------------------- + +--------------------------------------------------------- + +Azure.Core 1.25.0 - MIT + + +(c) Microsoft Corporation + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--------------------------------------------------------- + +--------------------------------------------------------- + +Azure.Identity 1.6.1 - MIT + + +(c) Microsoft Corporation + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--------------------------------------------------------- + +--------------------------------------------------------- + DotNetty.Buffers 0.7.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft 2015 - 2021 (c) Microsoft Corporation. Copyright (c) Microsoft Corporation @@ -2093,7 +2261,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI DotNetty.Codecs 0.7.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft 2015 - 2021 (c) Microsoft Corporation. Copyright (c) Microsoft Corporation @@ -2115,7 +2282,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI DotNetty.Codecs.Mqtt 0.7.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft 2015 - 2021 (c) Microsoft Corporation. Copyright (c) Microsoft Corporation @@ -2134,10 +2300,9 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -DotNetty.Common 0.7.0 - MIT +DotNetty.Common 0.7.1 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft 2015 - 2021 (c) Microsoft Corporation. Copyright (c) Microsoft Corporation @@ -2159,7 +2324,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI DotNetty.Handlers 0.7.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft 2015 - 2021 (c) Microsoft Corporation. Copyright (c) Microsoft Corporation @@ -2181,7 +2345,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI DotNetty.Transport 0.7.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft 2015 - 2021 (c) Microsoft Corporation. Copyright (c) Microsoft Corporation @@ -2232,11 +2395,9 @@ SOFTWARE. --------------------------------------------------------- -Microsoft.ApplicationInsights 2.20.0 - MIT +Microsoft.ApplicationInsights 2.21.0 - MIT -(c) 2008 VeriSign, Inc. -(c) Microsoft Corporation. MIT License @@ -2252,11 +2413,9 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -Microsoft.ApplicationInsights.AspNetCore 2.20.0 - MIT +Microsoft.ApplicationInsights.AspNetCore 2.21.0 - MIT -(c) 2008 VeriSign, Inc. -(c) Microsoft Corporation. MIT License @@ -2272,7 +2431,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -Microsoft.ApplicationInsights.DependencyCollector 2.20.0 - MIT +Microsoft.ApplicationInsights.DependencyCollector 2.21.0 - MIT @@ -2290,11 +2449,9 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -Microsoft.ApplicationInsights.EventCounterCollector 2.20.0 - MIT +Microsoft.ApplicationInsights.EventCounterCollector 2.21.0 - MIT -(c) 2008 VeriSign, Inc. -(c) Microsoft Corporation. MIT License @@ -2310,11 +2467,9 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -Microsoft.ApplicationInsights.PerfCounterCollector 2.20.0 - MIT +Microsoft.ApplicationInsights.PerfCounterCollector 2.21.0 - MIT -(c) 2008 VeriSign, Inc. -(c) Microsoft Corporation. MIT License @@ -2330,7 +2485,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -Microsoft.ApplicationInsights.WindowsServer 2.20.0 - MIT +Microsoft.ApplicationInsights.WindowsServer 2.21.0 - MIT @@ -2348,11 +2503,9 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel 2.20.0 - MIT +Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel 2.21.0 - MIT -(c) 2008 VeriSign, Inc. -(c) Microsoft Corporation. MIT License @@ -2368,47 +2521,27 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -Microsoft.Extensions.Caching.Abstractions 6.0.0 - MIT +Microsoft.Azure.Amqp 2.5.12 - MIT + + + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--------------------------------------------------------- + +--------------------------------------------------------- + +Microsoft.Bcl.AsyncInterfaces 1.1.1 - MIT -(c) 2008 VeriSign, Inc. -(c) Microsoft Corporation. -Copyright (c) Andrew Arnott -Copyright 2018 Daniel Lemire -Copyright 2012 the V8 project -Copyright (c) .NET Foundation. -Copyright (c) 2011, Google Inc. -Copyright (c) 1998 Microsoft. To -(c) 1997-2005 Sean Eron Anderson. -Copyright (c) 2017 Yoshifumi Kawai -Copyright (c) Microsoft Corporation -Copyright (c) 2007 James Newton-King -Copyright (c) 2012-2014, Yann Collet -Copyright (c) 2013-2017, Alfred Klomp -Copyright (c) 2015-2017, Wojciech Mula -Copyright (c) 2005-2007, Nick Galbreath -Copyright (c) 2018 Alexander Chermyanin -Portions (c) International Organization -Copyright (c) 2015 The Chromium Authors. -Copyright (c) The Internet Society 1997. -Copyright (c) 2004-2006 Intel Corporation -Copyright (c) 2013-2017, Milosz Krajewski -Copyright (c) 2016-2017, Matthieu Darbois -Copyright (c) .NET Foundation Contributors -Copyright (c) The Internet Society (2003). -Copyright (c) .NET Foundation and Contributors -Copyright (c) 2019 Microsoft Corporation, Daan Leijen -Copyright (c) 2011 Novell, Inc (http://www.novell.com) -Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler -Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) -Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. -Copyright (c) 2014 Ryan Juckett http://www.ryanjuckett.com -Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. -Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip. -Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS -Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the University of California. -Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. -Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. To The MIT License (MIT) @@ -2439,10 +2572,9 @@ SOFTWARE. --------------------------------------------------------- -Microsoft.Extensions.Caching.Memory 6.0.0 - MIT +Microsoft.Extensions.Caching.Abstractions 6.0.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Copyright (c) Andrew Arnott Copyright 2018 Daniel Lemire @@ -2510,46 +2642,49 @@ SOFTWARE. --------------------------------------------------------- -Microsoft.Extensions.Configuration 6.0.0 - MIT +Microsoft.Extensions.Caching.Memory 6.0.1 - MIT -(c) 2008 VeriSign, Inc. -(c) Microsoft Corporation. +(c) Microsoft Corporation Copyright (c) Andrew Arnott Copyright 2018 Daniel Lemire -Copyright 2012 the V8 project -Copyright (c) .NET Foundation. +Copyright (c) .NET Foundation Copyright (c) 2011, Google Inc. +Copyright (c) 2020 Dan Shechter +(c) 1997-2005 Sean Eron Anderson Copyright (c) 1998 Microsoft. To -(c) 1997-2005 Sean Eron Anderson. Copyright (c) 2017 Yoshifumi Kawai +Copyright (c) 2005-2020 Rich Felker Copyright (c) Microsoft Corporation Copyright (c) 2007 James Newton-King Copyright (c) 2012-2014, Yann Collet +Copyright (c) 1991-2020 Unicode, Inc. Copyright (c) 2013-2017, Alfred Klomp +Copyright 2012 the V8 project authors +Copyright (c) 2011-2020 Microsoft Corp Copyright (c) 2015-2017, Wojciech Mula Copyright (c) 2005-2007, Nick Galbreath +Copyright (c) 2015 The Chromium Authors Copyright (c) 2018 Alexander Chermyanin +Copyright (c) The Internet Society 1997 Portions (c) International Organization -Copyright (c) 2015 The Chromium Authors. -Copyright (c) The Internet Society 1997. Copyright (c) 2004-2006 Intel Corporation Copyright (c) 2013-2017, Milosz Krajewski Copyright (c) 2016-2017, Matthieu Darbois +Copyright (c) The Internet Society (2003) Copyright (c) .NET Foundation Contributors -Copyright (c) The Internet Society (2003). Copyright (c) .NET Foundation and Contributors Copyright (c) 2019 Microsoft Corporation, Daan Leijen Copyright (c) 2011 Novell, Inc (http://www.novell.com) Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) -Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors Copyright (c) 2014 Ryan Juckett http://www.ryanjuckett.com Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. -Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip. -Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS -Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the University of California. -Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. +Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers +Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip +Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the University of California +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. To The MIT License (MIT) @@ -2581,10 +2716,9 @@ SOFTWARE. --------------------------------------------------------- -Microsoft.Extensions.Configuration.Abstractions 6.0.0 - MIT +Microsoft.Extensions.DependencyInjection 6.0.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Copyright (c) Andrew Arnott Copyright 2018 Daniel Lemire @@ -2652,10 +2786,9 @@ SOFTWARE. --------------------------------------------------------- -Microsoft.Extensions.Configuration.Binder 6.0.0 - MIT +Microsoft.Extensions.DependencyInjection.Abstractions 6.0.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Copyright (c) Andrew Arnott Copyright 2018 Daniel Lemire @@ -2723,10 +2856,9 @@ SOFTWARE. --------------------------------------------------------- -Microsoft.Extensions.DependencyInjection 6.0.0 - MIT +Microsoft.Extensions.Http 6.0.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Copyright (c) Andrew Arnott Copyright 2018 Daniel Lemire @@ -2794,72 +2926,32 @@ SOFTWARE. --------------------------------------------------------- -Microsoft.Extensions.DependencyInjection.Abstractions 6.0.0 - MIT +Microsoft.Extensions.Http.Polly 6.0.9 - MIT -(c) 2008 VeriSign, Inc. -(c) Microsoft Corporation. +(c) Microsoft Corporation Copyright (c) Andrew Arnott -Copyright 2018 Daniel Lemire -Copyright 2012 the V8 project -Copyright (c) .NET Foundation. -Copyright (c) 2011, Google Inc. -Copyright (c) 1998 Microsoft. To -(c) 1997-2005 Sean Eron Anderson. +Copyright (c) 2019 David Fowler +Copyright (c) 2016 Richard Morris Copyright (c) 2017 Yoshifumi Kawai Copyright (c) Microsoft Corporation Copyright (c) 2007 James Newton-King -Copyright (c) 2012-2014, Yann Collet -Copyright (c) 2013-2017, Alfred Klomp -Copyright (c) 2015-2017, Wojciech Mula -Copyright (c) 2005-2007, Nick Galbreath -Copyright (c) 2018 Alexander Chermyanin -Portions (c) International Organization -Copyright (c) 2015 The Chromium Authors. -Copyright (c) The Internet Society 1997. -Copyright (c) 2004-2006 Intel Corporation +Copyright (c) 2014-2018 Michael Daines Copyright (c) 2013-2017, Milosz Krajewski -Copyright (c) 2016-2017, Matthieu Darbois -Copyright (c) .NET Foundation Contributors -Copyright (c) The Internet Society (2003). -Copyright (c) .NET Foundation and Contributors -Copyright (c) 2019 Microsoft Corporation, Daan Leijen -Copyright (c) 2011 Novell, Inc (http://www.novell.com) -Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler -Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) -Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. -Copyright (c) 2014 Ryan Juckett http://www.ryanjuckett.com -Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. -Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip. -Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS -Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the University of California. -Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. -Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. To - -The MIT License (MIT) - Copyright (c) .NET Foundation and Contributors +Copyright (c) 2019-2020 West Wind Technologies +Copyright (c) 2010-2019 Google LLC. http://angular.io/license +Copyright (c) Sindre Sorhus (https://sindresorhus.com) -All rights reserved. +MIT License -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Copyright (c) -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- @@ -2868,7 +2960,6 @@ SOFTWARE. Microsoft.Extensions.Logging 6.0.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Copyright (c) Andrew Arnott Copyright 2018 Daniel Lemire @@ -2939,7 +3030,6 @@ SOFTWARE. Microsoft.Extensions.Logging.Abstractions 6.0.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Copyright (c) Andrew Arnott Copyright 2018 Daniel Lemire @@ -3007,7 +3097,7 @@ SOFTWARE. --------------------------------------------------------- -Microsoft.Extensions.Logging.ApplicationInsights 2.20.0 - MIT +Microsoft.Extensions.Logging.ApplicationInsights 2.21.0 - MIT @@ -3025,10 +3115,9 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -Microsoft.Extensions.Logging.Configuration 6.0.0 - MIT +Microsoft.Extensions.Options 6.0.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Copyright (c) Andrew Arnott Copyright 2018 Daniel Lemire @@ -3096,10 +3185,9 @@ SOFTWARE. --------------------------------------------------------- -Microsoft.Extensions.Options 6.0.0 - MIT +Microsoft.Extensions.Primitives 6.0.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Copyright (c) Andrew Arnott Copyright 2018 Daniel Lemire @@ -3167,81 +3255,68 @@ SOFTWARE. --------------------------------------------------------- -Microsoft.Extensions.Options.ConfigurationExtensions 6.0.0 - MIT +Microsoft.Identity.Client 4.39.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. -Copyright (c) Andrew Arnott -Copyright 2018 Daniel Lemire -Copyright 2012 the V8 project -Copyright (c) .NET Foundation. -Copyright (c) 2011, Google Inc. -Copyright (c) 1998 Microsoft. To -(c) 1997-2005 Sean Eron Anderson. -Copyright (c) 2017 Yoshifumi Kawai -Copyright (c) Microsoft Corporation -Copyright (c) 2007 James Newton-King -Copyright (c) 2012-2014, Yann Collet -Copyright (c) 2013-2017, Alfred Klomp -Copyright (c) 2015-2017, Wojciech Mula -Copyright (c) 2005-2007, Nick Galbreath -Copyright (c) 2018 Alexander Chermyanin -Portions (c) International Organization -Copyright (c) 2015 The Chromium Authors. -Copyright (c) The Internet Society 1997. -Copyright (c) 2004-2006 Intel Corporation -Copyright (c) 2013-2017, Milosz Krajewski -Copyright (c) 2016-2017, Matthieu Darbois -Copyright (c) .NET Foundation Contributors -Copyright (c) The Internet Society (2003). -Copyright (c) .NET Foundation and Contributors -Copyright (c) 2019 Microsoft Corporation, Daan Leijen -Copyright (c) 2011 Novell, Inc (http://www.novell.com) -Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler -Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) -Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. -Copyright (c) 2014 Ryan Juckett http://www.ryanjuckett.com -Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. -Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip. -Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS -Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the University of California. -Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. -Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. To -The MIT License (MIT) +MIT License -Copyright (c) .NET Foundation and Contributors +Copyright (c) -All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- +Microsoft.Identity.Client.Extensions.Msal 2.19.3 - MIT + + +(c) Microsoft Corporation. + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- -Microsoft.Extensions.Primitives 6.0.0 - MIT +Microsoft.Rest.ClientRuntime 2.3.21 - MIT + + +(c) Microsoft Corporation. +Copyright (c) 2019 Microsoft +Copyright (c) Microsoft Corporation + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--------------------------------------------------------- + +--------------------------------------------------------- + +Microsoft.Win32.SystemEvents 5.0.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Copyright (c) Andrew Arnott Copyright 2018 Daniel Lemire @@ -3267,7 +3342,6 @@ Copyright (c) 2016-2017, Matthieu Darbois Copyright (c) .NET Foundation Contributors Copyright (c) The Internet Society (2003). Copyright (c) .NET Foundation and Contributors -Copyright (c) 2019 Microsoft Corporation, Daan Leijen Copyright (c) 2011 Novell, Inc (http://www.novell.com) Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) @@ -3309,67 +3383,13 @@ SOFTWARE. --------------------------------------------------------- -Microsoft.Win32.SystemEvents 4.7.0 - MIT - - -(c) 2008 VeriSign, Inc. -(c) Microsoft Corporation. -Copyright (c) .NET Foundation. -Copyright (c) 2011, Google Inc. -(c) 1997-2005 Sean Eron Anderson. -Copyright (c) 2007 James Newton-King -Copyright (c) 1991-2017 Unicode, Inc. -Copyright (c) 2013-2017, Alfred Klomp -Copyright (c) 2015-2017, Wojciech Mula -Copyright (c) 2005-2007, Nick Galbreath -Portions (c) International Organization -Copyright (c) 2015 The Chromium Authors. -Copyright (c) 2004-2006 Intel Corporation -Copyright (c) 2016-2017, Matthieu Darbois -Copyright (c) .NET Foundation Contributors -Copyright (c) .NET Foundation and Contributors -Copyright (c) 2011 Novell, Inc (http://www.novell.com) -Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler -Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) -Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. -Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS - -The MIT License (MIT) - -Copyright (c) .NET Foundation and Contributors - -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -Newtonsoft.Json 12.0.3 - MIT +Newtonsoft.Json 13.0.1 - MIT -(c) 2008 VeriSign, Inc. Copyright James Newton-King 2008 Copyright (c) 2007 James Newton-King Copyright (c) James Newton-King 2008 +Copyright James Newton-King 2008 Json.NET The MIT License (MIT) @@ -3397,45 +3417,28 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -Newtonsoft.Json 13.0.1 - MIT - +Pipelines.Sockets.Unofficial 2.2.2 - MIT -(c) 2008 VeriSign, Inc. -Copyright James Newton-King 2008 -Copyright (c) 2007 James Newton-King -Copyright (c) James Newton-King 2008 -The MIT License (MIT) -Copyright (c) 2007 James Newton-King +MIT License -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: +Copyright (c) -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- -Nullable 1.3.0 - MIT +Portable.BouncyCastle 1.9.0 - MIT -(c) 2008 VeriSign, Inc. -Copyright (c) Manuel Romer +Copyright (c) 2000 - 2017 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org) MIT License @@ -3451,11 +3454,9 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -Portable.BouncyCastle 1.9.0 - MIT +prometheus-net 6.0.0 - MIT -(c) 2008 VeriSign, Inc. -Copyright (c) 2000 - 2017 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org) MIT License @@ -3471,10 +3472,9 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -prometheus-net 5.0.2 - MIT +prometheus-net.AspNetCore 6.0.0 - MIT -(c) 2008 VeriSign, Inc. MIT License @@ -3490,10 +3490,10 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -prometheus-net.AspNetCore 5.0.2 - MIT +StackExchange.Redis 2.6.66 - MIT -(c) 2008 VeriSign, Inc. +Copyright (c) .NET Foundation MIT License @@ -3542,30 +3542,45 @@ SOFTWARE. --------------------------------------------------------- -System.Configuration.ConfigurationManager 4.7.0 - MIT +System.Configuration.ConfigurationManager 5.0.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. +Copyright (c) Andrew Arnott +Copyright 2018 Daniel Lemire +Copyright 2012 the V8 project Copyright (c) .NET Foundation. Copyright (c) 2011, Google Inc. +Copyright (c) 1998 Microsoft. To (c) 1997-2005 Sean Eron Anderson. +Copyright (c) 2017 Yoshifumi Kawai +Copyright (c) Microsoft Corporation Copyright (c) 2007 James Newton-King -Copyright (c) 1991-2017 Unicode, Inc. +Copyright (c) 2012-2014, Yann Collet Copyright (c) 2013-2017, Alfred Klomp Copyright (c) 2015-2017, Wojciech Mula Copyright (c) 2005-2007, Nick Galbreath +Copyright (c) 2018 Alexander Chermyanin Portions (c) International Organization Copyright (c) 2015 The Chromium Authors. +Copyright (c) The Internet Society 1997. Copyright (c) 2004-2006 Intel Corporation +Copyright (c) 2013-2017, Milosz Krajewski Copyright (c) 2016-2017, Matthieu Darbois Copyright (c) .NET Foundation Contributors +Copyright (c) The Internet Society (2003). Copyright (c) .NET Foundation and Contributors Copyright (c) 2011 Novell, Inc (http://www.novell.com) Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. +Copyright (c) 2014 Ryan Juckett http://www.ryanjuckett.com +Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. +Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip. Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS +Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the University of California. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. To The MIT License (MIT) @@ -3596,30 +3611,45 @@ SOFTWARE. --------------------------------------------------------- -System.Diagnostics.PerformanceCounter 4.7.0 - MIT +System.Diagnostics.PerformanceCounter 5.0.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. +Copyright (c) Andrew Arnott +Copyright 2018 Daniel Lemire +Copyright 2012 the V8 project Copyright (c) .NET Foundation. Copyright (c) 2011, Google Inc. +Copyright (c) 1998 Microsoft. To (c) 1997-2005 Sean Eron Anderson. +Copyright (c) 2017 Yoshifumi Kawai +Copyright (c) Microsoft Corporation Copyright (c) 2007 James Newton-King -Copyright (c) 1991-2017 Unicode, Inc. +Copyright (c) 2012-2014, Yann Collet Copyright (c) 2013-2017, Alfred Klomp Copyright (c) 2015-2017, Wojciech Mula Copyright (c) 2005-2007, Nick Galbreath +Copyright (c) 2018 Alexander Chermyanin Portions (c) International Organization Copyright (c) 2015 The Chromium Authors. +Copyright (c) The Internet Society 1997. Copyright (c) 2004-2006 Intel Corporation +Copyright (c) 2013-2017, Milosz Krajewski Copyright (c) 2016-2017, Matthieu Darbois Copyright (c) .NET Foundation Contributors +Copyright (c) The Internet Society (2003). Copyright (c) .NET Foundation and Contributors Copyright (c) 2011 Novell, Inc (http://www.novell.com) Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. +Copyright (c) 2014 Ryan Juckett http://www.ryanjuckett.com +Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. +Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip. Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS +Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the University of California. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. To The MIT License (MIT) @@ -3650,30 +3680,45 @@ SOFTWARE. --------------------------------------------------------- -System.Drawing.Common 4.7.0 - MIT +System.Drawing.Common 5.0.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. +Copyright (c) Andrew Arnott +Copyright 2018 Daniel Lemire +Copyright 2012 the V8 project Copyright (c) .NET Foundation. Copyright (c) 2011, Google Inc. +Copyright (c) 1998 Microsoft. To (c) 1997-2005 Sean Eron Anderson. +Copyright (c) 2017 Yoshifumi Kawai +Copyright (c) Microsoft Corporation Copyright (c) 2007 James Newton-King -Copyright (c) 1991-2017 Unicode, Inc. +Copyright (c) 2012-2014, Yann Collet Copyright (c) 2013-2017, Alfred Klomp Copyright (c) 2015-2017, Wojciech Mula Copyright (c) 2005-2007, Nick Galbreath +Copyright (c) 2018 Alexander Chermyanin Portions (c) International Organization Copyright (c) 2015 The Chromium Authors. +Copyright (c) The Internet Society 1997. Copyright (c) 2004-2006 Intel Corporation +Copyright (c) 2013-2017, Milosz Krajewski Copyright (c) 2016-2017, Matthieu Darbois Copyright (c) .NET Foundation Contributors +Copyright (c) The Internet Society (2003). Copyright (c) .NET Foundation and Contributors Copyright (c) 2011 Novell, Inc (http://www.novell.com) Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. +Copyright (c) 2014 Ryan Juckett http://www.ryanjuckett.com +Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. +Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip. Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS +Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the University of California. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. To The MIT License (MIT) @@ -3707,7 +3752,6 @@ SOFTWARE. System.IO.FileSystem.AccessControl 4.7.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Copyright (c) .NET Foundation. Copyright (c) 2011, Google Inc. @@ -3754,6 +3798,141 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +System.IO.Pipelines 5.0.1 - MIT + + +(c) Microsoft Corporation. +Copyright (c) Andrew Arnott +Copyright 2018 Daniel Lemire +Copyright 2012 the V8 project +Copyright (c) .NET Foundation. +Copyright (c) 2011, Google Inc. +Copyright (c) 1998 Microsoft. To +(c) 1997-2005 Sean Eron Anderson. +Copyright (c) 2017 Yoshifumi Kawai +Copyright (c) Microsoft Corporation +Copyright (c) 2007 James Newton-King +Copyright (c) 2012-2014, Yann Collet +Copyright (c) 2013-2017, Alfred Klomp +Copyright (c) 2015-2017, Wojciech Mula +Copyright (c) 2005-2007, Nick Galbreath +Copyright (c) 2018 Alexander Chermyanin +Portions (c) International Organization +Copyright (c) 2015 The Chromium Authors. +Copyright (c) The Internet Society 1997. +Copyright (c) 2004-2006 Intel Corporation +Copyright (c) 2013-2017, Milosz Krajewski +Copyright (c) 2016-2017, Matthieu Darbois +Copyright (c) .NET Foundation Contributors +Copyright (c) The Internet Society (2003). +Copyright (c) .NET Foundation and Contributors +Copyright (c) 2011 Novell, Inc (http://www.novell.com) +Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler +Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. +Copyright (c) 2014 Ryan Juckett http://www.ryanjuckett.com +Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. +Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip. +Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS +Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the University of California. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. To + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +System.Memory 4.5.4 - MIT + + +(c) Microsoft Corporation. +Copyright (c) 2011, Google Inc. +(c) 1997-2005 Sean Eron Anderson. +Copyright (c) 1991-2017 Unicode, Inc. +Portions (c) International Organization +Copyright (c) 2015 The Chromium Authors. +Copyright (c) 2004-2006 Intel Corporation +Copyright (c) .NET Foundation Contributors +Copyright (c) .NET Foundation and Contributors +Copyright (c) 2011 Novell, Inc (http://www.novell.com) +Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler +Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. +Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +System.Memory.Data 1.0.2 - MIT + + +(c) Microsoft Corporation. + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + --------------------------------------------------------- --------------------------------------------------------- @@ -3761,7 +3940,6 @@ SOFTWARE. System.Runtime.CompilerServices.Unsafe 6.0.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Copyright (c) Andrew Arnott Copyright 2018 Daniel Lemire @@ -3829,30 +4007,45 @@ SOFTWARE. --------------------------------------------------------- -System.Security.AccessControl 4.7.0 - MIT +System.Security.AccessControl 5.0.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. +Copyright (c) Andrew Arnott +Copyright 2018 Daniel Lemire +Copyright 2012 the V8 project Copyright (c) .NET Foundation. Copyright (c) 2011, Google Inc. +Copyright (c) 1998 Microsoft. To (c) 1997-2005 Sean Eron Anderson. +Copyright (c) 2017 Yoshifumi Kawai +Copyright (c) Microsoft Corporation Copyright (c) 2007 James Newton-King -Copyright (c) 1991-2017 Unicode, Inc. +Copyright (c) 2012-2014, Yann Collet Copyright (c) 2013-2017, Alfred Klomp Copyright (c) 2015-2017, Wojciech Mula Copyright (c) 2005-2007, Nick Galbreath +Copyright (c) 2018 Alexander Chermyanin Portions (c) International Organization Copyright (c) 2015 The Chromium Authors. +Copyright (c) The Internet Society 1997. Copyright (c) 2004-2006 Intel Corporation +Copyright (c) 2013-2017, Milosz Krajewski Copyright (c) 2016-2017, Matthieu Darbois Copyright (c) .NET Foundation Contributors +Copyright (c) The Internet Society (2003). Copyright (c) .NET Foundation and Contributors Copyright (c) 2011 Novell, Inc (http://www.novell.com) Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. +Copyright (c) 2014 Ryan Juckett http://www.ryanjuckett.com +Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. +Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip. Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS +Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the University of California. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. To The MIT License (MIT) @@ -3883,20 +4076,28 @@ SOFTWARE. --------------------------------------------------------- -System.Security.Cryptography.ProtectedData 4.4.0 - MIT +System.Security.Cryptography.ProtectedData 4.7.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. +Copyright (c) .NET Foundation. +Copyright (c) 2011, Google Inc. (c) 1997-2005 Sean Eron Anderson. +Copyright (c) 2007 James Newton-King Copyright (c) 1991-2017 Unicode, Inc. +Copyright (c) 2013-2017, Alfred Klomp +Copyright (c) 2015-2017, Wojciech Mula +Copyright (c) 2005-2007, Nick Galbreath Portions (c) International Organization +Copyright (c) 2015 The Chromium Authors. Copyright (c) 2004-2006 Intel Corporation +Copyright (c) 2016-2017, Matthieu Darbois Copyright (c) .NET Foundation Contributors Copyright (c) .NET Foundation and Contributors Copyright (c) 2011 Novell, Inc (http://www.novell.com) Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS The MIT License (MIT) @@ -3928,30 +4129,45 @@ SOFTWARE. --------------------------------------------------------- -System.Security.Cryptography.ProtectedData 4.7.0 - MIT +System.Security.Cryptography.ProtectedData 5.0.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. +Copyright (c) Andrew Arnott +Copyright 2018 Daniel Lemire +Copyright 2012 the V8 project Copyright (c) .NET Foundation. Copyright (c) 2011, Google Inc. +Copyright (c) 1998 Microsoft. To (c) 1997-2005 Sean Eron Anderson. +Copyright (c) 2017 Yoshifumi Kawai +Copyright (c) Microsoft Corporation Copyright (c) 2007 James Newton-King -Copyright (c) 1991-2017 Unicode, Inc. +Copyright (c) 2012-2014, Yann Collet Copyright (c) 2013-2017, Alfred Klomp Copyright (c) 2015-2017, Wojciech Mula Copyright (c) 2005-2007, Nick Galbreath +Copyright (c) 2018 Alexander Chermyanin Portions (c) International Organization Copyright (c) 2015 The Chromium Authors. +Copyright (c) The Internet Society 1997. Copyright (c) 2004-2006 Intel Corporation +Copyright (c) 2013-2017, Milosz Krajewski Copyright (c) 2016-2017, Matthieu Darbois Copyright (c) .NET Foundation Contributors +Copyright (c) The Internet Society (2003). Copyright (c) .NET Foundation and Contributors Copyright (c) 2011 Novell, Inc (http://www.novell.com) Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. +Copyright (c) 2014 Ryan Juckett http://www.ryanjuckett.com +Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. +Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip. Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS +Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the University of California. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. To The MIT License (MIT) @@ -3982,30 +4198,45 @@ SOFTWARE. --------------------------------------------------------- -System.Security.Permissions 4.7.0 - MIT +System.Security.Permissions 5.0.0 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. +Copyright (c) Andrew Arnott +Copyright 2018 Daniel Lemire +Copyright 2012 the V8 project Copyright (c) .NET Foundation. Copyright (c) 2011, Google Inc. +Copyright (c) 1998 Microsoft. To (c) 1997-2005 Sean Eron Anderson. +Copyright (c) 2017 Yoshifumi Kawai +Copyright (c) Microsoft Corporation Copyright (c) 2007 James Newton-King -Copyright (c) 1991-2017 Unicode, Inc. +Copyright (c) 2012-2014, Yann Collet Copyright (c) 2013-2017, Alfred Klomp Copyright (c) 2015-2017, Wojciech Mula Copyright (c) 2005-2007, Nick Galbreath +Copyright (c) 2018 Alexander Chermyanin Portions (c) International Organization Copyright (c) 2015 The Chromium Authors. +Copyright (c) The Internet Society 1997. Copyright (c) 2004-2006 Intel Corporation +Copyright (c) 2013-2017, Milosz Krajewski Copyright (c) 2016-2017, Matthieu Darbois Copyright (c) .NET Foundation Contributors +Copyright (c) The Internet Society (2003). Copyright (c) .NET Foundation and Contributors Copyright (c) 2011 Novell, Inc (http://www.novell.com) Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. +Copyright (c) 2014 Ryan Juckett http://www.ryanjuckett.com +Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. +Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip. Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS +Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the University of California. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. To The MIT License (MIT) @@ -4036,17 +4267,22 @@ SOFTWARE. --------------------------------------------------------- -System.Text.Encodings.Web 4.5.1 - MIT +System.Text.Encodings.Web 4.7.2 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. +Copyright (c) .NET Foundation. Copyright (c) 2011, Google Inc. (c) 1997-2005 Sean Eron Anderson. +Copyright (c) 2007 James Newton-King Copyright (c) 1991-2017 Unicode, Inc. +Copyright (c) 2013-2017, Alfred Klomp +Copyright (c) 2015-2017, Wojciech Mula +Copyright (c) 2005-2007, Nick Galbreath Portions (c) International Organization Copyright (c) 2015 The Chromium Authors. Copyright (c) 2004-2006 Intel Corporation +Copyright (c) 2016-2017, Matthieu Darbois Copyright (c) .NET Foundation Contributors Copyright (c) .NET Foundation and Contributors Copyright (c) 2011 Novell, Inc (http://www.novell.com) @@ -4084,10 +4320,9 @@ SOFTWARE. --------------------------------------------------------- -System.Windows.Extensions 4.7.0 - MIT +System.Text.Json 4.7.2 - MIT -(c) 2008 VeriSign, Inc. (c) Microsoft Corporation. Copyright (c) .NET Foundation. Copyright (c) 2011, Google Inc. @@ -4136,3 +4371,72 @@ SOFTWARE. --------------------------------------------------------- +--------------------------------------------------------- + +System.Windows.Extensions 5.0.0 - MIT + + +(c) Microsoft Corporation. +Copyright (c) Andrew Arnott +Copyright 2018 Daniel Lemire +Copyright 2012 the V8 project +Copyright (c) .NET Foundation. +Copyright (c) 2011, Google Inc. +Copyright (c) 1998 Microsoft. To +(c) 1997-2005 Sean Eron Anderson. +Copyright (c) 2017 Yoshifumi Kawai +Copyright (c) Microsoft Corporation +Copyright (c) 2007 James Newton-King +Copyright (c) 2012-2014, Yann Collet +Copyright (c) 2013-2017, Alfred Klomp +Copyright (c) 2015-2017, Wojciech Mula +Copyright (c) 2005-2007, Nick Galbreath +Copyright (c) 2018 Alexander Chermyanin +Portions (c) International Organization +Copyright (c) 2015 The Chromium Authors. +Copyright (c) The Internet Society 1997. +Copyright (c) 2004-2006 Intel Corporation +Copyright (c) 2013-2017, Milosz Krajewski +Copyright (c) 2016-2017, Matthieu Darbois +Copyright (c) .NET Foundation Contributors +Copyright (c) The Internet Society (2003). +Copyright (c) .NET Foundation and Contributors +Copyright (c) 2011 Novell, Inc (http://www.novell.com) +Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler +Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. +Copyright (c) 2014 Ryan Juckett http://www.ryanjuckett.com +Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. +Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip. +Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS +Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the University of California. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass. To + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--------------------------------------------------------- + diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/cgmanifest.json b/LoRaEngine/modules/LoRaWanNetworkSrvModule/cgmanifest.json index 1a0754b5dc..336b351a1d 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/cgmanifest.json +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/cgmanifest.json @@ -1,13 +1,15 @@ { - "Registrations": [ + "$schema": "https://json.schemastore.org/component-detection-manifest.json", + "registrations": [ { - "Component": { - "Type": "git", - "git": { - "RepositoryUrl": "https://github.com/jieter/python-lora", - "CommitHash": "00bc45c6d6ba366a0bc09ba287b58f7bc9bc27e0" + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/jieter/python-lora", + "commitHash": "00bc45c6d6ba366a0bc09ba287b58f7bc9bc27e0" + } } } - } - ] -} + ], + "version": 1 +} \ No newline at end of file diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/module.json b/LoRaEngine/modules/LoRaWanNetworkSrvModule/module.json index 1a268aefd0..937340ca7f 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/module.json +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/module.json @@ -8,7 +8,8 @@ "platforms": { "amd64": "./Dockerfile.amd64", "amd64.debug": "./Dockerfile.amd64.debug", - "arm32v7": "./Dockerfile.arm32v7" + "arm32v7": "./Dockerfile.arm32v7", + "arm64v8": "./Dockerfile.arm64v8" } }, "buildOptions": [], diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..869fdfe2b2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). + + diff --git a/Samples/CayenneSample/CayenneDecoder/CayenneDecoderModule.csproj b/Samples/CayenneSample/CayenneDecoder/CayenneDecoderModule.csproj deleted file mode 100644 index 57197f7f36..0000000000 --- a/Samples/CayenneSample/CayenneDecoder/CayenneDecoderModule.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - netcoreapp2.1 - CayenneDecoderModule - CayenneDecoderModule - - - - - - - - - - - - - - - - diff --git a/Samples/CayenneSample/CayenneDecoder/CayenneDecoderModule.sln b/Samples/CayenneSample/CayenneDecoder/CayenneDecoderModule.sln deleted file mode 100644 index f99d60a01b..0000000000 --- a/Samples/CayenneSample/CayenneDecoder/CayenneDecoderModule.sln +++ /dev/null @@ -1,30 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31727.386 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CayenneDecoderModule", "CayenneDecoderModule.csproj", "{067DCA55-90EA-47C0-ABFA-B9CB32603992}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CayenneDecoderTest", "..\CayenneDecoderTest\CayenneDecoderTest.csproj", "{BFF1E6B6-0569-4C66-B7A4-1C73DCD20BBF}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {067DCA55-90EA-47C0-ABFA-B9CB32603992}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {067DCA55-90EA-47C0-ABFA-B9CB32603992}.Debug|Any CPU.Build.0 = Debug|Any CPU - {067DCA55-90EA-47C0-ABFA-B9CB32603992}.Release|Any CPU.ActiveCfg = Release|Any CPU - {067DCA55-90EA-47C0-ABFA-B9CB32603992}.Release|Any CPU.Build.0 = Release|Any CPU - {BFF1E6B6-0569-4C66-B7A4-1C73DCD20BBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BFF1E6B6-0569-4C66-B7A4-1C73DCD20BBF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BFF1E6B6-0569-4C66-B7A4-1C73DCD20BBF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BFF1E6B6-0569-4C66-B7A4-1C73DCD20BBF}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {0363BFE0-78B2-4322-BC4F-5251526C9B06} - EndGlobalSection -EndGlobal diff --git a/Samples/CayenneSample/CayenneDecoder/Classes/CayenneDecoder.cs b/Samples/CayenneSample/CayenneDecoder/Classes/CayenneDecoder.cs deleted file mode 100644 index bea88733f9..0000000000 --- a/Samples/CayenneSample/CayenneDecoder/Classes/CayenneDecoder.cs +++ /dev/null @@ -1,129 +0,0 @@ -namespace CayenneDecoderModule.Classes -{ - using System; - - public class CayenneDecoder - { - readonly byte[] buffer; - public CayenneDevice CayenneDevice { get; internal set; } - - public CayenneDecoder(byte[] payload) - { - this.buffer = payload; - CayenneDevice = new CayenneDevice(); - ExtractAllDevices(); - } - - private void ExtractAllDevices() - { - var cursor = 0; - try - { - foreach (var cayenneTypes in Enum.GetValues(typeof(CayenneTypes))) - { - while (cursor != this.buffer.Length) - { - var channel = this.buffer[cursor++]; - var type = this.buffer[cursor++]; - var size = (byte)Enum.Parse(Enum.GetName(typeof(CayenneTypes), type)); - - switch ((CayenneTypes)type) - { - case CayenneTypes.DigitalInput: - var digitalInput = new DigitalInput() { Channel = channel, Value = this.buffer[cursor] }; - CayenneDevice.DigitalInput.Add(digitalInput); - break; - case CayenneTypes.DigitalOutput: - var digitalOutput = new DigitalOutput() { Channel = channel, Value = this.buffer[cursor] }; - CayenneDevice.DigitaOutput.Add(digitalOutput); - break; - case CayenneTypes.AnalogInput: - var analogInput = new AnalogInput() { Channel = channel, Value = ((short)(this.buffer[cursor] << 8) + this.buffer[cursor + 1]) / 100.0 }; - CayenneDevice.AnalogInput.Add(analogInput); - break; - case CayenneTypes.AnalogOutput: - var analogOutput = new AnalogOutput() { Channel = channel, Value = ((short)(this.buffer[cursor] << 8) + this.buffer[cursor + 1]) / 100.0 }; - CayenneDevice.AnalogOutput.Add(analogOutput); - break; - case CayenneTypes.Luminosity: - var illuminanceSensor = new IlluminanceSensor() { Channel = channel, Value = (ushort)((this.buffer[cursor] << 8) + this.buffer[cursor + 1]) }; - CayenneDevice.IlluminanceSensor.Add(illuminanceSensor); - break; - case CayenneTypes.Presence: - var presenceSensor = new PresenceSensor() { Channel = channel, Value = this.buffer[cursor] }; - CayenneDevice.PresenceSensor.Add(presenceSensor); - break; - case CayenneTypes.Temperature: - var temperatureSensor = new TemperatureSensor() { Channel = channel, Value = ((short)(this.buffer[cursor] << 8) + this.buffer[cursor + 1]) / 10.0 }; - CayenneDevice.TemperatureSensor.Add(temperatureSensor); - break; - case CayenneTypes.RelativeHumidity: - var humiditySensor = new HumiditySensor() { Channel = channel, Value = this.buffer[cursor] / 2.0 }; - CayenneDevice.HumiditySensor.Add(humiditySensor); - break; - case CayenneTypes.Accelerator: - var accelerator = new Accelerator() - { - Channel = channel, - X = ((short)(this.buffer[cursor] << 8) + this.buffer[cursor + 1]) / 1000.0, - Y = ((short)(this.buffer[cursor + 2] << 8) + this.buffer[cursor + 3]) / 1000.0, - Z = ((short)(this.buffer[cursor + 4] << 8) + this.buffer[cursor + 5]) / 1000.0 - }; - CayenneDevice.Accelerator.Add(accelerator); - break; - case CayenneTypes.Barometer: - var barometer = new Barometer() { Channel = channel, Value = ((this.buffer[cursor] << 8) + this.buffer[cursor + 1]) / 10.0 }; - CayenneDevice.Barometer.Add(barometer); - break; - case CayenneTypes.Gyrometer: - var gyrometer = new Gyrometer() - { - Channel = channel, - X = ((short)(this.buffer[cursor] << 8) + this.buffer[cursor + 1]) / 100.0, - Y = ((short)(this.buffer[cursor + 2] << 8) + this.buffer[cursor + 3]) / 100.0, - Z = ((short)(this.buffer[cursor + 4] << 8) + this.buffer[cursor + 5]) / 100.0 - }; - CayenneDevice.Gyrometer.Add(gyrometer); - break; - case CayenneTypes.Gps: - var gPSLocation = new GPSLocation - { - Channel = channel - }; - var sign = 1.0; - if ((this.buffer[cursor] & 0x80) == 0x80) - { - this.buffer[cursor] = (byte)(this.buffer[cursor] & 0x7F); - sign = -1.0; - } - gPSLocation.Latitude = sign * ((this.buffer[cursor] << 16) + (this.buffer[cursor + 1] << 8) + this.buffer[cursor + 2]) / 10000.0; - sign = 1.0; - if ((this.buffer[cursor + 3] & 0x80) == 0x80) - { - this.buffer[cursor + 3] = (byte)(this.buffer[cursor + 3] & 0x7F); - sign = -1.0; - } - gPSLocation.Longitude = sign * ((this.buffer[cursor + 3] << 16) + (this.buffer[cursor + 4] << 8) + this.buffer[cursor + 5]) / 10000.0; - sign = 1.0; - if ((this.buffer[cursor + 6] & 0x80) == 0x80) - { - this.buffer[cursor + 6] = (byte)(this.buffer[cursor + 6] & 0x7F); - sign = -1.0; - } - gPSLocation.Altitude = sign * ((this.buffer[cursor + 6] << 16) + (this.buffer[cursor + 7] << 8) + this.buffer[cursor + 8]) / 100.0; - CayenneDevice.GPSLocation.Add(gPSLocation); - break; - default: - break; - } - cursor += size - 2; - } - } - } - catch (Exception) - { } - } - } - - -} diff --git a/Samples/CayenneSample/CayenneDecoder/Classes/CayenneDevice.cs b/Samples/CayenneSample/CayenneDecoder/Classes/CayenneDevice.cs deleted file mode 100644 index 52951678eb..0000000000 --- a/Samples/CayenneSample/CayenneDecoder/Classes/CayenneDevice.cs +++ /dev/null @@ -1,112 +0,0 @@ -namespace CayenneDecoderModule.Classes -{ - using Newtonsoft.Json; - using System; - using System.Collections.Generic; - - public class DigitalInput - { - public byte Channel { get; set; } - public byte Value { get; set; } - } - - public class DigitalOutput - { - public byte Channel { get; set; } - public byte Value { get; set; } - } - - public class AnalogInput - { - public byte Channel { get; set; } - public double Value { get; set; } - } - - public class AnalogOutput - { - public byte Channel { get; set; } - public double Value { get; set; } - } - - public class IlluminanceSensor - { - public byte Channel { get; set; } - public UInt16 Value { get; set; } - } - - public class PresenceSensor - { - public byte Channel { get; set; } - public byte Value { get; set; } - } - - public class TemperatureSensor - { - public byte Channel { get; set; } - public double Value { get; set; } - } - - public class HumiditySensor - { - public byte Channel { get; set; } - public double Value { get; set; } - } - - public class Accelerator - { - public byte Channel { get; set; } - public double X { get; set; } - public double Y { get; set; } - public double Z { get; set; } - } - - public class Barometer - { - public byte Channel { get; set; } - public double Value { get; set; } - } - - public class Gyrometer - { - public byte Channel { get; set; } - public double X { get; set; } - public double Y { get; set; } - public double Z { get; set; } - } - - public class GPSLocation - { - public byte Channel { get; set; } - public double Latitude { get; set; } - public double Longitude { get; set; } - public double Altitude { get; set; } - } - - public class CayenneDevice - { - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public IList DigitalInput { get; } = new List(); - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public IList DigitaOutput { get; } = new List(); - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public IList AnalogInput { get; } = new List(); - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public IList AnalogOutput { get; } = new List(); - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public IList IlluminanceSensor { get; } = new List(); - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public IList PresenceSensor { get; } = new List(); - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public IList TemperatureSensor { get; } = new List(); - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public IList HumiditySensor { get; } = new List(); - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public IList Accelerator { get; } = new List(); - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public IList Barometer { get; } = new List(); - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public IList Gyrometer { get; } = new List(); - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public IList GPSLocation { get; } = new List(); - } -} diff --git a/Samples/CayenneSample/CayenneDecoder/Classes/CayenneEncoder.cs b/Samples/CayenneSample/CayenneDecoder/Classes/CayenneEncoder.cs deleted file mode 100644 index 2f6a31b285..0000000000 --- a/Samples/CayenneSample/CayenneDecoder/Classes/CayenneEncoder.cs +++ /dev/null @@ -1,209 +0,0 @@ -namespace CayenneDecoderModule.Classes -{ - using System; - - public class CayenneEncoder - { - - private byte[] buffer; - - public CayenneEncoder() - { - Reset(); - } - - public void Reset() - { - this.buffer = Array.Empty(); - } - - public int Size => this.buffer.Length; - - public byte[] GetBuffer() - { - return this.buffer; - } - - public int AddDigitalInput(byte channel, byte value) - { - var cursor = this.buffer.Length; - Array.Resize(ref this.buffer, cursor + (int)CayenneTypeSize.DigitalInput); - this.buffer[cursor++] = channel; - this.buffer[cursor++] = (byte)CayenneTypes.DigitalInput; - this.buffer[cursor++] = value; - - return cursor; - } - - public int AddDigitalOutput(byte channel, byte value) - { - var cursor = this.buffer.Length; - Array.Resize(ref this.buffer, cursor + (int)CayenneTypeSize.DigitalOutput); - this.buffer[cursor++] = channel; - this.buffer[cursor++] = (byte)CayenneTypes.DigitalOutput; - this.buffer[cursor++] = value; - - return cursor; - } - - public int AddAnalogInput(byte channel, double value) - { - var cursor = this.buffer.Length; - Array.Resize(ref this.buffer, cursor + (int)CayenneTypeSize.AnalogInput); - var val = (short)(value * 100); - this.buffer[cursor++] = channel; - this.buffer[cursor++] = (byte)CayenneTypes.AnalogInput; - this.buffer[cursor++] = (byte)(val >> 8); - this.buffer[cursor++] = (byte)(val & 0xFF); - - return cursor; - } - - public int AddAnalogOutput(byte channel, double value) - { - var cursor = this.buffer.Length; - Array.Resize(ref this.buffer, cursor + (int)CayenneTypeSize.AnalogOutput); - var val = (short)(value * 100); - this.buffer[cursor++] = channel; - this.buffer[cursor++] = (byte)CayenneTypes.AnalogOutput; - this.buffer[cursor++] = (byte)((val >> 8) & 0xFF); - this.buffer[cursor++] = (byte)(val & 0xFF); - - return cursor; - } - - public int AddLuminosity(byte channel, ushort lux) - { - var cursor = this.buffer.Length; - Array.Resize(ref this.buffer, cursor + (int)CayenneTypeSize.Luminosity); - this.buffer[cursor++] = channel; - this.buffer[cursor++] = (byte)CayenneTypes.Luminosity; - this.buffer[cursor++] = (byte)((lux >> 8) & 0xFF); - this.buffer[cursor++] = (byte)(lux & 0xFF); - - return cursor; - } - - public int AddPresence(byte channel, byte value) - { - var cursor = this.buffer.Length; - Array.Resize(ref this.buffer, cursor + (int)CayenneTypeSize.Presence); - this.buffer[cursor++] = channel; - this.buffer[cursor++] = (byte)CayenneTypes.Presence; - this.buffer[cursor++] = value; - - return cursor; - } - - public int AddTemperature(byte channel, double celsius) - { - var cursor = this.buffer.Length; - Array.Resize(ref this.buffer, cursor + (int)CayenneTypeSize.Temperature); - var val = (short)(celsius * 10); - this.buffer[cursor++] = channel; - this.buffer[cursor++] = (byte)CayenneTypes.Temperature; - this.buffer[cursor++] = (byte)((val >> 8) & 0xFF); - this.buffer[cursor++] = (byte)(val & 0xFF); - - return cursor; - } - - public int AddRelativeHumidity(byte channel, double rh) - { - var cursor = this.buffer.Length; - Array.Resize(ref this.buffer, cursor + (int)CayenneTypeSize.RelativeHumidity); - this.buffer[cursor++] = channel; - this.buffer[cursor++] = (byte)CayenneTypes.RelativeHumidity; - this.buffer[cursor++] = (byte)(rh * 2); - - return cursor; - } - - public int AddAccelerometer(byte channel, double x, double y, double z) - { - var cursor = this.buffer.Length; - Array.Resize(ref this.buffer, cursor + (int)CayenneTypeSize.Accelerator); - var vx = (short)(x * 1000); - var vy = (short)(y * 1000); - var vz = (short)(z * 1000); - - this.buffer[cursor++] = channel; - this.buffer[cursor++] = (byte)CayenneTypes.Accelerator; - this.buffer[cursor++] = (byte)((vx >> 8) & 0xFF); - this.buffer[cursor++] = (byte)(vx & 0xFF); - this.buffer[cursor++] = (byte)((vy >> 8) & 0xFF); - this.buffer[cursor++] = (byte)(vy & 0xFF); - this.buffer[cursor++] = (byte)((vz >> 8) & 0xFF); - this.buffer[cursor++] = (byte)(vz & 0xFF); - - return cursor; - } - - public int AddBarometricPressure(byte channel, double hpa) - { - var cursor = this.buffer.Length; - Array.Resize(ref this.buffer, cursor + (int)CayenneTypeSize.Barometer); - var val = (short)(hpa * 10); - this.buffer[cursor++] = channel; - this.buffer[cursor++] = (byte)CayenneTypes.Barometer; - this.buffer[cursor++] = (byte)((val >> 8) & 0xFF); - this.buffer[cursor++] = (byte)(val & 0xFF); - - return cursor; - } - - public int AddGyrometer(byte channel, double x, double y, double z) - { - var cursor = this.buffer.Length; - Array.Resize(ref this.buffer, cursor + (int)CayenneTypeSize.Gyrometer); - var vx = (short)(x * 100); - var vy = (short)(y * 100); - var vz = (short)(z * 100); - this.buffer[cursor++] = channel; - this.buffer[cursor++] = (byte)CayenneTypes.Gyrometer; - this.buffer[cursor++] = (byte)((vx >> 8) & 0xFF); - this.buffer[cursor++] = (byte)(vx & 0xFF); - this.buffer[cursor++] = (byte)((vy >> 8) & 0xFF); - this.buffer[cursor++] = (byte)(vy & 0xFF); - this.buffer[cursor++] = (byte)((vz >> 8) & 0xFF); - this.buffer[cursor++] = (byte)(vz & 0xFF); - - return cursor; - } - - public int AddGPS(byte channel, double latitude, double longitude, double meters) - { - var cursor = this.buffer.Length; - Array.Resize(ref this.buffer, cursor + (int)CayenneTypeSize.Gps); - var lat = (int)(latitude * 10000); - var latNeg = lat < 0; - lat = latNeg ? -lat : lat; - var lon = (int)(longitude * 10000); - var lonNeg = lon < 0; - lon = lonNeg ? -lon : lon; - var alt = (int)(meters * 100); - var altNeg = alt < 0; - alt = altNeg ? -alt : alt; - // Need to add the sign - this.buffer[cursor++] = channel; - this.buffer[cursor++] = (byte)CayenneTypes.Gps; - this.buffer[cursor++] = (byte)((lat >> 16) & 0xFF); - if (latNeg) - this.buffer[cursor - 1] = (byte)(this.buffer[cursor - 1] | 0x80); - this.buffer[cursor++] = (byte)((lat >> 8) & 0xFF); - this.buffer[cursor++] = (byte)(lat & 0xFF); - this.buffer[cursor++] = (byte)((lon >> 16) & 0xFF); - if (lonNeg) - this.buffer[cursor - 1] = (byte)(this.buffer[cursor - 1] | 0x80); - this.buffer[cursor++] = (byte)((lon >> 8) & 0xFF); - this.buffer[cursor++] = (byte)(lon & 0xFF); - this.buffer[cursor++] = (byte)((alt >> 16) & 0xFF); - if (altNeg) - this.buffer[cursor - 1] = (byte)(this.buffer[cursor - 1] | 0x80); - this.buffer[cursor++] = (byte)((alt >> 8) & 0xFF); - this.buffer[cursor++] = (byte)(alt & 0xFF); - - return cursor; - } - } -} diff --git a/Samples/CayenneSample/CayenneDecoder/Classes/CayenneTypes.cs b/Samples/CayenneSample/CayenneDecoder/Classes/CayenneTypes.cs deleted file mode 100644 index 0373b40d54..0000000000 --- a/Samples/CayenneSample/CayenneDecoder/Classes/CayenneTypes.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace CayenneDecoderModule.Classes -{ - /// - /// This enum is used to find the sensor type - /// The member names has to match with the CayenneTypeSize one - /// - public enum CayenneTypes - { - DigitalInput = 0, // 1 byte - DigitalOutput = 1, // 1 byte - AnalogInput = 2, // 2 bytes, 0.01 signed - AnalogOutput = 3, // 2 bytes, 0.01 signed - Luminosity = 101, // 2 bytes, 1 lux unsigned - Presence = 102, // 1 byte, 1 - Temperature = 103, // 2 bytes, 0.1°C signed - RelativeHumidity = 104, // 1 byte, 0.5% unsigned - Accelerator = 113, // 2 bytes per axis, 0.001G - Barometer = 115, // 2 bytes 0.1 hPa Unsigned - Gyrometer = 134, // 2 bytes per axis, 0.01 °/s - Gps = 136 // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter - } - - /// - /// This enum is sued to find the size of the message for a specific sensor - /// The member names has to match with the CayenneTypes one - /// -#pragma warning disable CA1008 // Enums should have zero value -#pragma warning disable CA1069 // Enums values should not be duplicated - // Enum is used as a lookup for constants. - public enum CayenneTypeSize - { - // Data ID + Data Type + Data Size - DigitalInput = 3, // 1 byte - DigitalOutput = 3, // 1 byte - AnalogInput = 4, // 2 bytes, 0.01 signed - AnalogOutput = 4, // 2 bytes, 0.01 signed - Luminosity = 4, // 2 bytes, 1 lux unsigned - Presence = 3, // 1 byte, 1 - Temperature = 4, // 2 bytes, 0.1°C signed - RelativeHumidity = 3, // 1 byte, 0.5% unsigned - Accelerator = 8, // 2 bytes per axis, 0.001G - Barometer = 4, // 2 bytes 0.1 hPa Unsigned - Gyrometer = 8, // 2 bytes per axis, 0.01 °/s - Gps = 11 // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter - } -#pragma warning restore CA1069 // Enums values should not be duplicated -#pragma warning restore CA1008 // Enums should have zero value -} - diff --git a/Samples/CayenneSample/CayenneDecoder/Classes/ConversionHelper.cs b/Samples/CayenneSample/CayenneDecoder/Classes/ConversionHelper.cs deleted file mode 100644 index 34710bc454..0000000000 --- a/Samples/CayenneSample/CayenneDecoder/Classes/ConversionHelper.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace CayenneDecoderModule.Classes -{ - using System; - using System.Text; - - public static class ConversionHelper - { - /// - /// Method enabling to convert a hex string to a byte array. - /// - /// Input hex string - /// - public static byte[] StringToByteArray(string hex) - { - if (hex is null) throw new ArgumentNullException(nameof(hex)); - var NumberChars = hex.Length; - var bytes = new byte[NumberChars / 2]; - for (var i = 0; i < NumberChars; i += 2) - bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); - return bytes; - } - - public static string ByteArrayToString(byte[] bytes) - { - if (bytes is null) throw new ArgumentNullException(nameof(bytes)); - var Result = new StringBuilder(bytes.Length * 2); - var HexAlphabet = "0123456789ABCDEF"; - - foreach (var B in bytes) - { - _ = Result.Append(HexAlphabet[B >> 4]); - _ = Result.Append(HexAlphabet[B & 0xF]); - } - - return Result.ToString(); - } - } -} diff --git a/Samples/CayenneSample/CayenneDecoder/Classes/LoraDecoders.cs b/Samples/CayenneSample/CayenneDecoder/Classes/LoraDecoders.cs deleted file mode 100644 index 8355667b40..0000000000 --- a/Samples/CayenneSample/CayenneDecoder/Classes/LoraDecoders.cs +++ /dev/null @@ -1,20 +0,0 @@ -#pragma warning disable IDE0051 // Remove unused private members -#pragma warning disable CA1801 // Review unused parameters -#pragma warning disable IDE0060 // Remove unused parameter -#pragma warning disable CA1812 // Remove unused class - -namespace CayenneDecoderModule.Classes -{ - using Newtonsoft.Json; - - internal static class LoraDecoders - { - private static string CayenneDecoder(byte[] payload, uint fport) - { - var cayenneDecoder = new CayenneDecoder(payload); - - // Return a JSON string containing the decoded data - return JsonConvert.SerializeObject(new { value = cayenneDecoder.CayenneDevice }); - } - } -} diff --git a/Samples/CayenneSample/CayenneDecoder/Classes/Validator.cs b/Samples/CayenneSample/CayenneDecoder/Classes/Validator.cs deleted file mode 100644 index 60aa77974a..0000000000 --- a/Samples/CayenneSample/CayenneDecoder/Classes/Validator.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace CayenneDecoderModule.Classes -{ - using System.Net; - - public static class Validator - { - public static void ValidateParameters(string fport, string payload) - { - var error = ""; - - if (fport == null) - { - error += "Fport missing"; - } - if (payload == null) - { - if (!string.IsNullOrEmpty(error)) - error += " and "; - error += "Payload missing"; - } - - if (!string.IsNullOrEmpty(error)) - { - throw new WebException(error); - } - } - } -} diff --git a/Samples/CayenneSample/CayenneDecoder/Controllers/DecoderController.cs b/Samples/CayenneSample/CayenneDecoder/Controllers/DecoderController.cs deleted file mode 100644 index 7c0d878a35..0000000000 --- a/Samples/CayenneSample/CayenneDecoder/Controllers/DecoderController.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace CayenneDecoderModule.Controllers -{ - using System; - using Microsoft.AspNetCore.Mvc; - using System.Net; - using CayenneDecoderModule.Classes; - using System.Reflection; - using System.Globalization; - - [Route("api")] - [ApiController] - public class DecoderController : ControllerBase - { - // GET: api/TestDecoder - [HttpGet("{decoder}", Name = "Get")] - [ProducesResponseType(200)] - [ProducesResponseType(400)] - public ActionResult Get(string decoder, string fport, string payload) - { - // Validate that fport and payload URL parameters are present. - Validator.ValidateParameters(fport, payload); - - var decoderType = typeof(LoraDecoders); - var toInvoke = decoderType.GetMethod(decoder, BindingFlags.Static | BindingFlags.NonPublic); - - if (toInvoke != null) - { - return (string)toInvoke.Invoke(null, new object[] { Convert.FromBase64String(payload), Convert.ToUInt16(fport, CultureInfo.InvariantCulture) }); - } - else - { - throw new WebException($"Decoder {decoder} not found."); - } - } - } -} diff --git a/Samples/CayenneSample/CayenneDecoder/Program.cs b/Samples/CayenneSample/CayenneDecoder/Program.cs deleted file mode 100644 index 88bcf12652..0000000000 --- a/Samples/CayenneSample/CayenneDecoder/Program.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace CayenneDecoderModule -{ - using System; - using Microsoft.AspNetCore; - using Microsoft.AspNetCore.Hosting; - - public static class Program - { - public static void Main(string[] args) - { - CreateWebHostBuilder(args).Build().Run(); - } - - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup() - .UseKestrel(x => x.Limits.KeepAliveTimeout = TimeSpan.FromDays(10)); - - } -} diff --git a/Samples/CayenneSample/CayenneDecoder/Startup.cs b/Samples/CayenneSample/CayenneDecoder/Startup.cs deleted file mode 100644 index 67a76a131f..0000000000 --- a/Samples/CayenneSample/CayenneDecoder/Startup.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace CayenneDecoderModule -{ - using System; - using System.Text; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Diagnostics; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - _ = services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) - { - if (env.IsDevelopment()) - { - _ = app.UseDeveloperExceptionPage(); - } - else - { - //app.UseHsts(); - } - - _ = app.UseExceptionHandler(errorApp => - { - errorApp.Run(async context => - { - context.Response.StatusCode = 400; // We are using HTTP Status Code 400 - Bad Request. - context.Response.ContentType = "text/plain"; - - var error = context.Features.Get(); - if (error != null) - { - var ex = error.Error; - string exMessage; - if (ex.InnerException != null) - exMessage = $"Decoder error: {ex.InnerException.Message}"; - - else - exMessage = ex.Message; - - Console.WriteLine($"Exception at: {System.DateTime.UtcNow}: {exMessage}"); - await context.Response.WriteAsync(exMessage, Encoding.UTF8); - } - }); - }); - - //app.UseHttpsRedirection(); - _ = app.UseMvc(); - } - } -} diff --git a/Samples/CayenneSample/CayenneDecoder/appsettings.Development.json b/Samples/CayenneSample/CayenneDecoder/appsettings.Development.json deleted file mode 100644 index e203e9407e..0000000000 --- a/Samples/CayenneSample/CayenneDecoder/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } -} diff --git a/Samples/CayenneSample/CayenneDecoder/appsettings.json b/Samples/CayenneSample/CayenneDecoder/appsettings.json deleted file mode 100644 index def9159a7d..0000000000 --- a/Samples/CayenneSample/CayenneDecoder/appsettings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/Samples/CayenneSample/CayenneDecoderTest/CayenneDecoderTest.csproj b/Samples/CayenneSample/CayenneDecoderTest/CayenneDecoderTest.csproj deleted file mode 100644 index 27d1efcff3..0000000000 --- a/Samples/CayenneSample/CayenneDecoderTest/CayenneDecoderTest.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - netcoreapp2.1 - - false - - - - - - - - - - - - - - diff --git a/Samples/CayenneSample/CayenneDecoderTest/CayenneTest.cs b/Samples/CayenneSample/CayenneDecoderTest/CayenneTest.cs deleted file mode 100644 index af076d0057..0000000000 --- a/Samples/CayenneSample/CayenneDecoderTest/CayenneTest.cs +++ /dev/null @@ -1,515 +0,0 @@ -namespace CayenneDecoderTest -{ - using System; - using Xunit; - using CayenneDecoderModule.Classes; - using CayenneDecoderModule.Controllers; - using System.Linq; - - public class CayenneTest - { - [Fact] - public void TestDecoderFromPayload() - { - var payload64 = "AWcA5gJoMANzJigEZQD9"; - var decodedjson = "{\"value\":{\"IlluminanceSensor\":[{\"Channel\":4,\"Value\":253}],\"TemperatureSensor\":[{\"Channel\":1,\"Value\":23.0}],\"HumiditySensor\":[{\"Channel\":2,\"Value\":24.0}],\"Barometer\":[{\"Channel\":3,\"Value\":976.8}]}}"; - var decoderController = new DecoderController(); - var jsonret = decoderController.Get("CayenneDecoder", "1", payload64).Value; - Assert.Equal(decodedjson, jsonret); - } - - - - [Fact] - public void TestAllSensors() - { - var cayenneEncoder = new CayenneEncoder(); - var temp = 33.7; - byte channelt = 1; - _ = cayenneEncoder.AddTemperature(channelt, temp); - byte di = 37; - byte channeldi = 2; - _ = cayenneEncoder.AddDigitalInput(channeldi, di); - byte dod = 37; - byte channeldo = 3; - _ = cayenneEncoder.AddDigitalOutput(channeldo, dod); - double ao = -37; - byte channelao = 4; - _ = cayenneEncoder.AddAnalogOutput(channelao, ao); - double ai = 37; - byte channelai = 5; - _ = cayenneEncoder.AddAnalogInput(channelai, ai); - ushort lum = 37124; - byte channellum = 6; - _ = cayenneEncoder.AddLuminosity(channellum, lum); - byte ps = 104; - byte channelps = 6; - _ = cayenneEncoder.AddPresence(channelps, ps); - var hum = 99.5; - byte channelhum = 6; - _ = cayenneEncoder.AddRelativeHumidity(channelhum, hum); - var baro = 1014.5; - byte channelbaro = 7; - _ = cayenneEncoder.AddBarometricPressure(channelbaro, baro); - var ax = -4.545; - var ay = 4.673; - var az = 1.455; - byte channela = 8; - _ = cayenneEncoder.AddAccelerometer(channela, ax, ay, az); - var gx = 4.54; - var gy = -4.63; - var gz = 1.55; - byte channelg = 6; - _ = cayenneEncoder.AddGyrometer(channelg, gx, gy, gz); - var lat = -4.54; - var lon = 4.63; - var alt = 1.55; - byte channelgps = 6; - _ = cayenneEncoder.AddGPS(channelgps, lat, lon, alt); - - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.TemperatureSensor[0].Value, temp); - Assert.Equal(cayenneDecoder.CayenneDevice.TemperatureSensor[0].Channel, channelt); - Assert.Equal(cayenneDecoder.CayenneDevice.DigitalInput[0].Value, di); - Assert.Equal(cayenneDecoder.CayenneDevice.DigitalInput[0].Channel, channeldi); - Assert.Equal(cayenneDecoder.CayenneDevice.DigitaOutput[0].Value, dod); - Assert.Equal(cayenneDecoder.CayenneDevice.DigitaOutput[0].Channel, channeldo); - Assert.Equal(cayenneDecoder.CayenneDevice.AnalogOutput[0].Value, ao); - Assert.Equal(cayenneDecoder.CayenneDevice.AnalogOutput[0].Channel, channelao); - Assert.Equal(cayenneDecoder.CayenneDevice.AnalogOutput[0].Value, ao); - Assert.Equal(cayenneDecoder.CayenneDevice.AnalogOutput[0].Channel, channelao); - Assert.Equal(cayenneDecoder.CayenneDevice.IlluminanceSensor[0].Value, lum); - Assert.Equal(cayenneDecoder.CayenneDevice.IlluminanceSensor[0].Channel, channellum); - Assert.Equal(cayenneDecoder.CayenneDevice.PresenceSensor[0].Value, ps); - Assert.Equal(cayenneDecoder.CayenneDevice.PresenceSensor[0].Channel, channelps); - Assert.Equal(cayenneDecoder.CayenneDevice.HumiditySensor[0].Value, hum); - Assert.Equal(cayenneDecoder.CayenneDevice.HumiditySensor[0].Channel, channelhum); - Assert.Equal(cayenneDecoder.CayenneDevice.Barometer[0].Value, baro); - Assert.Equal(cayenneDecoder.CayenneDevice.Barometer[0].Channel, channelbaro); - Assert.Equal(cayenneDecoder.CayenneDevice.Accelerator[0].X, ax); - Assert.Equal(cayenneDecoder.CayenneDevice.Accelerator[0].Y, ay); - Assert.Equal(cayenneDecoder.CayenneDevice.Accelerator[0].Z, az); - Assert.Equal(cayenneDecoder.CayenneDevice.Accelerator[0].Channel, channela); - Assert.Equal(cayenneDecoder.CayenneDevice.Gyrometer[0].X, gx); - Assert.Equal(cayenneDecoder.CayenneDevice.Gyrometer[0].Y, gy); - Assert.Equal(cayenneDecoder.CayenneDevice.Gyrometer[0].Z, gz); - Assert.Equal(cayenneDecoder.CayenneDevice.Gyrometer[0].Channel, channelg); - Assert.Equal(cayenneDecoder.CayenneDevice.GPSLocation[0].Latitude, lat); - Assert.Equal(cayenneDecoder.CayenneDevice.GPSLocation[0].Longitude, lon); - Assert.Equal(cayenneDecoder.CayenneDevice.GPSLocation[0].Altitude, alt); - Assert.Equal(cayenneDecoder.CayenneDevice.GPSLocation[0].Channel, channelgps); - } - [Fact] - public void TemeperatureMultiple() - { - var cayenneEncoder = new CayenneEncoder(); - var temp1 = 33.7; - byte channelt1 = 1; - _ = cayenneEncoder.AddTemperature(channelt1, temp1); - var temp2 = -10.2; - byte channelt2 = 2; - _ = cayenneEncoder.AddTemperature(channelt2, temp2); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.TemperatureSensor[0].Value, temp1); - Assert.Equal(cayenneDecoder.CayenneDevice.TemperatureSensor[0].Channel, channelt1); - Assert.Equal(cayenneDecoder.CayenneDevice.TemperatureSensor[1].Value, temp2); - Assert.Equal(cayenneDecoder.CayenneDevice.TemperatureSensor[1].Channel, channelt2); - } - - [Fact] - public void Temeperature() - { - var cayenneEncoder = new CayenneEncoder(); - var temp = 33.7; - byte channelt = 1; - _ = cayenneEncoder.AddTemperature(channelt, temp); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.TemperatureSensor[0].Value, temp); - Assert.Equal(cayenneDecoder.CayenneDevice.TemperatureSensor[0].Channel, channelt); - } - - [Fact] - public void DigitalInputMultiple() - { - var cayenneEncoder = new CayenneEncoder(); - byte di = 37; - byte channeldi = 2; - _ = cayenneEncoder.AddDigitalInput(channeldi, di); - byte di2 = 255; - byte channeldi2 = 5; - _ = cayenneEncoder.AddDigitalInput(channeldi2, di2); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.DigitalInput[0].Value, di); - Assert.Equal(cayenneDecoder.CayenneDevice.DigitalInput[0].Channel, channeldi); - Assert.Equal(cayenneDecoder.CayenneDevice.DigitalInput[1].Value, di2); - Assert.Equal(cayenneDecoder.CayenneDevice.DigitalInput[1].Channel, channeldi2); - } - - [Fact] - public void DigitalInput() - { - var cayenneEncoder = new CayenneEncoder(); - byte di = 37; - byte channeldi = 2; - _ = cayenneEncoder.AddDigitalInput(channeldi, di); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.DigitalInput[0].Value, di); - Assert.Equal(cayenneDecoder.CayenneDevice.DigitalInput[0].Channel, channeldi); - } - - [Fact] - public void DigitalOutputMultiple() - { - var cayenneEncoder = new CayenneEncoder(); - byte dod = 37; - byte channeldo = 3; - _ = cayenneEncoder.AddDigitalOutput(channeldo, dod); - byte dod2 = 12; - byte channeldo2 = 35; - _ = cayenneEncoder.AddDigitalOutput(channeldo2, dod2); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.DigitaOutput[0].Value, dod); - Assert.Equal(cayenneDecoder.CayenneDevice.DigitaOutput[0].Channel, channeldo); - Assert.Equal(cayenneDecoder.CayenneDevice.DigitaOutput[1].Value, dod2); - Assert.Equal(cayenneDecoder.CayenneDevice.DigitaOutput[1].Channel, channeldo2); - - } - - [Fact] - public void DigitalOutput() - { - var cayenneEncoder = new CayenneEncoder(); - byte dod = 37; - byte channeldo = 3; - _ = cayenneEncoder.AddDigitalOutput(channeldo, dod); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.DigitaOutput[0].Value, dod); - Assert.Equal(cayenneDecoder.CayenneDevice.DigitaOutput[0].Channel, channeldo); - } - - [Fact] - public void AnalogOutputMultiple() - { - var cayenneEncoder = new CayenneEncoder(); - double ao = -37; - byte channelao = 4; - _ = cayenneEncoder.AddAnalogOutput(channelao, ao); - double ao2 = 128; - byte channelao2 = 40; - _ = cayenneEncoder.AddAnalogOutput(channelao2, ao2); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.AnalogOutput[0].Value, ao); - Assert.Equal(cayenneDecoder.CayenneDevice.AnalogOutput[0].Channel, channelao); - Assert.Equal(cayenneDecoder.CayenneDevice.AnalogOutput[1].Value, ao2); - Assert.Equal(cayenneDecoder.CayenneDevice.AnalogOutput[1].Channel, channelao2); - } - - [Fact] - public void AnalogOutput() - { - var cayenneEncoder = new CayenneEncoder(); - double ao = -37; - byte channelao = 4; - _ = cayenneEncoder.AddAnalogOutput(channelao, ao); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.AnalogOutput[0].Value, ao); - Assert.Equal(cayenneDecoder.CayenneDevice.AnalogOutput[0].Channel, channelao); - } - - [Fact] - public void AnalogInputMultiple() - { - var cayenneEncoder = new CayenneEncoder(); - double ai = 37; - byte channelai = 5; - _ = cayenneEncoder.AddAnalogInput(channelai, ai); - double ai2 = -37; - byte channelai2 = 50; - _ = cayenneEncoder.AddAnalogInput(channelai2, ai2); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.AnalogInput[0].Value, ai); - Assert.Equal(cayenneDecoder.CayenneDevice.AnalogInput[0].Channel, channelai); - Assert.Equal(cayenneDecoder.CayenneDevice.AnalogInput[1].Value, ai2); - Assert.Equal(cayenneDecoder.CayenneDevice.AnalogInput[1].Channel, channelai2); - } - - [Fact] - public void AnalogInput() - { - var cayenneEncoder = new CayenneEncoder(); - double ai = 37; - byte channelai = 5; - _ = cayenneEncoder.AddAnalogInput(channelai, ai); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.AnalogInput[0].Value, ai); - Assert.Equal(cayenneDecoder.CayenneDevice.AnalogInput[0].Channel, channelai); - } - - [Fact] - public void LuminosityMultiple() - { - var cayenneEncoder = new CayenneEncoder(); - ushort lum = 37124; - byte channellum = 6; - _ = cayenneEncoder.AddLuminosity(channellum, lum); - ushort lum2 = 374; - byte channellum2 = 60; - _ = cayenneEncoder.AddLuminosity(channellum2, lum2); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.IlluminanceSensor[0].Value, lum); - Assert.Equal(cayenneDecoder.CayenneDevice.IlluminanceSensor[0].Channel, channellum); - Assert.Equal(cayenneDecoder.CayenneDevice.IlluminanceSensor[1].Value, lum2); - Assert.Equal(cayenneDecoder.CayenneDevice.IlluminanceSensor[1].Channel, channellum2); - } - - [Fact] - public void Luminosity() - { - var cayenneEncoder = new CayenneEncoder(); - ushort lum = 37124; - byte channellum = 6; - _ = cayenneEncoder.AddLuminosity(channellum, lum); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.IlluminanceSensor[0].Value, lum); - Assert.Equal(cayenneDecoder.CayenneDevice.IlluminanceSensor[0].Channel, channellum); - } - - [Fact] - public void PresenceSensorMultiple() - { - var cayenneEncoder = new CayenneEncoder(); - byte ps = 104; - byte channelps = 6; - _ = cayenneEncoder.AddPresence(channelps, ps); - byte ps2 = 14; - byte channelps2 = 16; - _ = cayenneEncoder.AddPresence(channelps2, ps2); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.PresenceSensor[0].Value, ps); - Assert.Equal(cayenneDecoder.CayenneDevice.PresenceSensor[0].Channel, channelps); - Assert.Equal(cayenneDecoder.CayenneDevice.PresenceSensor[1].Value, ps2); - Assert.Equal(cayenneDecoder.CayenneDevice.PresenceSensor[1].Channel, channelps2); - } - - [Fact] - public void PresenceSensor() - { - var cayenneEncoder = new CayenneEncoder(); - byte ps = 104; - byte channelps = 6; - _ = cayenneEncoder.AddPresence(channelps, ps); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.PresenceSensor[0].Value, ps); - Assert.Equal(cayenneDecoder.CayenneDevice.PresenceSensor[0].Channel, channelps); - } - - [Fact] - public void HumiditySensorMultiple() - { - var cayenneEncoder = new CayenneEncoder(); - var hum = 99.5; - byte channelhum = 6; - _ = cayenneEncoder.AddRelativeHumidity(channelhum, hum); - var hum2 = 10.0; - byte channelhum2 = 64; - _ = cayenneEncoder.AddRelativeHumidity(channelhum2, hum2); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.HumiditySensor[0].Value, hum); - Assert.Equal(cayenneDecoder.CayenneDevice.HumiditySensor[0].Channel, channelhum); - Assert.Equal(cayenneDecoder.CayenneDevice.HumiditySensor[1].Value, hum2); - Assert.Equal(cayenneDecoder.CayenneDevice.HumiditySensor[1].Channel, channelhum2); - } - - [Fact] - public void HumiditySensor() - { - var cayenneEncoder = new CayenneEncoder(); - var hum = 99.5; - byte channelhum = 6; - _ = cayenneEncoder.AddRelativeHumidity(channelhum, hum); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.HumiditySensor[0].Value, hum); - Assert.Equal(cayenneDecoder.CayenneDevice.HumiditySensor[0].Channel, channelhum); - } - - [Fact] - public void BarometerMultiple() - { - var cayenneEncoder = new CayenneEncoder(); - var baro = 1014.5; - byte channelbaro = 7; - _ = cayenneEncoder.AddBarometricPressure(channelbaro, baro); - var baro2 = 914.0; - byte channelbaro2 = 17; - _ = cayenneEncoder.AddBarometricPressure(channelbaro2, baro2); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.Barometer[0].Value, baro); - Assert.Equal(cayenneDecoder.CayenneDevice.Barometer[0].Channel, channelbaro); - Assert.Equal(cayenneDecoder.CayenneDevice.Barometer[1].Value, baro2); - Assert.Equal(cayenneDecoder.CayenneDevice.Barometer[1].Channel, channelbaro2); - } - - [Fact] - public void Barometer() - { - var cayenneEncoder = new CayenneEncoder(); - var baro = 1014.5; - byte channelbaro = 7; - _ = cayenneEncoder.AddBarometricPressure(channelbaro, baro); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.Barometer[0].Value, baro); - Assert.Equal(cayenneDecoder.CayenneDevice.Barometer[0].Channel, channelbaro); - } - - [Fact] - public void AcceleratorMultiple() - { - var cayenneEncoder = new CayenneEncoder(); - var ax = -4.545; - var ay = 4.673; - var az = 1.455; - byte channela = 8; - _ = cayenneEncoder.AddAccelerometer(channela, ax, ay, az); - _ = cayenneEncoder.AddAccelerometer((byte)(channela * 2), ax * 2, ay * -2, az * 2); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.Accelerator[0].X, ax); - Assert.Equal(cayenneDecoder.CayenneDevice.Accelerator[0].Y, ay); - Assert.Equal(cayenneDecoder.CayenneDevice.Accelerator[0].Z, az); - Assert.Equal(cayenneDecoder.CayenneDevice.Accelerator[0].Channel, channela); - Assert.Equal(cayenneDecoder.CayenneDevice.Accelerator[1].X, ax * 2); - Assert.Equal(cayenneDecoder.CayenneDevice.Accelerator[1].Y, ay * -2); - Assert.Equal(cayenneDecoder.CayenneDevice.Accelerator[1].Z, az * 2); - Assert.Equal(cayenneDecoder.CayenneDevice.Accelerator[1].Channel, channela * 2); - } - - [Fact] - public void Accelerator() - { - var cayenneEncoder = new CayenneEncoder(); - var ax = -4.545; - var ay = 4.673; - var az = 1.455; - byte channela = 8; - _ = cayenneEncoder.AddAccelerometer(channela, ax, ay, az); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.Accelerator[0].X, ax); - Assert.Equal(cayenneDecoder.CayenneDevice.Accelerator[0].Y, ay); - Assert.Equal(cayenneDecoder.CayenneDevice.Accelerator[0].Z, az); - Assert.Equal(cayenneDecoder.CayenneDevice.Accelerator[0].Channel, channela); - } - - [Fact] - public void GyrometerMultiple() - { - var cayenneEncoder = new CayenneEncoder(); - var gx = 4.54; - var gy = -4.63; - var gz = 1.55; - byte channelg = 6; - _ = cayenneEncoder.AddGyrometer(channelg, gx, gy, gz); - _ = cayenneEncoder.AddGyrometer((byte)(channelg * 2), gx * -2, gy * 2, gz * -2); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.Gyrometer[0].X, gx); - Assert.Equal(cayenneDecoder.CayenneDevice.Gyrometer[0].Y, gy); - Assert.Equal(cayenneDecoder.CayenneDevice.Gyrometer[0].Z, gz); - Assert.Equal(cayenneDecoder.CayenneDevice.Gyrometer[0].Channel, channelg); - Assert.Equal(cayenneDecoder.CayenneDevice.Gyrometer[1].X, gx * -2); - Assert.Equal(cayenneDecoder.CayenneDevice.Gyrometer[1].Y, gy * 2); - Assert.Equal(cayenneDecoder.CayenneDevice.Gyrometer[1].Z, gz * -2); - Assert.Equal(cayenneDecoder.CayenneDevice.Gyrometer[1].Channel, channelg * 2); - } - - [Fact] - public void Gyrometer() - { - var cayenneEncoder = new CayenneEncoder(); - var gx = 4.54; - var gy = -4.63; - var gz = 1.55; - byte channelg = 6; - _ = cayenneEncoder.AddGyrometer(channelg, gx, gy, gz); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.Gyrometer[0].X, gx); - Assert.Equal(cayenneDecoder.CayenneDevice.Gyrometer[0].Y, gy); - Assert.Equal(cayenneDecoder.CayenneDevice.Gyrometer[0].Z, gz); - Assert.Equal(cayenneDecoder.CayenneDevice.Gyrometer[0].Channel, channelg); - } - - [Fact] - public void GPSLocationMultiple() - { - var cayenneEncoder = new CayenneEncoder(); - var lat = -4.54; - var lon = 4.63; - var alt = 1.55; - byte channelgps = 6; - _ = cayenneEncoder.AddGPS(channelgps, lat, lon, alt); - _ = cayenneEncoder.AddGPS((byte)(channelgps * 2), lat * -2, lon * -2, alt * -2); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.GPSLocation[0].Latitude, lat); - Assert.Equal(cayenneDecoder.CayenneDevice.GPSLocation[0].Longitude, lon); - Assert.Equal(cayenneDecoder.CayenneDevice.GPSLocation[0].Altitude, alt); - Assert.Equal(cayenneDecoder.CayenneDevice.GPSLocation[0].Channel, channelgps); - Assert.Equal(cayenneDecoder.CayenneDevice.GPSLocation[1].Latitude, lat * -2); - Assert.Equal(cayenneDecoder.CayenneDevice.GPSLocation[1].Longitude, lon * -2); - Assert.Equal(cayenneDecoder.CayenneDevice.GPSLocation[1].Altitude, alt * -2); - Assert.Equal(cayenneDecoder.CayenneDevice.GPSLocation[1].Channel, channelgps * 2); - } - - [Fact] - public void GPSLocation() - { - var cayenneEncoder = new CayenneEncoder(); - var lat = -4.54; - var lon = 4.63; - var alt = 1.55; - byte channelgps = 6; - _ = cayenneEncoder.AddGPS(channelgps, lat, lon, alt); - var buff = cayenneEncoder.GetBuffer(); - var cayenneDecoder = new CayenneDecoder(buff); - Assert.Equal(cayenneDecoder.CayenneDevice.GPSLocation[0].Latitude, lat); - Assert.Equal(cayenneDecoder.CayenneDevice.GPSLocation[0].Longitude, lon); - Assert.Equal(cayenneDecoder.CayenneDevice.GPSLocation[0].Altitude, alt); - Assert.Equal(cayenneDecoder.CayenneDevice.GPSLocation[0].Channel, channelgps); - } - - [Fact] - public void EnumIntegrity() - { - foreach (var elem in Enum.GetNames(typeof(CayenneTypes))) - { - Assert.Equal(elem, Enum.GetNames(typeof(CayenneTypeSize)).Where(m => m == elem).FirstOrDefault()); - } - foreach (var elem in Enum.GetNames(typeof(CayenneTypeSize))) - { - Assert.Equal(elem, Enum.GetNames(typeof(CayenneTypes)).Where(m => m == elem).FirstOrDefault()); - } - - - } - } -} diff --git a/Samples/CayenneSample/Dockerfile.amd64 b/Samples/CayenneSample/Dockerfile.amd64 deleted file mode 100644 index ee7d638742..0000000000 --- a/Samples/CayenneSample/Dockerfile.amd64 +++ /dev/null @@ -1,14 +0,0 @@ -FROM microsoft/dotnet:2.1-sdk AS build -WORKDIR /build/Samples/Cayenne/ - -# copy everything else and build app -COPY ./Samples/CayenneSample ./ -WORKDIR /build/Samples/Cayenne/CayenneDecoder - -RUN dotnet restore -RUN dotnet publish -c Release -o out - -FROM microsoft/dotnet:2.1-aspnetcore-runtime AS runtime -WORKDIR /app -COPY --from=build /build/Samples/Cayenne/CayenneDecoder/out ./ -ENTRYPOINT ["dotnet", "CayenneDecoderModule.dll"] diff --git a/Samples/CayenneSample/Dockerfile.arm32v7 b/Samples/CayenneSample/Dockerfile.arm32v7 deleted file mode 100644 index 4dbca9e806..0000000000 --- a/Samples/CayenneSample/Dockerfile.arm32v7 +++ /dev/null @@ -1,14 +0,0 @@ -FROM microsoft/dotnet:2.1-sdk AS build -WORKDIR /build/Samples/Cayenne/ - -# copy everything else and build app -COPY ./Samples/CayenneSample ./ -WORKDIR /build/Samples/Cayenne/CayenneDecoder - -RUN dotnet restore -RUN dotnet publish -c Release -o out - -FROM microsoft/dotnet:2.1-aspnetcore-runtime-stretch-slim-arm32v7 AS runtime -WORKDIR /app -COPY --from=build /build/Samples/Cayenne/CayenneDecoder/out ./ -ENTRYPOINT ["dotnet", "CayenneDecoderModule.dll"] diff --git a/Samples/CayenneSample/module.json b/Samples/CayenneSample/module.json deleted file mode 100644 index 5a3fc9741e..0000000000 --- a/Samples/CayenneSample/module.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema-version": "0.0.1", - "description": "", - "image": { - "repository": "your.azurecr.io/cayennedecoder", - "tag": { - "version": "0.0.1", - "platforms": { - "amd64": "./Dockerfile.amd64", - "arm32v7": "./Dockerfile.arm32v7" - } - }, - "buildOptions": [], - "contextPath": "../../" - }, - "language": "csharp" -} diff --git a/Samples/DecoderSample/SensorDecoderModule.csproj b/Samples/DecoderSample/SensorDecoderModule.csproj old mode 100755 new mode 100644 index 4733224b59..625d31faec --- a/Samples/DecoderSample/SensorDecoderModule.csproj +++ b/Samples/DecoderSample/SensorDecoderModule.csproj @@ -9,7 +9,7 @@ - + diff --git a/Samples/UniversalDecoder/Dockerfile.amd64 b/Samples/UniversalDecoder/Dockerfile.amd64 index 74adeac95c..158cf4d2c3 100644 --- a/Samples/UniversalDecoder/Dockerfile.amd64 +++ b/Samples/UniversalDecoder/Dockerfile.amd64 @@ -1,4 +1,6 @@ -FROM node:14-alpine +# NOTE: Use either docker.io or your own registry address to build the image +ARG SOURCE_CONTAINER_REGISTRY_ADDRESS=your-registry-address.azurecr.io +FROM $SOURCE_CONTAINER_REGISTRY_ADDRESS/amd64/node:14-alpine WORKDIR /app/ diff --git a/Samples/UniversalDecoder/Dockerfile.arm32v7 b/Samples/UniversalDecoder/Dockerfile.arm32v7 index 9f9f6aed35..1f065cfcf4 100644 --- a/Samples/UniversalDecoder/Dockerfile.arm32v7 +++ b/Samples/UniversalDecoder/Dockerfile.arm32v7 @@ -1,4 +1,6 @@ -FROM arm32v7/node:14-slim +# NOTE: Use either docker.io or your own registry address to build the image +ARG SOURCE_CONTAINER_REGISTRY_ADDRESS=your-registry-address.azurecr.io +FROM $SOURCE_CONTAINER_REGISTRY_ADDRESS/arm32v7/node:14-slim WORKDIR /app/ diff --git a/Samples/UniversalDecoder/Dockerfile.arm64v8 b/Samples/UniversalDecoder/Dockerfile.arm64v8 index 69787af0ab..152e79c517 100644 --- a/Samples/UniversalDecoder/Dockerfile.arm64v8 +++ b/Samples/UniversalDecoder/Dockerfile.arm64v8 @@ -1,4 +1,6 @@ -FROM arm64v8/node:14-slim +# NOTE: Use either docker.io or your own registry address to build the image +ARG SOURCE_CONTAINER_REGISTRY_ADDRESS=your-registry-address.azurecr.io +FROM $SOURCE_CONTAINER_REGISTRY_ADDRESS/arm64v8/node:14-slim WORKDIR /app/ diff --git a/Samples/UniversalDecoder/NOTICE.txt b/Samples/UniversalDecoder/NOTICE.txt index cfdb15ba1f..96bd1d9480 100644 --- a/Samples/UniversalDecoder/NOTICE.txt +++ b/Samples/UniversalDecoder/NOTICE.txt @@ -209,7 +209,62 @@ Copyright (c) 2019 The Things Network Foundation, The Things Industries B.V. --------------------------------------------------------- -qs 6.9.6 - BSD-3-Clause +esprima-next 5.8.4 - BSD-2-Clause +https://github.com/node-projects/esprima-next/ + +Copyright JS Foundation and other contributors, https://js.foundation + +Copyright JS Foundation and other contributors, https://js.foundation/ + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +ieee754 1.2.1 - BSD-3-Clause +https://github.com/feross/ieee754#readme + +Copyright 2008 Fair Oaks Labs, Inc. +Copyright (c) 2008, Fair Oaks Labs, Inc. + +Copyright 2008 Fair Oaks Labs, Inc. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +qs 6.10.3 - BSD-3-Clause https://github.com/ljharb/qs Copyright (c) 2014, Nathan LaFreniere and other contributors (https://github.com/ljharb/qs/graphs/contributors) @@ -249,7 +304,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --------------------------------------------------------- -secure-json-parse 2.4.0 - BSD-3-Clause +secure-json-parse 2.5.0 - BSD-3-Clause https://github.com/fastify/secure-json-parse#readme Copyright (c) 2019 The Fastify Team @@ -259,17 +314,19 @@ Copyright (c) 2019 The Fastify Team Copyright (c) 2019, Sideway Inc, and project contributors All rights reserved. +The complete list of contributors can be found at: +- https://github.com/hapijs/bourne/graphs/contributors +- https://github.com/fastify/secure-json-parse/graphs/contributors + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -* The names of any contributors may not be used to endorse or promote products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS OFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -The complete list of contributors can be found at: -- https://github.com/hapijs/bourne/graphs/contributors -- https://github.com/fastify/secure-json-parse/graphs/contributors +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --------------------------------------------------------- @@ -280,7 +337,7 @@ fs.realpath 1.0.0 - ISC https://github.com/isaacs/fs.realpath#readme Copyright (c) Isaac Z. Schlueter and Contributors -Copyright Joyent, Inc. and other Node contributors. +Copyright Joyent, Inc. and other Node contributors The ISC License @@ -348,14 +405,14 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE --------------------------------------------------------- -glob 7.2.0 - ISC +glob 8.0.3 - ISC https://github.com/isaacs/node-glob#readme -Copyright (c) Isaac Z. Schlueter and Contributors +Copyright (c) 2009-2022 Isaac Z. Schlueter and Contributors The ISC License -Copyright (c) Isaac Z. Schlueter and Contributors +Copyright (c) 2009-2022 Isaac Z. Schlueter and Contributors Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -369,18 +426,12 @@ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -## Glob Logo - -Glob's logo created by Tanya Brassie , licensed -under a Creative Commons Attribution-ShareAlike 4.0 International License -https://creativecommons.org/licenses/by-sa/4.0/ - --------------------------------------------------------- --------------------------------------------------------- -graceful-fs 4.2.8 - ISC +graceful-fs 4.2.9 - ISC https://github.com/isaacs/node-graceful-fs#readme Copyright (c) Isaac Z. Schlueter, Ben Noordhuis, and Contributors @@ -459,7 +510,7 @@ PERFORMANCE OF THIS SOFTWARE. --------------------------------------------------------- -minimatch 3.0.4 - ISC +minimatch 3.1.2 - ISC https://github.com/isaacs/minimatch#readme Copyright (c) Isaac Z. Schlueter and Contributors @@ -481,6 +532,32 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +minimatch 5.1.0 - ISC +https://github.com/isaacs/minimatch#readme + +Copyright (c) 2011-2022 Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) 2011-2022 Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + --------------------------------------------------------- --------------------------------------------------------- @@ -585,7 +662,40 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. --------------------------------------------------------- -accepts 1.3.7 - MIT +abort-controller 3.0.0 - MIT +https://github.com/mysticatea/abort-controller#readme + +copyright 2015 Toru Nagashima +Copyright (c) 2017 Toru Nagashima + +MIT License + +Copyright (c) 2017 Toru Nagashima + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +accepts 1.3.8 - MIT https://github.com/jshttp/accepts#readme Copyright (c) 2014 Jonathan Ong @@ -618,78 +728,6 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - -ansi-styles 3.2.1 - MIT -https://github.com/chalk/ansi-styles#readme - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -ansi-styles 4.3.0 - MIT -https://github.com/chalk/ansi-styles#readme - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -args 5.0.1 - MIT -https://github.com/leo/args#readme - -Copyright (c) 2016 Leonard Lamprecht - -The MIT License (MIT) - -Copyright (c) 2016 Leonard Lamprecht - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - --------------------------------------------------------- --------------------------------------------------------- @@ -790,7 +828,39 @@ SOFTWARE. --------------------------------------------------------- -body-parser 1.19.1 - MIT +base64-js 1.5.1 - MIT +https://github.com/beatgammit/base64-js + +Copyright (c) 2014 Jameson Little + +The MIT License (MIT) + +Copyright (c) 2014 Jameson Little + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +body-parser 1.20.0 - MIT https://github.com/expressjs/body-parser#readme Copyright (c) 2014 Jonathan Ong @@ -859,7 +929,72 @@ SOFTWARE. --------------------------------------------------------- -bytes 3.1.1 - MIT +brace-expansion 2.0.1 - MIT +https://github.com/juliangruber/brace-expansion + +Copyright (c) 2013 Julian Gruber + +MIT License + +Copyright (c) 2013 Julian Gruber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +buffer 6.0.3 - MIT +https://github.com/feross/buffer + +Copyright (c) Feross Aboukhadijeh, and other contributors +Copyright (c) Feross Aboukhadijeh (http://feross.org), and other contributors + +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh, and other contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +bytes 3.1.2 - MIT https://github.com/visionmedia/bytes.js#readme Copyright (c) 2015 Jed Watson @@ -896,155 +1031,39 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -camelcase 5.0.0 - MIT -https://github.com/sindresorhus/camelcase#readme +call-bind 1.0.2 - MIT +https://github.com/ljharb/call-bind#readme -(c) Sindre Sorhus (https://sindresorhus.com) -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) 2020 Jordan Harband MIT License -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) 2020 Jordan Harband -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -camelcase 5.3.1 - MIT -https://github.com/sindresorhus/camelcase#readme - -(c) Sindre Sorhus (https://sindresorhus.com) -Copyright (c) Sindre Sorhus (sindresorhus.com) - -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -chalk 2.4.2 - MIT -https://github.com/chalk/chalk#readme - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -chalk 4.1.2 - MIT -https://github.com/chalk/chalk#readme - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -color-convert 1.9.3 - MIT -https://github.com/Qix-/color-convert#readme - -Copyright (c) 2011-2016, Heather Arthur and Josh Junon. -Copyright (c) 2011-2016 Heather Arthur - -Copyright (c) 2011-2016 Heather Arthur - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - ---------------------------------------------------------- - ---------------------------------------------------------- - -color-convert 2.0.1 - MIT -https://github.com/Qix-/color-convert#readme - -Copyright (c) 2011-2016, Heather Arthur and Josh Junon. -Copyright (c) 2011-2016 Heather Arthur - -Copyright (c) 2011-2016 Heather Arthur - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- -colorette 2.0.12 - MIT +colorette 2.0.19 - MIT https://github.com/jorgebucaran/colorette#readme Copyright (c) Jorge Bucaran @@ -1058,42 +1077,6 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - -color-name 1.1.3 - MIT -https://github.com/dfcreative/color-name - -Copyright (c) 2015 Dmitry Ivanov - -The MIT License (MIT) -Copyright (c) 2015 Dmitry Ivanov - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ---------------------------------------------------------- - ---------------------------------------------------------- - -color-name 1.1.4 - MIT -https://github.com/colorjs/color-name - -Copyright (c) 2015 Dmitry Ivanov - -The MIT License (MIT) -Copyright (c) 2015 Dmitry Ivanov - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - --------------------------------------------------------- --------------------------------------------------------- @@ -1192,7 +1175,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -cookie 0.4.1 - MIT +cookie 0.5.0 - MIT https://github.com/jshttp/cookie#readme Copyright (c) 2012-2014 Roman Shtylman @@ -1312,17 +1295,15 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -depd 1.1.2 - MIT +depd 2.0.0 - MIT https://github.com/dougwilson/nodejs-depd#readme -Copyright (c) 2014 Douglas Christopher Wilson Copyright (c) 2015 Douglas Christopher Wilson -Copyright (c) 2014-2015 Douglas Christopher Wilson -Copyright (c) 2014-2017 Douglas Christopher Wilson +Copyright (c) 2014-2018 Douglas Christopher Wilson (The MIT License) -Copyright (c) 2014-2017 Douglas Christopher Wilson +Copyright (c) 2014-2018 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -1348,16 +1329,19 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -destroy 1.0.4 - MIT -https://github.com/stream-utils/destroy +destroy 1.2.0 - MIT +https://github.com/stream-utils/destroy#readme Copyright (c) 2014 Jonathan Ong Copyright (c) 2014 Jonathan Ong me@jongleberry.com +Copyright (c) 2015-2022 Douglas Christopher Wilson +Copyright (c) 2015-2022 Douglas Christopher Wilson doug@somethingdoug.com The MIT License (MIT) Copyright (c) 2014 Jonathan Ong me@jongleberry.com +Copyright (c) 2015-2022 Douglas Christopher Wilson doug@somethingdoug.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -1548,94 +1532,107 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -escape-string-regexp 1.0.5 - MIT -https://github.com/sindresorhus/escape-string-regexp +etag 1.8.1 - MIT +https://github.com/jshttp/etag#readme -(c) Sindre Sorhus (http://sindresorhus.com) -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) 2014-2016 Douglas Christopher Wilson -The MIT License (MIT) +(The MIT License) -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) 2014-2016 Douglas Christopher Wilson -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- -escape-string-regexp 2.0.0 - MIT -https://github.com/sindresorhus/escape-string-regexp#readme +events 3.3.0 - MIT +https://github.com/Gozala/events#readme -(c) Sindre Sorhus (https://sindresorhus.com) -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright Joyent, Inc. and other Node contributors -MIT License +MIT -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright Joyent, Inc. and other Node contributors. -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the +following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- -etag 1.8.1 - MIT -https://github.com/jshttp/etag#readme +event-target-shim 5.0.1 - MIT +https://github.com/mysticatea/event-target-shim -Copyright (c) 2014-2016 Douglas Christopher Wilson +copyright 2015 Toru Nagashima +Copyright (c) 2015 Toru Nagashima -(The MIT License) +The MIT License (MIT) -Copyright (c) 2014-2016 Douglas Christopher Wilson +Copyright (c) 2015 Toru Nagashima -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- -express 4.17.2 - MIT +express 4.18.1 - MIT http://expressjs.com/ Copyright (c) 2013 Roman Shtylman @@ -1707,7 +1704,7 @@ SOFTWARE. --------------------------------------------------------- -express-validator 6.14.0 - MIT +express-validator 6.14.2 - MIT https://express-validator.github.io/ Copyright (c) 2010 Chris O'Hara @@ -1739,9 +1736,42 @@ THE SOFTWARE. --------------------------------------------------------- -fast-redact 3.0.2 - MIT +fast-copy 3.0.0 - MIT +https://github.com/planttheidea/fast-copy#readme + +Copyright (c) 2018 Tony Quetano + +MIT License + +Copyright (c) 2018 Tony Quetano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +fast-redact 3.1.1 - MIT https://github.com/davidmarkclements/fast-redact#readme +Copyright (c) 2019-2020 David Mark Clements The MIT License (MIT) @@ -1835,15 +1865,15 @@ THE SOFTWARE. --------------------------------------------------------- -finalhandler 1.1.2 - MIT +finalhandler 1.2.0 - MIT https://github.com/pillarjs/finalhandler#readme -Copyright (c) 2014-2017 Douglas Christopher Wilson -Copyright (c) 2014-2017 Douglas Christopher Wilson +Copyright (c) 2014-2022 Douglas Christopher Wilson +Copyright (c) 2014-2022 Douglas Christopher Wilson (The MIT License) -Copyright (c) 2014-2017 Douglas Christopher Wilson +Copyright (c) 2014-2022 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -1939,7 +1969,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -fs-extra 10.0.0 - MIT +fs-extra 10.1.0 - MIT https://github.com/jprichardson/node-fs-extra Copyright (c) 2011-2017 JP Richardson @@ -1968,49 +1998,168 @@ OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHE --------------------------------------------------------- -has-flag 3.0.0 - MIT -https://github.com/sindresorhus/has-flag#readme +function-bind 1.1.1 - MIT +https://github.com/Raynos/function-bind -(c) Sindre Sorhus (https://sindresorhus.com) -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) 2013 Raynos + +Copyright (c) 2013 Raynos. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + + +--------------------------------------------------------- + +--------------------------------------------------------- + +get-intrinsic 1.1.1 - MIT +https://github.com/ljharb/get-intrinsic#readme + +Copyright (c) 2020 Jordan Harband MIT License -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) 2020 Jordan Harband -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- -has-flag 4.0.0 - MIT -https://github.com/sindresorhus/has-flag#readme +has 1.0.3 - MIT +https://github.com/tarruda/has -(c) Sindre Sorhus (https://sindresorhus.com) -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) 2013 Thiago de Arruda + +Copyright (c) 2013 Thiago de Arruda + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +has-symbols 1.0.3 - MIT +https://github.com/ljharb/has-symbols#readme + +Copyright (c) 2016 Jordan Harband MIT License -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) 2016 Jordan Harband -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +help-me 4.1.0 - MIT +https://github.com/mcollina/help-me + +Copyright (c) 2014-2022 Matteo Collina + +The MIT License (MIT) + +Copyright (c) 2014-2022 Matteo Collina + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- -http-errors 1.8.1 - MIT +http-errors 2.0.0 - MIT https://github.com/jshttp/http-errors#readme Copyright (c) 2014 Jonathan Ong @@ -2050,7 +2199,7 @@ THE SOFTWARE. iconv-lite 0.4.24 - MIT https://github.com/ashtuchkin/iconv-lite -Copyright (c) Microsoft Corporation. +Copyright (c) Microsoft Corporation Copyright (c) 2011 Alexander Shtuchkin Copyright (c) 2011 Alexander Shtuchkin @@ -2083,7 +2232,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ipaddr.js 1.9.1 - MIT https://github.com/whitequark/ipaddr.js#readme -Copyright (c) 2011-2017 +Copyright (c) 2011-2017 whitequark Copyright (C) 2011-2017 whitequark @@ -2152,71 +2301,17 @@ Copyright (c) 2012-2015, JP Richardson Copyright (c) 2012-2015, JP Richardson -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files -(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, - merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, - ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -leven 2.1.0 - MIT -https://github.com/sindresorhus/leven#readme - -(c) Sindre Sorhus (https://sindresorhus.com) -Copyright (c) Sindre Sorhus (sindresorhus.com) - -The MIT License (MIT) - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -leven 3.1.0 - MIT -https://github.com/sindresorhus/leven#readme - -(c) Sindre Sorhus (https://sindresorhus.com) -Copyright (c) Sindre Sorhus (sindresorhus.com) - -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- @@ -2423,7 +2518,7 @@ THE SOFTWARE. --------------------------------------------------------- -mime-db 1.49.0 - MIT +mime-db 1.51.0 - MIT https://github.com/jshttp/mime-db#readme Copyright (c) 2014 Jonathan Ong @@ -2457,7 +2552,7 @@ THE SOFTWARE. --------------------------------------------------------- -mime-types 2.1.32 - MIT +mime-types 2.1.34 - MIT https://github.com/jshttp/mime-types#readme Copyright (c) 2014 Jonathan Ong @@ -2494,33 +2589,28 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -mri 1.1.4 - MIT -https://github.com/lukeed/mri#readme - -(c) Luke Edwards (https://lukeed.com) -Copyright (c) Luke Edwards (lukeed.com) +minimist 1.2.6 - MIT +https://github.com/substack/minimist -The MIT License (MIT) -Copyright (c) Luke Edwards (lukeed.com) +This software is released under the MIT license: -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- @@ -2562,6 +2652,7 @@ SOFTWARE. ms 2.1.3 - MIT https://github.com/vercel/ms#readme +Copyright (c) 2020 Vercel, Inc. The MIT License (MIT) @@ -2590,7 +2681,7 @@ SOFTWARE. --------------------------------------------------------- -negotiator 0.6.2 - MIT +negotiator 0.6.3 - MIT https://github.com/jshttp/negotiator#readme Copyright (c) 2012 Federico Romero @@ -2627,6 +2718,38 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +object-inspect 1.12.2 - MIT +https://github.com/inspect-js/object-inspect + +Copyright (c) 2013 James Halliday + +MIT License + +Copyright (c) 2013 James Halliday + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + --------------------------------------------------------- --------------------------------------------------------- @@ -2662,8 +2785,40 @@ SOFTWARE. --------------------------------------------------------- -on-finished 2.3.0 - MIT -https://github.com/jshttp/on-finished +on-exit-leak-free 2.1.0 - MIT +https://github.com/mcollina/on-exit-or-gc#readme + +Copyright (c) 2021 Matteo Collina + +MIT License + +Copyright (c) 2021 Matteo Collina + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +on-finished 2.4.1 - MIT +https://github.com/jshttp/on-finished#readme Copyright (c) 2013 Jonathan Ong Copyright (c) 2014 Douglas Christopher Wilson @@ -2737,15 +2892,14 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -path-is-absolute 1.0.1 - MIT -https://github.com/sindresorhus/path-is-absolute#readme +path-to-regexp 0.1.7 - MIT +https://github.com/component/path-to-regexp#readme -(c) Sindre Sorhus (https://sindresorhus.com) -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) The MIT License (MIT) -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -2770,14 +2924,17 @@ THE SOFTWARE. --------------------------------------------------------- -path-to-regexp 0.1.7 - MIT -https://github.com/component/path-to-regexp#readme +pino 7.11.0 - MIT +http://getpino.io/ -Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) +Copyright (c) 2016-2019 Matteo Collina, David Mark Clements and the Pino contributors The MIT License (MIT) -Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) +Copyright (c) 2016-2019 Matteo Collina, David Mark Clements and the Pino contributors + +Pino contributors listed at https://github.com/pinojs/pino#the-team and in +the README file. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -2786,26 +2943,25 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- -pino 7.6.4 - MIT -http://getpino.io/ +pino 8.6.1 - MIT +https://getpino.io/ -Copyright (c) 2016-2019 Matteo Collina, David Mark Clements and the Pino contributors The MIT License (MIT) @@ -2864,55 +3020,188 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- +--------------------------------------------------------- + +--------------------------------------------------------- + +pino-abstract-transport 1.0.0 - MIT +https://github.com/pinojs/pino-abstract-transport#readme + +Copyright (c) 2021 pino + +MIT License + +Copyright (c) 2021 pino + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +pino-http 6.2.0 - MIT +https://github.com/pinojs/pino-http#readme + +Copyright (c) 2016 Matteo Collina +Copyright (c) 2016 David Mark Clements + +The MIT License (MIT) + +Copyright (c) 2016 Matteo Collina +Copyright (c) 2016 David Mark Clements + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +pino-pretty 9.1.1 - MIT +https://github.com/pinojs/pino-pretty#readme + +Copyright (c) 2019 the Pino team + +The MIT License (MIT) + +Copyright (c) 2019 the Pino team + +Pino team listed at https://github.com/pinojs/pino#the-team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +pino-std-serializers 4.0.0 - MIT +https://github.com/pinojs/pino-std-serializers#readme + +Copyright Mateo Collina, David Mark Clements, James Sumners + +Copyright Mateo Collina, David Mark Clements, James Sumners + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +pino-std-serializers 6.0.0 - MIT +https://github.com/pinojs/pino-std-serializers#readme + +Copyright Mateo Collina, David Mark Clements, James Sumners + +Copyright Mateo Collina, David Mark Clements, James Sumners + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + --------------------------------------------------------- -pino-http 6.2.0 - MIT -https://github.com/pinojs/pino-http#readme +--------------------------------------------------------- -Copyright (c) 2016 Matteo Collina -Copyright (c) 2016 David Mark Clements +process 0.11.10 - MIT +https://github.com/shtylman/node-process#readme -The MIT License (MIT) +Copyright (c) 2013 Roman Shtylman -Copyright (c) 2016 Matteo Collina -Copyright (c) 2016 David Mark Clements +(The MIT License) +Copyright (c) 2013 Roman Shtylman -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- -pino-pretty 7.5.1 - MIT -https://github.com/pinojs/pino-pretty#readme - -Copyright (c) 2019 the Pino team +process-warning 1.0.0 - MIT +https://github.com/fastify/fastify-warning#readme -The MIT License (MIT) +Copyright (c) Fastify -Copyright (c) 2019 the Pino team +MIT License -Pino team listed at https://github.com/pinojs/pino#the-team +Copyright (c) Fastify Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -2937,25 +3226,7 @@ SOFTWARE. --------------------------------------------------------- -pino-std-serializers 4.0.0 - MIT -https://github.com/pinojs/pino-std-serializers#readme - -Copyright Mateo Collina, David Mark Clements, James Sumners - -Copyright Mateo Collina, David Mark Clements, James Sumners - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -process-warning 1.0.0 - MIT +process-warning 2.0.0 - MIT https://github.com/fastify/fastify-warning#readme Copyright (c) Fastify @@ -3151,18 +3422,18 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -raw-body 2.4.2 - MIT +raw-body 2.5.1 - MIT https://github.com/stream-utils/raw-body#readme Copyright (c) 2013-2014 Jonathan Ong -Copyright (c) 2014-2015 Douglas Christopher Wilson +Copyright (c) 2014-2022 Douglas Christopher Wilson Copyright (c) 2013-2014 Jonathan Ong -Copyright (c) 2014-2015 Douglas Christopher Wilson +Copyright (c) 2014-2022 Douglas Christopher Wilson The MIT License (MIT) Copyright (c) 2013-2014 Jonathan Ong -Copyright (c) 2014-2015 Douglas Christopher Wilson +Copyright (c) 2014-2022 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -3190,7 +3461,65 @@ THE SOFTWARE. readable-stream 3.6.0 - MIT https://github.com/nodejs/readable-stream#readme -Copyright Joyent, Inc. and other Node contributors. +Copyright Joyent, Inc. and other Node contributors + +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + + +--------------------------------------------------------- + +--------------------------------------------------------- + +readable-stream 4.2.0 - MIT +https://github.com/nodejs/readable-stream + +Copyright Joyent, Inc. and other Node contributors Node.js is licensed for use as follows: @@ -3300,26 +3629,33 @@ SOFTWARE. --------------------------------------------------------- -rfdc 1.3.0 - MIT -https://github.com/davidmarkclements/rfdc#readme +real-require 0.2.0 - MIT +https://github.com/pinojs/real-require -Copyright 2019 David Mark Clements +Copyright (c) 2021 Paolo Insogna +Copyright Paolo Insogna and real-require contributors 2021 -Copyright 2019 "David Mark Clements " +The MIT License (MIT) -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Copyright (c) 2021 Paolo Insogna and the real-require contributors -The above copyright notice and this permission notice shall be included in all copies or substantial portions -of the Software. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. --------------------------------------------------------- @@ -3424,7 +3760,7 @@ SOFTWARE. --------------------------------------------------------- -safe-stable-stringify 2.2.0 - MIT +safe-stable-stringify 2.3.1 - MIT https://github.com/BridgeAR/safe-stable-stringify#readme Copyright (c) Ruben Bridgewater @@ -3456,16 +3792,16 @@ SOFTWARE. --------------------------------------------------------- -send 0.17.2 - MIT +send 0.18.0 - MIT https://github.com/pillarjs/send#readme Copyright (c) 2012 TJ Holowaychuk -Copyright (c) 2014-2016 Douglas Christopher Wilson +Copyright (c) 2014-2022 Douglas Christopher Wilson (The MIT License) Copyright (c) 2012 TJ Holowaychuk -Copyright (c) 2014-2016 Douglas Christopher Wilson +Copyright (c) 2014-2022 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -3491,7 +3827,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -serve-static 1.14.2 - MIT +serve-static 1.15.0 - MIT https://github.com/expressjs/serve-static#readme Copyright (c) 2011 LearnBoost @@ -3526,6 +3862,38 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +side-channel 1.0.4 - MIT +https://github.com/ljharb/side-channel#readme + +Copyright (c) 2019 Jordan Harband + +MIT License + +Copyright (c) 2019 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + --------------------------------------------------------- --------------------------------------------------------- @@ -3562,7 +3930,39 @@ SOFTWARE. --------------------------------------------------------- -statuses 1.5.0 - MIT +sonic-boom 3.2.0 - MIT +https://github.com/pinojs/sonic-boom#readme + +Copyright (c) 2017 Matteo Collina + +MIT License + +Copyright (c) 2017 Matteo Collina + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--------------------------------------------------------- + +--------------------------------------------------------- + +statuses 2.0.1 - MIT https://github.com/jshttp/statuses#readme Copyright (c) 2014 Jonathan Ong @@ -3634,7 +4034,7 @@ THE SOFTWARE. string_decoder 1.3.0 - MIT https://github.com/nodejs/string_decoder -Copyright Joyent, Inc. and other Node contributors. +Copyright Joyent, Inc. and other Node contributors Node.js is licensed for use as follows: @@ -3710,49 +4110,42 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -supports-color 5.5.0 - MIT -https://github.com/chalk/supports-color#readme - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -supports-color 7.2.0 - MIT -https://github.com/chalk/supports-color#readme +thread-stream 0.15.2 - MIT +https://github.com/mcollina/thread-stream#readme -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) 2021 Matteo Collina MIT License -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) 2021 Matteo Collina -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- -thread-stream 0.13.0 - MIT +thread-stream 2.2.0 - MIT https://github.com/mcollina/thread-stream#readme +Copyright (c) 2021 Matteo Collina MIT License diff --git a/Samples/UniversalDecoder/app.decoder.js b/Samples/UniversalDecoder/app.decoder.js index ad50120916..51401ce180 100644 --- a/Samples/UniversalDecoder/app.decoder.js +++ b/Samples/UniversalDecoder/app.decoder.js @@ -1,11 +1,21 @@ 'use strict'; -const glob = require('glob'); -const path = require('path'); const {logger} = require('./app.logging'); +const decoders = (() => { + try { + return require('./codecs'); + } catch (e) { + if (e instanceof Error && e.code === 'MODULE_NOT_FOUND') { + return {}; + } else { + throw e; + } + } +})(); + function getAllDecoders() { - return glob.sync(`./codecs/**/*.js`).map(d => path.basename(d).split('.')[0]); + return Object.keys(decoders); } // gets decoder by name @@ -16,16 +26,11 @@ function getDecoder(decoderName) { decodeUplink: (input) => { return { data: input.bytes.join('') } } } } - - // search for codec in "codecs" directory - const files = glob.sync(`./codecs/**/${decoderName}.js`); - if (files.length === 0) { + const decoder = decoders[decoderName]; + if (!decoder) { throw new Error(`No codec found: ${decoderName}`); - } else if (files.length > 1) { - throw new Error(`Multiple codecs found: ${JSON.stringify(files)}`); } - - return require(files[0]); + return decoder; } function decode(decoderName, payload, fPort) { diff --git a/Samples/UniversalDecoder/package-lock.json b/Samples/UniversalDecoder/package-lock.json index f179c89347..c43690735c 100644 --- a/Samples/UniversalDecoder/package-lock.json +++ b/Samples/UniversalDecoder/package-lock.json @@ -1,5217 +1,61 @@ { "name": "universaldecoder", "version": "0.0.1", - "lockfileVersion": 2, + "lockfileVersion": 1, "requires": true, - "packages": { - "": { - "name": "universaldecoder", - "version": "0.0.1", - "license": "MIT", - "dependencies": { - "express": "^4.17.2", - "express-pino-logger": "^7.0.0", - "express-validator": "^6.14.0", - "fs-extra": "^10.0.0", - "glob": "^7.1.7", - "pino": "^7.6.4", - "pino-pretty": "^7.5.1" - }, - "devDependencies": { - "jest": "^27.4.7", - "supertest": "^6.2.2" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.16.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.4.tgz", - "integrity": "sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.7.tgz", - "integrity": "sha512-aeLaqcqThRNZYmbMqtulsetOQZ/5gbR/dWruUCJcpas4Qoyy+QeagfDsPdMrqwsPRDNxJvBlRiZxxX7THO7qtA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.16.7", - "@babel/helper-compilation-targets": "^7.16.7", - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helpers": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.1.2", - "semver": "^6.3.0", - "source-map": "^0.5.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/core/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 - }, - "node_modules/@babel/core/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@babel/generator": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.7.tgz", - "integrity": "sha512-/ST3Sg8MLGY5HVYmrjOgL60ENux/HfO/CsUh7y4MalThufhE/Ff/6EibFDHi4jiDCaWfJKoqbE6oTh21c5hrRg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz", - "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.16.4", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.17.5", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", - "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", - "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", - "dev": true, - "dependencies": { - "@babel/helper-get-function-arity": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-get-function-arity": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", - "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", - "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", - "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", - "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.16.7.tgz", - "integrity": "sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.7.tgz", - "integrity": "sha512-aKpPMfLvGO3Q97V0qhw/V2SWNWlwfJknuwAunU7wZLSfrM4xTBvg7E5opUVi1kJTBKihE38CPg4nBiqX83PWYw==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.7.tgz", - "integrity": "sha512-sR4eaSrnM7BV7QPzGfEX5paG/6wrZM3I0HDzfIAK06ESvo9oy3xBuVBxE3MbQaKNhvg8g/ixjMWo2CGpzpHsDA==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz", - "integrity": "sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.7.tgz", - "integrity": "sha512-8KWJPIb8c2VvY8AJrydh6+fVRo2ODx1wYBU2398xJVq0JomuLBZmVQzLPBblJgHIGYG4znCpUZUZ0Pt2vdmVYQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.16.7", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/traverse/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 - }, - "node_modules/@babel/types": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.7.tgz", - "integrity": "sha512-E8HuV7FO9qLpx6OtoGfUQ2cjIYnbFwvZWYBS+87EwtdMvmUPJSwykpovFB+8insbpF0uJcpr8KMUi64XZntZcg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "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==", - "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" - }, - "engines": { - "node": ">=8" - } - }, - "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/@jest/console": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.4.6.tgz", - "integrity": "sha512-jauXyacQD33n47A44KrlOVeiXHEXDqapSdfb9kTekOchH/Pd18kBIO1+xxJQRLuG+LUuljFCwTG92ra4NW7SpA==", - "dev": true, - "dependencies": { - "@jest/types": "^27.4.2", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^27.4.6", - "jest-util": "^27.4.2", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/core": { - "version": "27.4.7", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.4.7.tgz", - "integrity": "sha512-n181PurSJkVMS+kClIFSX/LLvw9ExSb+4IMtD6YnfxZVerw9ANYtW0bPrm0MJu2pfe9SY9FJ9FtQ+MdZkrZwjg==", - "dev": true, - "dependencies": { - "@jest/console": "^27.4.6", - "@jest/reporters": "^27.4.6", - "@jest/test-result": "^27.4.6", - "@jest/transform": "^27.4.6", - "@jest/types": "^27.4.2", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-changed-files": "^27.4.2", - "jest-config": "^27.4.7", - "jest-haste-map": "^27.4.6", - "jest-message-util": "^27.4.6", - "jest-regex-util": "^27.4.0", - "jest-resolve": "^27.4.6", - "jest-resolve-dependencies": "^27.4.6", - "jest-runner": "^27.4.6", - "jest-runtime": "^27.4.6", - "jest-snapshot": "^27.4.6", - "jest-util": "^27.4.2", - "jest-validate": "^27.4.6", - "jest-watcher": "^27.4.6", - "micromatch": "^4.0.4", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/environment": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.4.6.tgz", - "integrity": "sha512-E6t+RXPfATEEGVidr84WngLNWZ8ffCPky8RqqRK6u1Bn0LK92INe0MDttyPl/JOzaq92BmDzOeuqk09TvM22Sg==", - "dev": true, - "dependencies": { - "@jest/fake-timers": "^27.4.6", - "@jest/types": "^27.4.2", - "@types/node": "*", - "jest-mock": "^27.4.6" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.4.6.tgz", - "integrity": "sha512-mfaethuYF8scV8ntPpiVGIHQgS0XIALbpY2jt2l7wb/bvq4Q5pDLk4EP4D7SAvYT1QrPOPVZAtbdGAOOyIgs7A==", - "dev": true, - "dependencies": { - "@jest/types": "^27.4.2", - "@sinonjs/fake-timers": "^8.0.1", - "@types/node": "*", - "jest-message-util": "^27.4.6", - "jest-mock": "^27.4.6", - "jest-util": "^27.4.2" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.4.6.tgz", - "integrity": "sha512-kAiwMGZ7UxrgPzu8Yv9uvWmXXxsy0GciNejlHvfPIfWkSxChzv6bgTS3YqBkGuHcis+ouMFI2696n2t+XYIeFw==", - "dev": true, - "dependencies": { - "@jest/environment": "^27.4.6", - "@jest/types": "^27.4.2", - "expect": "^27.4.6" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.4.6.tgz", - "integrity": "sha512-+Zo9gV81R14+PSq4wzee4GC2mhAN9i9a7qgJWL90Gpx7fHYkWpTBvwWNZUXvJByYR9tAVBdc8VxDWqfJyIUrIQ==", - "dev": true, - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^27.4.6", - "@jest/test-result": "^27.4.6", - "@jest/transform": "^27.4.6", - "@jest/types": "^27.4.2", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.4", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-haste-map": "^27.4.6", - "jest-resolve": "^27.4.6", - "jest-util": "^27.4.2", - "jest-worker": "^27.4.6", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^4.0.1", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^8.1.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/source-map": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.4.0.tgz", - "integrity": "sha512-Ntjx9jzP26Bvhbm93z/AKcPRj/9wrkI88/gK60glXDx1q+IeI0rf7Lw2c89Ch6ofonB0On/iRDreQuQ6te9pgQ==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.4", - "source-map": "^0.6.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.4.6.tgz", - "integrity": "sha512-fi9IGj3fkOrlMmhQqa/t9xum8jaJOOAi/lZlm6JXSc55rJMXKHxNDN1oCP39B0/DhNOa2OMupF9BcKZnNtXMOQ==", - "dev": true, - "dependencies": { - "@jest/console": "^27.4.6", - "@jest/types": "^27.4.2", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.4.6.tgz", - "integrity": "sha512-3GL+nsf6E1PsyNsJuvPyIz+DwFuCtBdtvPpm/LMXVkBJbdFvQYCDpccYT56qq5BGniXWlE81n2qk1sdXfZebnw==", - "dev": true, - "dependencies": { - "@jest/test-result": "^27.4.6", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^27.4.6", - "jest-runtime": "^27.4.6" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.4.6.tgz", - "integrity": "sha512-9MsufmJC8t5JTpWEQJ0OcOOAXaH5ioaIX6uHVBLBMoCZPfKKQF+EqP8kACAvCZ0Y1h2Zr3uOccg8re+Dr5jxyw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.1.0", - "@jest/types": "^27.4.2", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^27.4.6", - "jest-regex-util": "^27.4.0", - "jest-util": "^27.4.2", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/types": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.4.2.tgz", - "integrity": "sha512-j35yw0PMTPpZsUoOBiuHzr1zTYoad1cVIE0ajEjcrJONxxrko/IRGKkXx3os0Nsi4Hu3+5VmDbVfq5WhG/pWAg==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", - "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@types/babel__core": { - "version": "7.1.18", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.18.tgz", - "integrity": "sha512-S7unDjm/C7z2A2R9NzfKCK1I+BAALDtxEmsJBwlB3EzNfb929ykjL++1CK9LO++EIp2fQrC8O+BwjKvz6UeDyQ==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.14.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.14.2.tgz", - "integrity": "sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.3.0" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", - "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/node": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz", - "integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==", - "dev": true - }, - "node_modules/@types/prettier": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.2.tgz", - "integrity": "sha512-ekoj4qOQYp7CvjX8ZDBgN86w3MqQhLE1hczEJbEIjgFEumDy+na/4AJAbLXfgEWFNB2pKadM5rPFtuSGMWK7xA==", - "dev": true - }, - "node_modules/@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true - }, - "node_modules/@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "20.2.1", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz", - "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", - "dev": true - }, - "node_modules/abab": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", - "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", - "dev": true - }, - "node_modules/accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "dependencies": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", - "dev": true, - "dependencies": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - } - }, - "node_modules/acorn-globals/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/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agent-base/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/agent-base/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 - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/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/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "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" - } - }, - "node_modules/args": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/args/-/args-5.0.1.tgz", - "integrity": "sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==", - "dependencies": { - "camelcase": "5.0.0", - "chalk": "2.4.2", - "leven": "2.1.0", - "mri": "1.1.4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/args/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/args/node_modules/camelcase": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", - "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/args/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/args/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/args/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "node_modules/args/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/args/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "engines": { - "node": ">=4" - } - }, - "node_modules/args/node_modules/leven": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", - "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/args/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", - "dev": true - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/babel-jest": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.4.6.tgz", - "integrity": "sha512-qZL0JT0HS1L+lOuH+xC2DVASR3nunZi/ozGhpgauJHgmI7f8rudxf6hUjEHympdQ/J64CdKmPkgfJ+A3U6QCrg==", - "dev": true, - "dependencies": { - "@jest/transform": "^27.4.6", - "@jest/types": "^27.4.2", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^27.4.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "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-jest-hoist": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.4.0.tgz", - "integrity": "sha512-Jcu7qS4OX5kTWBc45Hz7BMmgXuJqRnhatqpUhnzGC3OBYpOmf2tv6jFNwZpwM7wU7MUuv2r9IPS/ZlYOuburVw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.4.0.tgz", - "integrity": "sha512-NK4jGYpnBvNxcGo7/ZpZJr51jCGT+3bwwpVIDY2oNfTxJJldRtB4VAcYdgp1loDE50ODuTu+yBjpMAswv5tlpg==", - "dev": true, - "dependencies": { - "babel-plugin-jest-hoist": "^27.4.0", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "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==" - }, - "node_modules/body-parser": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.1.tgz", - "integrity": "sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA==", - "dependencies": { - "bytes": "3.1.1", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.8.1", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.9.6", - "raw-body": "2.4.2", - "type-is": "~1.6.18" - }, - "engines": { - "node": ">= 0.8" - } - }, - "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==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true - }, - "node_modules/browserslist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", - "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", - "dev": true, - "dependencies": { - "caniuse-lite": "^1.0.30001286", - "electron-to-chromium": "^1.4.17", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "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 - }, - "node_modules/bytes": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz", - "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "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.30001296", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001296.tgz", - "integrity": "sha512-WfrtPEoNSoeATDlf4y3QvkwiELl9GyPLISV5GejTbbQRtQx4LhsXmc9IQ6XCL2d7UxCyEzToEZNMeqR79OUw8Q==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } - }, - "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/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/ci-info": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz", - "integrity": "sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==", - "dev": true - }, - "node_modules/cjs-module-lexer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", - "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", - "dev": true - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true, - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", - "dev": true - }, - "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/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/colorette": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.12.tgz", - "integrity": "sha512-lHID0PU+NtFzeNCwTL6JzUKdb6kDpyEjrwTD1H0cDZswTbsjLh2wTV2Eo2sNZLc0oSg0a5W1AI4Nj7bX4iIdjA==" - }, - "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/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.1" - } - }, - "node_modules/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "node_modules/cookiejar": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", - "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true - }, - "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "dependencies": { - "cssom": "~0.3.6" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - }, - "node_modules/data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", - "dev": true, - "dependencies": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "engines": { - "node": "*" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decimal.js": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", - "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", - "dev": true - }, - "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", - "dev": true - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", - "dev": true, - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/diff-sequences": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.4.0.tgz", - "integrity": "sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww==", - "dev": true, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", - "dev": true, - "dependencies": { - "webidl-conversions": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/domexception/node_modules/webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/duplexify": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", - "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "node_modules/electron-to-chromium": { - "version": "1.4.36", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.36.tgz", - "integrity": "sha512-MbLlbF39vKrXWlFEFpCgDHwdlz4O3LmHM5W4tiLRHjSmEUXjJjz8sZkMgWgvYxlZw3N1iDTmCEtOkkESb5TMCg==", - "dev": true - }, - "node_modules/emittery": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", - "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "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/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "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==", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/expect/-/expect-27.4.6.tgz", - "integrity": "sha512-1M/0kAALIaj5LaG66sFJTbRsWTADnylly82cu4bspI0nl+pgP4E6Bh/aqdHlTUjul06K7xQnnrAoqfxVU0+/ag==", - "dev": true, - "dependencies": { - "@jest/types": "^27.4.2", - "jest-get-type": "^27.4.0", - "jest-matcher-utils": "^27.4.6", - "jest-message-util": "^27.4.6" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/express": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.2.tgz", - "integrity": "sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg==", - "dependencies": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.4.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.9.6", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.17.2", - "serve-static": "1.14.2", - "setprototypeof": "1.2.0", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express-pino-logger": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/express-pino-logger/-/express-pino-logger-7.0.0.tgz", - "integrity": "sha512-g8T6nhqq9L9AuwppymXa1rm6+A7xVUfkcEodXA+d2ILsM1uyoqSn83kpXE61v6JR2eFL8n878VyFDir1w2PuPw==", - "dependencies": { - "pino-http": "^6.0.0" - } - }, - "node_modules/express-validator": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.14.0.tgz", - "integrity": "sha512-ZWHJfnRgePp3FKRSKMtnZVnD1s8ZchWD+jSl7UMseGIqhweCo1Z9916/xXBbJAa6PrA3pUZfkOvIsHZG4ZtIMw==", - "dependencies": { - "lodash": "^4.17.21", - "validator": "^13.7.0" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/express/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "node_modules/fast-redact": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.0.2.tgz", - "integrity": "sha512-YN+CYfCVRVMUZOUPeinHNKgytM1wPI/C/UCLEi56EsY2dwwvI00kIJHJoI7pMVqGoMew8SMZ2SSfHKHULHXDsg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" - }, - "node_modules/fast-url-parser": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", - "integrity": "sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=", - "dependencies": { - "punycode": "^1.3.2" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", - "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", - "dev": true, - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "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==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formidable": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", - "integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==", - "dev": true, - "dependencies": { - "dezalgo": "1.0.3", - "hexoid": "1.0.0", - "once": "1.4.0", - "qs": "6.9.3" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/formidable/node_modules/qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", - "dev": true, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", - "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - }, - "funding": { - "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", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "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" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "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/has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hexoid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", - "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", - "dev": true, - "dependencies": { - "whatwg-encoding": "^1.0.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dev": true, - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http-proxy-agent/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/http-proxy-agent/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 - }, - "node_modules/https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/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 - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/import-local": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.3.tgz", - "integrity": "sha512-bE9iaUY3CXH8Cwfan/abDKAxe1KGT9kyGsBPqf6DMK/z0a2OzAsrukeYNgIH6cH5Xr452jb1TUL8rSfCLjZ9uA==", - "dev": true, - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz", - "integrity": "sha512-czwUz525rkOFDJxfKK6mYfIs9zBKILyrZQxjz3ABhjQXhbhFsSbo1HW/BFcsDnfJYJWA6thRR5/TUY2qs5W99Q==", - "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-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.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/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/istanbul-lib-source-maps/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 - }, - "node_modules/istanbul-reports": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.3.tgz", - "integrity": "sha512-x9LtDVtfm/t1GFiLl3NffC7hz+I1ragvgX1P/Lg1NlIagifZDKUkuuaAxH/qpwj2IuEfD8G2Bs/UKp+sZ/pKkg==", - "dev": true, - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest": { - "version": "27.4.7", - "resolved": "https://registry.npmjs.org/jest/-/jest-27.4.7.tgz", - "integrity": "sha512-8heYvsx7nV/m8m24Vk26Y87g73Ba6ueUd0MWed/NXMhSZIm62U/llVbS0PJe1SHunbyXjJ/BqG1z9bFjGUIvTg==", - "dev": true, - "dependencies": { - "@jest/core": "^27.4.7", - "import-local": "^3.0.2", - "jest-cli": "^27.4.7" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.4.2.tgz", - "integrity": "sha512-/9x8MjekuzUQoPjDHbBiXbNEBauhrPU2ct7m8TfCg69ywt1y/N+yYwGh3gCpnqUS3klYWDU/lSNgv+JhoD2k1A==", - "dev": true, - "dependencies": { - "@jest/types": "^27.4.2", - "execa": "^5.0.0", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-circus": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.4.6.tgz", - "integrity": "sha512-UA7AI5HZrW4wRM72Ro80uRR2Fg+7nR0GESbSI/2M+ambbzVuA63mn5T1p3Z/wlhntzGpIG1xx78GP2YIkf6PhQ==", - "dev": true, - "dependencies": { - "@jest/environment": "^27.4.6", - "@jest/test-result": "^27.4.6", - "@jest/types": "^27.4.2", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "expect": "^27.4.6", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.4.6", - "jest-matcher-utils": "^27.4.6", - "jest-message-util": "^27.4.6", - "jest-runtime": "^27.4.6", - "jest-snapshot": "^27.4.6", - "jest-util": "^27.4.2", - "pretty-format": "^27.4.6", - "slash": "^3.0.0", - "stack-utils": "^2.0.3", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-cli": { - "version": "27.4.7", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.4.7.tgz", - "integrity": "sha512-zREYhvjjqe1KsGV15mdnxjThKNDgza1fhDT+iUsXWLCq3sxe9w5xnvyctcYVT5PcdLSjv7Y5dCwTS3FCF1tiuw==", - "dev": true, - "dependencies": { - "@jest/core": "^27.4.7", - "@jest/test-result": "^27.4.6", - "@jest/types": "^27.4.2", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "import-local": "^3.0.2", - "jest-config": "^27.4.7", - "jest-util": "^27.4.2", - "jest-validate": "^27.4.6", - "prompts": "^2.0.1", - "yargs": "^16.2.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "27.4.7", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.4.7.tgz", - "integrity": "sha512-xz/o/KJJEedHMrIY9v2ParIoYSrSVY6IVeE4z5Z3i101GoA5XgfbJz+1C8EYPsv7u7f39dS8F9v46BHDhn0vlw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.8.0", - "@jest/test-sequencer": "^27.4.6", - "@jest/types": "^27.4.2", - "babel-jest": "^27.4.6", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.4", - "jest-circus": "^27.4.6", - "jest-environment-jsdom": "^27.4.6", - "jest-environment-node": "^27.4.6", - "jest-get-type": "^27.4.0", - "jest-jasmine2": "^27.4.6", - "jest-regex-util": "^27.4.0", - "jest-resolve": "^27.4.6", - "jest-runner": "^27.4.6", - "jest-util": "^27.4.2", - "jest-validate": "^27.4.6", - "micromatch": "^4.0.4", - "pretty-format": "^27.4.6", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.4.6.tgz", - "integrity": "sha512-zjaB0sh0Lb13VyPsd92V7HkqF6yKRH9vm33rwBt7rPYrpQvS1nCvlIy2pICbKta+ZjWngYLNn4cCK4nyZkjS/w==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^27.4.0", - "jest-get-type": "^27.4.0", - "pretty-format": "^27.4.6" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.4.0.tgz", - "integrity": "sha512-7TBazUdCKGV7svZ+gh7C8esAnweJoG+SvcF6Cjqj4l17zA2q1cMwx2JObSioubk317H+cjcHgP+7fTs60paulg==", - "dev": true, - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-each": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.4.6.tgz", - "integrity": "sha512-n6QDq8y2Hsmn22tRkgAk+z6MCX7MeVlAzxmZDshfS2jLcaBlyhpF3tZSJLR+kXmh23GEvS0ojMR8i6ZeRvpQcA==", - "dev": true, - "dependencies": { - "@jest/types": "^27.4.2", - "chalk": "^4.0.0", - "jest-get-type": "^27.4.0", - "jest-util": "^27.4.2", - "pretty-format": "^27.4.6" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-jsdom": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.4.6.tgz", - "integrity": "sha512-o3dx5p/kHPbUlRvSNjypEcEtgs6LmvESMzgRFQE6c+Prwl2JLA4RZ7qAnxc5VM8kutsGRTB15jXeeSbJsKN9iA==", - "dev": true, - "dependencies": { - "@jest/environment": "^27.4.6", - "@jest/fake-timers": "^27.4.6", - "@jest/types": "^27.4.2", - "@types/node": "*", - "jest-mock": "^27.4.6", - "jest-util": "^27.4.2", - "jsdom": "^16.6.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.4.6.tgz", - "integrity": "sha512-yfHlZ9m+kzTKZV0hVfhVu6GuDxKAYeFHrfulmy7Jxwsq4V7+ZK7f+c0XP/tbVDMQW7E4neG2u147hFkuVz0MlQ==", - "dev": true, - "dependencies": { - "@jest/environment": "^27.4.6", - "@jest/fake-timers": "^27.4.6", - "@jest/types": "^27.4.2", - "@types/node": "*", - "jest-mock": "^27.4.6", - "jest-util": "^27.4.2" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.4.0.tgz", - "integrity": "sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ==", - "dev": true, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.4.6.tgz", - "integrity": "sha512-0tNpgxg7BKurZeFkIOvGCkbmOHbLFf4LUQOxrQSMjvrQaQe3l6E8x6jYC1NuWkGo5WDdbr8FEzUxV2+LWNawKQ==", - "dev": true, - "dependencies": { - "@jest/types": "^27.4.2", - "@types/graceful-fs": "^4.1.2", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.4", - "jest-regex-util": "^27.4.0", - "jest-serializer": "^27.4.0", - "jest-util": "^27.4.2", - "jest-worker": "^27.4.6", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-jasmine2": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.4.6.tgz", - "integrity": "sha512-uAGNXF644I/whzhsf7/qf74gqy9OuhvJ0XYp8SDecX2ooGeaPnmJMjXjKt0mqh1Rl5dtRGxJgNrHlBQIBfS5Nw==", - "dev": true, - "dependencies": { - "@jest/environment": "^27.4.6", - "@jest/source-map": "^27.4.0", - "@jest/test-result": "^27.4.6", - "@jest/types": "^27.4.2", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "expect": "^27.4.6", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.4.6", - "jest-matcher-utils": "^27.4.6", - "jest-message-util": "^27.4.6", - "jest-runtime": "^27.4.6", - "jest-snapshot": "^27.4.6", - "jest-util": "^27.4.2", - "pretty-format": "^27.4.6", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-leak-detector": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.4.6.tgz", - "integrity": "sha512-kkaGixDf9R7CjHm2pOzfTxZTQQQ2gHTIWKY/JZSiYTc90bZp8kSZnUMS3uLAfwTZwc0tcMRoEX74e14LG1WapA==", - "dev": true, - "dependencies": { - "jest-get-type": "^27.4.0", - "pretty-format": "^27.4.6" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.4.6.tgz", - "integrity": "sha512-XD4PKT3Wn1LQnRAq7ZsTI0VRuEc9OrCPFiO1XL7bftTGmfNF0DcEwMHRgqiu7NGf8ZoZDREpGrCniDkjt79WbA==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^27.4.6", - "jest-get-type": "^27.4.0", - "pretty-format": "^27.4.6" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.4.6.tgz", - "integrity": "sha512-0p5szriFU0U74czRSFjH6RyS7UYIAkn/ntwMuOwTGWrQIOh5NzXXrq72LOqIkJKKvFbPq+byZKuBz78fjBERBA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^27.4.2", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "micromatch": "^4.0.4", - "pretty-format": "^27.4.6", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-mock": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.4.6.tgz", - "integrity": "sha512-kvojdYRkst8iVSZ1EJ+vc1RRD9llueBjKzXzeCytH3dMM7zvPV/ULcfI2nr0v0VUgm3Bjt3hBCQvOeaBz+ZTHw==", - "dev": true, - "dependencies": { - "@jest/types": "^27.4.2", - "@types/node": "*" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true, - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.4.0.tgz", - "integrity": "sha512-WeCpMpNnqJYMQoOjm1nTtsgbR4XHAk1u00qDoNBQoykM280+/TmgA5Qh5giC1ecy6a5d4hbSsHzpBtu5yvlbEg==", - "dev": true, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.4.6.tgz", - "integrity": "sha512-SFfITVApqtirbITKFAO7jOVN45UgFzcRdQanOFzjnbd+CACDoyeX7206JyU92l4cRr73+Qy/TlW51+4vHGt+zw==", - "dev": true, - "dependencies": { - "@jest/types": "^27.4.2", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^27.4.6", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^27.4.2", - "jest-validate": "^27.4.6", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.4.6.tgz", - "integrity": "sha512-W85uJZcFXEVZ7+MZqIPCscdjuctruNGXUZ3OHSXOfXR9ITgbUKeHj+uGcies+0SsvI5GtUfTw4dY7u9qjTvQOw==", - "dev": true, - "dependencies": { - "@jest/types": "^27.4.2", - "jest-regex-util": "^27.4.0", - "jest-snapshot": "^27.4.6" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runner": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.4.6.tgz", - "integrity": "sha512-IDeFt2SG4DzqalYBZRgbbPmpwV3X0DcntjezPBERvnhwKGWTW7C5pbbA5lVkmvgteeNfdd/23gwqv3aiilpYPg==", - "dev": true, - "dependencies": { - "@jest/console": "^27.4.6", - "@jest/environment": "^27.4.6", - "@jest/test-result": "^27.4.6", - "@jest/transform": "^27.4.6", - "@jest/types": "^27.4.2", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-docblock": "^27.4.0", - "jest-environment-jsdom": "^27.4.6", - "jest-environment-node": "^27.4.6", - "jest-haste-map": "^27.4.6", - "jest-leak-detector": "^27.4.6", - "jest-message-util": "^27.4.6", - "jest-resolve": "^27.4.6", - "jest-runtime": "^27.4.6", - "jest-util": "^27.4.2", - "jest-worker": "^27.4.6", - "source-map-support": "^0.5.6", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.4.6.tgz", - "integrity": "sha512-eXYeoR/MbIpVDrjqy5d6cGCFOYBFFDeKaNWqTp0h6E74dK0zLHzASQXJpl5a2/40euBmKnprNLJ0Kh0LCndnWQ==", - "dev": true, - "dependencies": { - "@jest/environment": "^27.4.6", - "@jest/fake-timers": "^27.4.6", - "@jest/globals": "^27.4.6", - "@jest/source-map": "^27.4.0", - "@jest/test-result": "^27.4.6", - "@jest/transform": "^27.4.6", - "@jest/types": "^27.4.2", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^27.4.6", - "jest-message-util": "^27.4.6", - "jest-mock": "^27.4.6", - "jest-regex-util": "^27.4.0", - "jest-resolve": "^27.4.6", - "jest-snapshot": "^27.4.6", - "jest-util": "^27.4.2", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-serializer": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.4.0.tgz", - "integrity": "sha512-RDhpcn5f1JYTX2pvJAGDcnsNTnsV9bjYPU8xcV+xPwOXnUPOQwf4ZEuiU6G9H1UztH+OapMgu/ckEVwO87PwnQ==", - "dev": true, - "dependencies": { - "@types/node": "*", - "graceful-fs": "^4.2.4" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.4.6.tgz", - "integrity": "sha512-fafUCDLQfzuNP9IRcEqaFAMzEe7u5BF7mude51wyWv7VRex60WznZIC7DfKTgSIlJa8aFzYmXclmN328aqSDmQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.7.2", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.0.0", - "@jest/transform": "^27.4.6", - "@jest/types": "^27.4.2", - "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^27.4.6", - "graceful-fs": "^4.2.4", - "jest-diff": "^27.4.6", - "jest-get-type": "^27.4.0", - "jest-haste-map": "^27.4.6", - "jest-matcher-utils": "^27.4.6", - "jest-message-util": "^27.4.6", - "jest-util": "^27.4.2", - "natural-compare": "^1.4.0", - "pretty-format": "^27.4.6", - "semver": "^7.3.2" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-util": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.4.2.tgz", - "integrity": "sha512-YuxxpXU6nlMan9qyLuxHaMMOzXAl5aGZWCSzben5DhLHemYQxCc4YK+4L3ZrCutT8GPQ+ui9k5D8rUJoDioMnA==", - "dev": true, - "dependencies": { - "@jest/types": "^27.4.2", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.4", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-validate": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.4.6.tgz", - "integrity": "sha512-872mEmCPVlBqbA5dToC57vA3yJaMRfIdpCoD3cyHWJOMx+SJwLNw0I71EkWs41oza/Er9Zno9XuTkRYCPDUJXQ==", - "dev": true, - "dependencies": { - "@jest/types": "^27.4.2", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^27.4.0", - "leven": "^3.1.0", - "pretty-format": "^27.4.6" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.4.6.tgz", - "integrity": "sha512-yKQ20OMBiCDigbD0quhQKLkBO+ObGN79MO4nT7YaCuQ5SM+dkBNWE8cZX0FjU6czwMvWw6StWbe+Wv4jJPJ+fw==", - "dev": true, - "dependencies": { - "@jest/test-result": "^27.4.6", - "@jest/types": "^27.4.2", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "jest-util": "^27.4.2", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-worker": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.6.tgz", - "integrity": "sha512-gHWJF/6Xi5CTG5QCvROr6GcmpIqNYpDJyc8A1h/DyXqH1tD6SnRCM0d3U5msV31D2LB/U+E0M+W4oyvKV44oNw==", - "dev": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "engines": { - "node": ">=10" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "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==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", - "dev": true, - "dependencies": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.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==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "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/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", - "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.32", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", - "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", - "dependencies": { - "mime-db": "1.49.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "node_modules/mri": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", - "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==", - "engines": { - "node": ">=4" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "node_modules/negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node_modules/node-releases": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", - "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", - "dev": true - }, - "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/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/nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", - "dev": true - }, - "node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-exit-leak-free": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", - "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==" - }, - "node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dependencies": { - "wrappy": "1" - } - }, - "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/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.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==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "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==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "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, - "engines": { - "node": ">=8" - } - }, - "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": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "engines": { - "node": ">=0.10.0" - } - }, - "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, - "engines": { - "node": ">=8" - } - }, - "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-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "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/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pino": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/pino/-/pino-7.6.4.tgz", - "integrity": "sha512-ktibPg3ttWONxYQ2Efk1zYbIvofD5zdd/ReoujK84ggEp0REflb9TsXavSjt8u1CdT2mMJe9QQ3ZpyOQxUKipA==", - "dependencies": { - "fast-redact": "^3.0.0", - "on-exit-leak-free": "^0.2.0", - "pino-abstract-transport": "v0.5.0", - "pino-std-serializers": "^4.0.0", - "process-warning": "^1.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.1.0", - "safe-stable-stringify": "^2.1.0", - "sonic-boom": "^2.2.1", - "thread-stream": "^0.13.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz", - "integrity": "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==", - "dependencies": { - "duplexify": "^4.1.2", - "split2": "^4.0.0" - } - }, - "node_modules/pino-http": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-6.2.0.tgz", - "integrity": "sha512-3RrKWiyfR9lvIndc1AQoj3hI7mOS5hYHG/ZQYj6R7eu/J1OYOLk7o6ZQwiatEiNIsgqHT/6FPjp3NCUPohvq0g==", - "dependencies": { - "fast-url-parser": "^1.1.3", - "get-caller-file": "^2.0.5", - "pino": "^7.0.5", - "pino-std-serializers": "^4.0.0" - } - }, - "node_modules/pino-pretty": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-7.5.1.tgz", - "integrity": "sha512-xEOUJiokdBGcZ9d0v7OY6SqEp+rrVH2drE3bHOUsK8elw44eh9V83InZqeL1dFwgD1IDnd6crUoec3hIXxfdBQ==", - "dependencies": { - "args": "^5.0.1", - "colorette": "^2.0.7", - "dateformat": "^4.6.3", - "fast-safe-stringify": "^2.0.7", - "joycon": "^3.1.1", - "pino-abstract-transport": "^0.5.0", - "pump": "^3.0.0", - "readable-stream": "^3.6.0", - "rfdc": "^1.3.0", - "secure-json-parse": "^2.4.0", - "sonic-boom": "^2.2.0", - "strip-json-comments": "^3.1.1" - }, - "bin": { - "pino-pretty": "bin.js" - } - }, - "node_modules/pino-std-serializers": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", - "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==" - }, - "node_modules/pirates": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.4.tgz", - "integrity": "sha512-ZIrVPH+A52Dw84R0L3/VS9Op04PuQ2SEoJL6bkshmiTic/HldyW9Tf7oH5mhJZBK7NmDx27vSMrYEXPXclpDKw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/pretty-format": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.4.6.tgz", - "integrity": "sha512-NblstegA1y/RJW2VyML+3LlpFjzx62cUrtBIKIWDXEDkjNeleA7Od7nrzcs/VLQvAeV4CgSYhrN39DRN88Qi/g==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.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==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "dev": true - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "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": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "node_modules/qs": { - "version": "6.9.6", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz", - "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==", - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz", - "integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==", - "dependencies": { - "bytes": "3.1.1", - "http-errors": "1.8.1", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/real-require": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.1.0.tgz", - "integrity": "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", - "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", - "dev": true, - "dependencies": { - "is-core-module": "^2.8.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve.exports": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", - "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/safe-stable-stringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.2.0.tgz", - "integrity": "sha512-C6AuMdYPuPV/P1leplHNu0lgc2LAElq/g3TdoksDCIVtBhr78o/CH03bt/9SKqugFbKU9CUjsNlCu0fjtQzQUw==", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dev": true, - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/secure-json-parse": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.4.0.tgz", - "integrity": "sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg==" - }, - "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "0.17.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", - "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", - "dependencies": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "1.8.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "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==" - }, - "node_modules/serve-static": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", - "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", - "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", - "dev": true - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/sonic-boom": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.3.0.tgz", - "integrity": "sha512-lEPaw654/4/rCJHz/TNzV4GIthqCq4inO+O3aFhbdOvR1bE+2//sVkcS+xlqPdb8gdjQCEE0hE9BuvnVixbnWQ==", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "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/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/split2": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", - "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "node_modules/stack-utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", - "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" - }, - "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==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "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/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/superagent": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.1.tgz", - "integrity": "sha512-CQ2weSS6M+doIwwYFoMatklhRbx6sVNdB99OEJ5czcP3cng76Ljqus694knFWgOj3RkrtxZqIgpe6vhe0J7QWQ==", - "dev": true, - "dependencies": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.3", - "debug": "^4.3.3", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.0.1", - "methods": "^1.1.2", - "mime": "^2.5.0", - "qs": "^6.10.1", - "readable-stream": "^3.6.0", - "semver": "^7.3.5" - }, - "engines": { - "node": ">=6.4.0 <13 || >=14" - } - }, - "node_modules/superagent/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/superagent/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/superagent/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/superagent/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 - }, - "node_modules/superagent/node_modules/qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", - "dev": true, - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/superagent/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/supertest": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.2.2.tgz", - "integrity": "sha512-wCw9WhAtKJsBvh07RaS+/By91NNE0Wh0DN19/hWPlBOU8tAfOtbZoVSV4xXeoKoxgPx0rx2y+y+8660XtE7jzg==", - "dev": true, - "dependencies": { - "methods": "^1.1.2", - "superagent": "^7.1.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "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/supports-hyperlinks": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", - "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, - "node_modules/terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/thread-stream": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.13.0.tgz", - "integrity": "sha512-kTMZeX4Dzlb1zZ00/01aerGaTw2i8NE4sWF0TvF1uXewRhCiUjCvatQkvxIvFqauWG2ADFS2Wpd3qBeYL9i3dg==", - "dependencies": { - "real-require": "^0.1.0" - } - }, - "node_modules/throat": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", - "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", - "dev": true - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", - "dev": true, - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.1.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/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, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dev": true, - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tr46/node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/v8-to-istanbul": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.0.tgz", - "integrity": "sha512-/PRhfd8aTNp9Ggr62HPzXg2XasNFGy5PBt0Rp04du7/8GNNSgxFL6WBTkgMKSL9bFjH+8kKEG3f37FmxiTqUUA==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/v8-to-istanbul/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/validator": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", - "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "dev": true, - "dependencies": { - "browser-process-hrtime": "^1.0.0" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", - "dev": true, - "dependencies": { - "xml-name-validator": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true, - "engines": { - "node": ">=10.4" - } - }, - "node_modules/whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "dependencies": { - "iconv-lite": "0.4.24" - } - }, - "node_modules/whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "node_modules/whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dev": true, - "dependencies": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "node_modules/ws": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", - "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==", - "dev": true, - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "node_modules/xmlchars": { + "dependencies": { + "@ampproject/remapping": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "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/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" } }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - } - }, - "dependencies": { "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", "dev": true, "requires": { - "@babel/highlight": "^7.16.7" + "@babel/highlight": "^7.18.6" } }, "@babel/compat-data": { - "version": "7.16.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.4.tgz", - "integrity": "sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q==", + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.3.tgz", + "integrity": "sha512-prBHMK4JYYK+wDjJF1q99KK4JLL+egWS4nmNqdlMUgCExMZ+iZW0hGhyC3VEbsPjvaN0TBhW//VIFwBrk8sEiw==", "dev": true }, "@babel/core": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.7.tgz", - "integrity": "sha512-aeLaqcqThRNZYmbMqtulsetOQZ/5gbR/dWruUCJcpas4Qoyy+QeagfDsPdMrqwsPRDNxJvBlRiZxxX7THO7qtA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.16.7", - "@babel/helper-compilation-targets": "^7.16.7", - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helpers": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7", + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", + "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.3", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.0", + "@babel/helpers": "^7.19.0", + "@babel/parser": "^7.19.3", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.3", + "@babel/types": "^7.19.3", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.1.2", - "semver": "^6.3.0", - "source-map": "^0.5.0" + "json5": "^2.2.1", + "semver": "^6.3.0" }, "dependencies": { "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -5222,163 +66,155 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true } } }, "@babel/generator": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.7.tgz", - "integrity": "sha512-/ST3Sg8MLGY5HVYmrjOgL60ENux/HfO/CsUh7y4MalThufhE/Ff/6EibFDHi4jiDCaWfJKoqbE6oTh21c5hrRg==", + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.3.tgz", + "integrity": "sha512-fqVZnmp1ncvZU757UzDheKZpfPgatqY59XtW2/j/18H7u76akb8xqvjw82f+i2UKd/ksYsSick/BCLQUUtJ/qQ==", "dev": true, "requires": { - "@babel/types": "^7.16.7", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" + "@babel/types": "^7.19.3", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" }, "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } } } }, "@babel/helper-compilation-targets": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz", - "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz", + "integrity": "sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==", "dev": true, "requires": { - "@babel/compat-data": "^7.16.4", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.17.5", + "@babel/compat-data": "^7.19.3", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", "semver": "^6.3.0" } }, "@babel/helper-environment-visitor": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", - "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true }, "@babel/helper-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", - "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", - "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", + "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", "dev": true, "requires": { - "@babel/types": "^7.16.7" + "@babel/template": "^7.18.10", + "@babel/types": "^7.19.0" } }, "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", "dev": true, "requires": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.18.6" } }, "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", "dev": true, "requires": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.18.6" } }, "@babel/helper-module-transforms": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", - "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz", + "integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.18.6", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.0", + "@babel/types": "^7.19.0" } }, "@babel/helper-plugin-utils": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", - "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz", + "integrity": "sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==", "dev": true }, "@babel/helper-simple-access": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", - "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz", + "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==", "dev": true, "requires": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.18.6" } }, "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", "dev": true, "requires": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.18.6" } }, + "@babel/helper-string-parser": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", + "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==", + "dev": true + }, "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", "dev": true }, "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", "dev": true }, "@babel/helpers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.16.7.tgz", - "integrity": "sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.0.tgz", + "integrity": "sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg==", "dev": true, "requires": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.0", + "@babel/types": "^7.19.0" } }, "@babel/highlight": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.7.tgz", - "integrity": "sha512-aKpPMfLvGO3Q97V0qhw/V2SWNWlwfJknuwAunU7wZLSfrM4xTBvg7E5opUVi1kJTBKihE38CPg4nBiqX83PWYw==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.16.7", + "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -5415,19 +251,13 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true }, "supports-color": { @@ -5442,9 +272,9 @@ } }, "@babel/parser": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.7.tgz", - "integrity": "sha512-sR4eaSrnM7BV7QPzGfEX5paG/6wrZM3I0HDzfIAK06ESvo9oy3xBuVBxE3MbQaKNhvg8g/ixjMWo2CGpzpHsDA==", + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.3.tgz", + "integrity": "sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -5492,6 +322,15 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, + "@babel/plugin-syntax-jsx": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", + "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, "@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", @@ -5556,47 +395,47 @@ } }, "@babel/plugin-syntax-typescript": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz", - "integrity": "sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz", + "integrity": "sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.16.7" + "@babel/helper-plugin-utils": "^7.18.6" } }, "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", "dev": true, "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.18.10", + "@babel/types": "^7.18.10" } }, "@babel/traverse": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.7.tgz", - "integrity": "sha512-8KWJPIb8c2VvY8AJrydh6+fVRo2ODx1wYBU2398xJVq0JomuLBZmVQzLPBblJgHIGYG4znCpUZUZ0Pt2vdmVYQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.16.7", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7", + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.3.tgz", + "integrity": "sha512-qh5yf6149zhq2sgIXmwjnsvmnNQC2iw70UFjp4olxucKrWd/dvlUsBI88VSLUsnMNF7/vnOiA+nk1+yLoCqROQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.3", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.19.3", + "@babel/types": "^7.19.3", "debug": "^4.1.0", "globals": "^11.1.0" }, "dependencies": { "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -5611,12 +450,13 @@ } }, "@babel/types": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.7.tgz", - "integrity": "sha512-E8HuV7FO9qLpx6OtoGfUQ2cjIYnbFwvZWYBS+87EwtdMvmUPJSwykpovFB+8insbpF0uJcpr8KMUi64XZntZcg==", + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.3.tgz", + "integrity": "sha512-hGCaQzIY22DJlDh9CH7NOxgKkFjBk0Cw9xDO1Xmh2151ti7wiGfQ3LauXzL4HP1fmFlTX6XjpRETTpUcv7wQLw==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.16.7", + "@babel/helper-string-parser": "^7.18.10", + "@babel/helper-validator-identifier": "^7.19.1", "to-fast-properties": "^2.0.0" } }, @@ -5646,196 +486,286 @@ "dev": true }, "@jest/console": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.4.6.tgz", - "integrity": "sha512-jauXyacQD33n47A44KrlOVeiXHEXDqapSdfb9kTekOchH/Pd18kBIO1+xxJQRLuG+LUuljFCwTG92ra4NW7SpA==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.1.2.tgz", + "integrity": "sha512-ujEBCcYs82BTmRxqfHMQggSlkUZP63AE5YEaTPj7eFyJOzukkTorstOUC7L6nE3w5SYadGVAnTsQ/ZjTGL0qYQ==", "dev": true, "requires": { - "@jest/types": "^27.4.2", + "@jest/types": "^29.1.2", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^27.4.6", - "jest-util": "^27.4.2", + "jest-message-util": "^29.1.2", + "jest-util": "^29.1.2", "slash": "^3.0.0" } }, "@jest/core": { - "version": "27.4.7", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.4.7.tgz", - "integrity": "sha512-n181PurSJkVMS+kClIFSX/LLvw9ExSb+4IMtD6YnfxZVerw9ANYtW0bPrm0MJu2pfe9SY9FJ9FtQ+MdZkrZwjg==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.1.2.tgz", + "integrity": "sha512-sCO2Va1gikvQU2ynDN8V4+6wB7iVrD2CvT0zaRst4rglf56yLly0NQ9nuRRAWFeimRf+tCdFsb1Vk1N9LrrMPA==", "dev": true, "requires": { - "@jest/console": "^27.4.6", - "@jest/reporters": "^27.4.6", - "@jest/test-result": "^27.4.6", - "@jest/transform": "^27.4.6", - "@jest/types": "^27.4.2", + "@jest/console": "^29.1.2", + "@jest/reporters": "^29.1.2", + "@jest/test-result": "^29.1.2", + "@jest/transform": "^29.1.2", + "@jest/types": "^29.1.2", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", - "emittery": "^0.8.1", + "ci-info": "^3.2.0", "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-changed-files": "^27.4.2", - "jest-config": "^27.4.7", - "jest-haste-map": "^27.4.6", - "jest-message-util": "^27.4.6", - "jest-regex-util": "^27.4.0", - "jest-resolve": "^27.4.6", - "jest-resolve-dependencies": "^27.4.6", - "jest-runner": "^27.4.6", - "jest-runtime": "^27.4.6", - "jest-snapshot": "^27.4.6", - "jest-util": "^27.4.2", - "jest-validate": "^27.4.6", - "jest-watcher": "^27.4.6", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.0.0", + "jest-config": "^29.1.2", + "jest-haste-map": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-regex-util": "^29.0.0", + "jest-resolve": "^29.1.2", + "jest-resolve-dependencies": "^29.1.2", + "jest-runner": "^29.1.2", + "jest-runtime": "^29.1.2", + "jest-snapshot": "^29.1.2", + "jest-util": "^29.1.2", + "jest-validate": "^29.1.2", + "jest-watcher": "^29.1.2", "micromatch": "^4.0.4", - "rimraf": "^3.0.0", + "pretty-format": "^29.1.2", "slash": "^3.0.0", "strip-ansi": "^6.0.0" } }, "@jest/environment": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.4.6.tgz", - "integrity": "sha512-E6t+RXPfATEEGVidr84WngLNWZ8ffCPky8RqqRK6u1Bn0LK92INe0MDttyPl/JOzaq92BmDzOeuqk09TvM22Sg==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.1.2.tgz", + "integrity": "sha512-rG7xZ2UeOfvOVzoLIJ0ZmvPl4tBEQ2n73CZJSlzUjPw4or1oSWC0s0Rk0ZX+pIBJ04aVr6hLWFn1DFtrnf8MhQ==", "dev": true, "requires": { - "@jest/fake-timers": "^27.4.6", - "@jest/types": "^27.4.2", + "@jest/fake-timers": "^29.1.2", + "@jest/types": "^29.1.2", "@types/node": "*", - "jest-mock": "^27.4.6" + "jest-mock": "^29.1.2" + } + }, + "@jest/expect": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.1.2.tgz", + "integrity": "sha512-FXw/UmaZsyfRyvZw3M6POgSNqwmuOXJuzdNiMWW9LCYo0GRoRDhg+R5iq5higmRTHQY7hx32+j7WHwinRmoILQ==", + "dev": true, + "requires": { + "expect": "^29.1.2", + "jest-snapshot": "^29.1.2" + } + }, + "@jest/expect-utils": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.1.2.tgz", + "integrity": "sha512-4a48bhKfGj/KAH39u0ppzNTABXQ8QPccWAFUFobWBaEMSMp+sB31Z2fK/l47c4a/Mu1po2ffmfAIPxXbVTXdtg==", + "dev": true, + "requires": { + "jest-get-type": "^29.0.0" } }, "@jest/fake-timers": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.4.6.tgz", - "integrity": "sha512-mfaethuYF8scV8ntPpiVGIHQgS0XIALbpY2jt2l7wb/bvq4Q5pDLk4EP4D7SAvYT1QrPOPVZAtbdGAOOyIgs7A==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.1.2.tgz", + "integrity": "sha512-GppaEqS+QQYegedxVMpCe2xCXxxeYwQ7RsNx55zc8f+1q1qevkZGKequfTASI7ejmg9WwI+SJCrHe9X11bLL9Q==", "dev": true, "requires": { - "@jest/types": "^27.4.2", - "@sinonjs/fake-timers": "^8.0.1", + "@jest/types": "^29.1.2", + "@sinonjs/fake-timers": "^9.1.2", "@types/node": "*", - "jest-message-util": "^27.4.6", - "jest-mock": "^27.4.6", - "jest-util": "^27.4.2" + "jest-message-util": "^29.1.2", + "jest-mock": "^29.1.2", + "jest-util": "^29.1.2" } }, "@jest/globals": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.4.6.tgz", - "integrity": "sha512-kAiwMGZ7UxrgPzu8Yv9uvWmXXxsy0GciNejlHvfPIfWkSxChzv6bgTS3YqBkGuHcis+ouMFI2696n2t+XYIeFw==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.1.2.tgz", + "integrity": "sha512-uMgfERpJYoQmykAd0ffyMq8wignN4SvLUG6orJQRe9WAlTRc9cdpCaE/29qurXixYJVZWUqIBXhSk8v5xN1V9g==", "dev": true, "requires": { - "@jest/environment": "^27.4.6", - "@jest/types": "^27.4.2", - "expect": "^27.4.6" + "@jest/environment": "^29.1.2", + "@jest/expect": "^29.1.2", + "@jest/types": "^29.1.2", + "jest-mock": "^29.1.2" } }, "@jest/reporters": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.4.6.tgz", - "integrity": "sha512-+Zo9gV81R14+PSq4wzee4GC2mhAN9i9a7qgJWL90Gpx7fHYkWpTBvwWNZUXvJByYR9tAVBdc8VxDWqfJyIUrIQ==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.1.2.tgz", + "integrity": "sha512-X4fiwwyxy9mnfpxL0g9DD0KcTmEIqP0jUdnc2cfa9riHy+I6Gwwp5vOZiwyg0vZxfSDxrOlK9S4+340W4d+DAA==", "dev": true, "requires": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^27.4.6", - "@jest/test-result": "^27.4.6", - "@jest/transform": "^27.4.6", - "@jest/types": "^27.4.2", + "@jest/console": "^29.1.2", + "@jest/test-result": "^29.1.2", + "@jest/transform": "^29.1.2", + "@jest/types": "^29.1.2", + "@jridgewell/trace-mapping": "^0.3.15", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.4", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^5.1.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-haste-map": "^27.4.6", - "jest-resolve": "^27.4.6", - "jest-util": "^27.4.2", - "jest-worker": "^27.4.6", + "jest-message-util": "^29.1.2", + "jest-util": "^29.1.2", + "jest-worker": "^29.1.2", "slash": "^3.0.0", - "source-map": "^0.6.0", "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", "terminal-link": "^2.0.0", - "v8-to-istanbul": "^8.1.0" + "v8-to-istanbul": "^9.0.1" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "@jest/schemas": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz", + "integrity": "sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.24.1" } }, "@jest/source-map": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.4.0.tgz", - "integrity": "sha512-Ntjx9jzP26Bvhbm93z/AKcPRj/9wrkI88/gK60glXDx1q+IeI0rf7Lw2c89Ch6ofonB0On/iRDreQuQ6te9pgQ==", + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.0.0.tgz", + "integrity": "sha512-nOr+0EM8GiHf34mq2GcJyz/gYFyLQ2INDhAylrZJ9mMWoW21mLBfZa0BUVPPMxVYrLjeiRe2Z7kWXOGnS0TFhQ==", "dev": true, "requires": { + "@jridgewell/trace-mapping": "^0.3.15", "callsites": "^3.0.0", - "graceful-fs": "^4.2.4", - "source-map": "^0.6.0" + "graceful-fs": "^4.2.9" } }, "@jest/test-result": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.4.6.tgz", - "integrity": "sha512-fi9IGj3fkOrlMmhQqa/t9xum8jaJOOAi/lZlm6JXSc55rJMXKHxNDN1oCP39B0/DhNOa2OMupF9BcKZnNtXMOQ==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.1.2.tgz", + "integrity": "sha512-jjYYjjumCJjH9hHCoMhA8PCl1OxNeGgAoZ7yuGYILRJX9NjgzTN0pCT5qAoYR4jfOP8htIByvAlz9vfNSSBoVg==", "dev": true, "requires": { - "@jest/console": "^27.4.6", - "@jest/types": "^27.4.2", + "@jest/console": "^29.1.2", + "@jest/types": "^29.1.2", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" } }, "@jest/test-sequencer": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.4.6.tgz", - "integrity": "sha512-3GL+nsf6E1PsyNsJuvPyIz+DwFuCtBdtvPpm/LMXVkBJbdFvQYCDpccYT56qq5BGniXWlE81n2qk1sdXfZebnw==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.1.2.tgz", + "integrity": "sha512-fU6dsUqqm8sA+cd85BmeF7Gu9DsXVWFdGn9taxM6xN1cKdcP/ivSgXh5QucFRFz1oZxKv3/9DYYbq0ULly3P/Q==", "dev": true, "requires": { - "@jest/test-result": "^27.4.6", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^27.4.6", - "jest-runtime": "^27.4.6" + "@jest/test-result": "^29.1.2", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.1.2", + "slash": "^3.0.0" } }, "@jest/transform": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.4.6.tgz", - "integrity": "sha512-9MsufmJC8t5JTpWEQJ0OcOOAXaH5ioaIX6uHVBLBMoCZPfKKQF+EqP8kACAvCZ0Y1h2Zr3uOccg8re+Dr5jxyw==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.1.2.tgz", + "integrity": "sha512-2uaUuVHTitmkx1tHF+eBjb4p7UuzBG7SXIaA/hNIkaMP6K+gXYGxP38ZcrofzqN0HeZ7A90oqsOa97WU7WZkSw==", "dev": true, "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^27.4.2", + "@babel/core": "^7.11.6", + "@jest/types": "^29.1.2", + "@jridgewell/trace-mapping": "^0.3.15", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^27.4.6", - "jest-regex-util": "^27.4.0", - "jest-util": "^27.4.2", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.1.2", + "jest-regex-util": "^29.0.0", + "jest-util": "^29.1.2", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" + "write-file-atomic": "^4.0.1" } }, "@jest/types": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.4.2.tgz", - "integrity": "sha512-j35yw0PMTPpZsUoOBiuHzr1zTYoad1cVIE0ajEjcrJONxxrko/IRGKkXx3os0Nsi4Hu3+5VmDbVfq5WhG/pWAg==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.1.2.tgz", + "integrity": "sha512-DcXGtoTykQB5jiwCmVr8H4vdg2OJhQex3qPkG+ISyDO7xQXbt/4R6dowcRyPemRnkH7JoHvZuxPBdlq+9JxFCg==", "dev": true, "requires": { + "@jest/schemas": "^29.0.0", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", - "@types/yargs": "^16.0.0", + "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", + "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@sinclair/typebox": { + "version": "0.24.44", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.44.tgz", + "integrity": "sha512-ka0W0KN5i6LfrSocduwliMMpqVgohtPFidKdMEOUjoOFCHcOOYkKsPRxfs5f15oPNHTm6ERAm0GV/+/LTKeiWg==", + "dev": true + }, "@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -5846,24 +776,18 @@ } }, "@sinonjs/fake-timers": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", - "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0" } }, - "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true - }, "@types/babel__core": { - "version": "7.1.18", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.18.tgz", - "integrity": "sha512-S7unDjm/C7z2A2R9NzfKCK1I+BAALDtxEmsJBwlB3EzNfb929ykjL++1CK9LO++EIp2fQrC8O+BwjKvz6UeDyQ==", + "version": "7.1.19", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz", + "integrity": "sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==", "dev": true, "requires": { "@babel/parser": "^7.1.0", @@ -5893,9 +817,9 @@ } }, "@types/babel__traverse": { - "version": "7.14.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.14.2.tgz", - "integrity": "sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz", + "integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==", "dev": true, "requires": { "@babel/types": "^7.3.0" @@ -5935,15 +859,15 @@ } }, "@types/node": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz", - "integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==", + "version": "18.7.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz", + "integrity": "sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg==", "dev": true }, "@types/prettier": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.2.tgz", - "integrity": "sha512-ekoj4qOQYp7CvjX8ZDBgN86w3MqQhLE1hczEJbEIjgFEumDy+na/4AJAbLXfgEWFNB2pKadM5rPFtuSGMWK7xA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==", "dev": true }, "@types/stack-utils": { @@ -5953,89 +877,35 @@ "dev": true }, "@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "version": "17.0.13", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", + "integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==", "dev": true, "requires": { "@types/yargs-parser": "*" } }, "@types/yargs-parser": { - "version": "20.2.1", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz", - "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", - "dev": true - }, - "abab": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", - "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", - "dev": true - }, - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, - "acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", - "dev": true, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "requires": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - }, - "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - } + "event-target-shim": "^5.0.0" } }, - "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "requires": { - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "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 - } + "mime-types": "~2.1.34", + "negotiator": "0.6.3" } }, "ansi-escapes": { @@ -6057,117 +927,45 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "args": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/args/-/args-5.0.1.tgz", - "integrity": "sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==", - "requires": { - "camelcase": "5.0.0", - "chalk": "2.4.2", - "leven": "2.1.0", - "mri": "1.1.4" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "camelcase": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", - "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==" - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "leven": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", - "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" } }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, "atomic-sleep": { @@ -6176,18 +974,17 @@ "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" }, "babel-jest": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.4.6.tgz", - "integrity": "sha512-qZL0JT0HS1L+lOuH+xC2DVASR3nunZi/ozGhpgauJHgmI7f8rudxf6hUjEHympdQ/J64CdKmPkgfJ+A3U6QCrg==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.1.2.tgz", + "integrity": "sha512-IuG+F3HTHryJb7gacC7SQ59A9kO56BctUsT67uJHp1mMCHUOMXpDwOHWGifWqdWVknN2WNkCVQELPjXx0aLJ9Q==", "dev": true, "requires": { - "@jest/transform": "^27.4.6", - "@jest/types": "^27.4.2", + "@jest/transform": "^29.1.2", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^27.4.0", + "babel-preset-jest": "^29.0.2", "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", + "graceful-fs": "^4.2.9", "slash": "^3.0.0" } }, @@ -6205,14 +1002,14 @@ } }, "babel-plugin-jest-hoist": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.4.0.tgz", - "integrity": "sha512-Jcu7qS4OX5kTWBc45Hz7BMmgXuJqRnhatqpUhnzGC3OBYpOmf2tv6jFNwZpwM7wU7MUuv2r9IPS/ZlYOuburVw==", + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.0.2.tgz", + "integrity": "sha512-eBr2ynAEFjcebVvu8Ktx580BD1QKCrBG1XwEUTXJe285p9HA/4hOhfWCFRQhTKSyBV0VzjhG7H91Eifz9s29hg==", "dev": true, "requires": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", + "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" } }, @@ -6237,12 +1034,12 @@ } }, "babel-preset-jest": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.4.0.tgz", - "integrity": "sha512-NK4jGYpnBvNxcGo7/ZpZJr51jCGT+3bwwpVIDY2oNfTxJJldRtB4VAcYdgp1loDE50ODuTu+yBjpMAswv5tlpg==", + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.0.2.tgz", + "integrity": "sha512-BeVXp7rH5TK96ofyEnHjznjLMQ2nAeDJ+QzxKnHAAMs0RgrQsCywjAN8m4mOm5Di0pxU//3AoEeJJrerMH5UeA==", "dev": true, "requires": { - "babel-plugin-jest-hoist": "^27.4.0", + "babel-plugin-jest-hoist": "^29.0.2", "babel-preset-current-node-syntax": "^1.0.0" } }, @@ -6251,27 +1048,35 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "body-parser": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.1.tgz", - "integrity": "sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", "requires": { - "bytes": "3.1.1", + "bytes": "3.1.2", "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.8.1", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.9.6", - "raw-body": "2.4.2", - "type-is": "~1.6.18" + "on-finished": "2.4.1", + "qs": "6.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" } }, "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, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6286,23 +1091,16 @@ "fill-range": "^7.0.1" } }, - "browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true - }, "browserslist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", - "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001286", - "electron-to-chromium": "^1.4.17", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" } }, "bser": { @@ -6314,6 +1112,15 @@ "node-int64": "^0.4.0" } }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6321,15 +1128,14 @@ "dev": true }, "bytes": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz", - "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -6348,9 +1154,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001296", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001296.tgz", - "integrity": "sha512-WfrtPEoNSoeATDlf4y3QvkwiELl9GyPLISV5GejTbbQRtQx4LhsXmc9IQ6XCL2d7UxCyEzToEZNMeqR79OUw8Q==", + "version": "1.0.30001414", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001414.tgz", + "integrity": "sha512-t55jfSaWjCdocnFdKQoO+d2ct9C59UZg4dY3OnUlSZ447r8pUtIKdp0hpAzrGFultmTC+Us+KpKi4GZl/LXlFg==", "dev": true }, "chalk": { @@ -6370,9 +1176,9 @@ "dev": true }, "ci-info": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz", - "integrity": "sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.4.0.tgz", + "integrity": "sha512-t5QdPT5jq3o262DOQ8zA6E1tlH2upmUc4Hlvrbx1pGYJuiiHl7O7rvVNI+l8HTVhd/q3Qc9vqimkNk5yiXsAug==", "dev": true }, "cjs-module-lexer": { @@ -6395,7 +1201,7 @@ "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true }, "collect-v8-coverage": { @@ -6420,9 +1226,9 @@ "dev": true }, "colorette": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.12.tgz", - "integrity": "sha512-lHID0PU+NtFzeNCwTL6JzUKdb6kDpyEjrwTD1H0cDZswTbsjLh2wTV2Eo2sNZLc0oSg0a5W1AI4Nj7bX4iIdjA==" + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" }, "combined-stream": { "version": "1.0.8", @@ -6442,7 +1248,8 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "content-disposition": { "version": "0.5.4", @@ -6474,14 +1281,14 @@ } }, "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, "cookiejar": { "version": "2.1.3", @@ -6500,40 +1307,6 @@ "which": "^2.0.1" } }, - "cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true - }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } - } - }, - "data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", - "dev": true, - "requires": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" - } - }, "dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", @@ -6547,22 +1320,10 @@ "ms": "2.0.0" } }, - "decimal.js": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", - "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", - "dev": true - }, "dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", - "dev": true - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, "deepmerge": { @@ -6574,18 +1335,18 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true }, "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" }, "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, "detect-newline": { "version": "3.1.0", @@ -6596,7 +1357,7 @@ "dezalgo": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", + "integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==", "dev": true, "requires": { "asap": "^2.0.0", @@ -6604,28 +1365,11 @@ } }, "diff-sequences": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.4.0.tgz", - "integrity": "sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww==", + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.0.0.tgz", + "integrity": "sha512-7Qe/zd1wxSDL4D/X/FPjOMB+ZMDt71W94KYaq05I2l0oQqgXgs7s4ftYYmV38gBSrPz2vcygxfs1xn0FT+rKNA==", "dev": true }, - "domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", - "dev": true, - "requires": { - "webidl-conversions": "^5.0.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true - } - } - }, "duplexify": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", @@ -6640,18 +1384,18 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { - "version": "1.4.36", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.36.tgz", - "integrity": "sha512-MbLlbF39vKrXWlFEFpCgDHwdlz4O3LmHM5W4tiLRHjSmEUXjJjz8sZkMgWgvYxlZw3N1iDTmCEtOkkESb5TMCg==", + "version": "1.4.269", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.269.tgz", + "integrity": "sha512-7mHFONwp7MNvdyto1v70fCwk28NJMFgsK79op+iYHzz1BLE8T66a1B2qW5alb8XgE0yi3FL3ZQjSYZpJpF6snw==", "dev": true }, "emittery": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", - "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", + "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", "dev": true }, "emoji-regex": { @@ -6663,7 +1407,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, "end-of-stream": { "version": "1.4.4", @@ -6673,6 +1417,15 @@ "once": "^1.4.0" } }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -6682,49 +1435,39 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true }, - "escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", - "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true + "esprima-next": { + "version": "5.8.4", + "resolved": "https://registry.npmjs.org/esprima-next/-/esprima-next-5.8.4.tgz", + "integrity": "sha512-8nYVZ4ioIH4Msjb/XmhnBdz5WRRBaYqevKa1cv9nGJdCehMbzZCPNEEnqfLCZVetUVrUPEcb5IYyu1GG4hFqgg==" }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, "execa": { "version": "5.1.1", @@ -6746,53 +1489,55 @@ "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", "dev": true }, "expect": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/expect/-/expect-27.4.6.tgz", - "integrity": "sha512-1M/0kAALIaj5LaG66sFJTbRsWTADnylly82cu4bspI0nl+pgP4E6Bh/aqdHlTUjul06K7xQnnrAoqfxVU0+/ag==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.1.2.tgz", + "integrity": "sha512-AuAGn1uxva5YBbBlXb+2JPxJRuemZsmlGcapPXWNSBNsQtAULfjioREGBWuI0EOvYUKjDnrCy8PW5Zlr1md5mw==", "dev": true, "requires": { - "@jest/types": "^27.4.2", - "jest-get-type": "^27.4.0", - "jest-matcher-utils": "^27.4.6", - "jest-message-util": "^27.4.6" + "@jest/expect-utils": "^29.1.2", + "jest-get-type": "^29.0.0", + "jest-matcher-utils": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-util": "^29.1.2" } }, "express": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.2.tgz", - "integrity": "sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", "requires": { - "accepts": "~1.3.7", + "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.19.1", + "body-parser": "1.20.0", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.4.1", + "cookie": "0.5.0", "cookie-signature": "1.0.6", "debug": "2.6.9", - "depd": "~1.1.2", + "depd": "2.0.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "~1.1.2", + "finalhandler": "1.2.0", "fresh": "0.5.2", + "http-errors": "2.0.0", "merge-descriptors": "1.0.1", "methods": "~1.1.2", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", - "qs": "6.9.6", + "qs": "6.10.3", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.17.2", - "serve-static": "1.14.2", + "send": "0.18.0", + "serve-static": "1.15.0", "setprototypeof": "1.2.0", - "statuses": "~1.5.0", + "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -6814,30 +1559,29 @@ } }, "express-validator": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.14.0.tgz", - "integrity": "sha512-ZWHJfnRgePp3FKRSKMtnZVnD1s8ZchWD+jSl7UMseGIqhweCo1Z9916/xXBbJAa6PrA3pUZfkOvIsHZG4ZtIMw==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.14.2.tgz", + "integrity": "sha512-8XfAUrQ6Y7dIIuy9KcUPCfG/uCbvREctrxf5EeeME+ulanJ4iiW71lWmm9r4YcKKYOCBMan0WpVg7FtHu4Z4Wg==", "requires": { "lodash": "^4.17.21", "validator": "^13.7.0" } }, + "fast-copy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.0.tgz", + "integrity": "sha512-4HzS+9pQ5Yxtv13Lhs1Z1unMXamBdn5nA4bEi1abYpDNSpSp7ODYQ1KPMF6nTatfEzgH6/zPvXKU1zvHiUjWlA==" + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, "fast-redact": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.0.2.tgz", - "integrity": "sha512-YN+CYfCVRVMUZOUPeinHNKgytM1wPI/C/UCLEi56EsY2dwwvI00kIJHJoI7pMVqGoMew8SMZ2SSfHKHULHXDsg==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.1.tgz", + "integrity": "sha512-odVmjC8x8jNeMZ3C+rPMESzXVSEU8tSWSHv9HFxP2mm89G/1WwqhrerJDQm9Zus8X6aoRgQDThKqptdNA6bt+A==" }, "fast-safe-stringify": { "version": "2.1.1", @@ -6853,9 +1597,9 @@ } }, "fb-watchman": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", - "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", "dev": true, "requires": { "bser": "2.1.1" @@ -6871,16 +1615,16 @@ } }, "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "requires": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", - "statuses": "~1.5.0", + "statuses": "2.0.1", "unpipe": "~1.0.0" } }, @@ -6895,9 +1639,9 @@ } }, "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "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, "requires": { "asynckit": "^0.4.0", @@ -6933,12 +1677,12 @@ "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, "fs-extra": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", - "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "requires": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -6948,7 +1692,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "fsevents": { "version": "2.3.2", @@ -6960,8 +1704,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "gensync": { "version": "1.0.0-beta.2", @@ -6978,7 +1721,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -6998,16 +1740,33 @@ "dev": true }, "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", "requires": { "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" + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "requires": { + "brace-expansion": "^2.0.1" + } + } } }, "globals": { @@ -7017,15 +1776,14 @@ "dev": true }, "graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -7037,10 +1795,18 @@ "dev": true }, "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "help-me": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.1.0.tgz", + "integrity": "sha512-5HMrkOks2j8Fpu2j5nTLhrBhT7VwHwELpqnSnx802ckofys5MO2SkLpgSz3dgNFHV7IYFX2igm5CM75SmuYidw==", + "requires": { + "glob": "^8.0.0", + "readable-stream": "^3.6.0" + } }, "hexoid": { "version": "1.0.0", @@ -7048,15 +1814,6 @@ "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", "dev": true }, - "html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.5" - } - }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -7064,72 +1821,17 @@ "dev": true }, "http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "requires": { - "depd": "~1.1.2", + "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", + "statuses": "2.0.1", "toidentifier": "1.0.1" } }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dev": true, - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "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 - } - } - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, - "requires": { - "agent-base": "6", - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "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 - } - } - }, "human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -7144,10 +1846,15 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, "import-local": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.3.tgz", - "integrity": "sha512-bE9iaUY3CXH8Cwfan/abDKAxe1KGT9kyGsBPqf6DMK/z0a2OzAsrukeYNgIH6cH5Xr452jb1TUL8rSfCLjZ9uA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", "dev": true, "requires": { "pkg-dir": "^4.2.0", @@ -7157,13 +1864,13 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "requires": { "once": "^1.3.0", "wrappy": "1" @@ -7179,10 +1886,16 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", "dev": true, "requires": { "has": "^1.0.3" @@ -7206,28 +1919,16 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true - }, "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, "istanbul-lib-coverage": { @@ -7237,9 +1938,9 @@ "dev": true }, "istanbul-lib-instrument": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz", - "integrity": "sha512-czwUz525rkOFDJxfKK6mYfIs9zBKILyrZQxjz3ABhjQXhbhFsSbo1HW/BFcsDnfJYJWA6thRR5/TUY2qs5W99Q==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz", + "integrity": "sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A==", "dev": true, "requires": { "@babel/core": "^7.12.3", @@ -7272,9 +1973,9 @@ }, "dependencies": { "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -7289,9 +1990,9 @@ } }, "istanbul-reports": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.3.tgz", - "integrity": "sha512-x9LtDVtfm/t1GFiLl3NffC7hz+I1ragvgX1P/Lg1NlIagifZDKUkuuaAxH/qpwj2IuEfD8G2Bs/UKp+sZ/pKkg==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -7299,414 +2000,429 @@ } }, "jest": { - "version": "27.4.7", - "resolved": "https://registry.npmjs.org/jest/-/jest-27.4.7.tgz", - "integrity": "sha512-8heYvsx7nV/m8m24Vk26Y87g73Ba6ueUd0MWed/NXMhSZIm62U/llVbS0PJe1SHunbyXjJ/BqG1z9bFjGUIvTg==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.1.2.tgz", + "integrity": "sha512-5wEIPpCezgORnqf+rCaYD1SK+mNN7NsstWzIsuvsnrhR/hSxXWd82oI7DkrbJ+XTD28/eG8SmxdGvukrGGK6Tw==", "dev": true, "requires": { - "@jest/core": "^27.4.7", + "@jest/core": "^29.1.2", + "@jest/types": "^29.1.2", "import-local": "^3.0.2", - "jest-cli": "^27.4.7" + "jest-cli": "^29.1.2" + }, + "dependencies": { + "jest-cli": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.1.2.tgz", + "integrity": "sha512-vsvBfQ7oS2o4MJdAH+4u9z76Vw5Q8WBQF5MchDbkylNknZdrPTX1Ix7YRJyTlOWqRaS7ue/cEAn+E4V1MWyMzw==", + "dev": true, + "requires": { + "@jest/core": "^29.1.2", + "@jest/test-result": "^29.1.2", + "@jest/types": "^29.1.2", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^29.1.2", + "jest-util": "^29.1.2", + "jest-validate": "^29.1.2", + "prompts": "^2.0.1", + "yargs": "^17.3.1" + } + } } }, "jest-changed-files": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.4.2.tgz", - "integrity": "sha512-/9x8MjekuzUQoPjDHbBiXbNEBauhrPU2ct7m8TfCg69ywt1y/N+yYwGh3gCpnqUS3klYWDU/lSNgv+JhoD2k1A==", + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.0.0.tgz", + "integrity": "sha512-28/iDMDrUpGoCitTURuDqUzWQoWmOmOKOFST1mi2lwh62X4BFf6khgH3uSuo1e49X/UDjuApAj3w0wLOex4VPQ==", "dev": true, "requires": { - "@jest/types": "^27.4.2", "execa": "^5.0.0", - "throat": "^6.0.1" + "p-limit": "^3.1.0" + }, + "dependencies": { + "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, + "requires": { + "yocto-queue": "^0.1.0" + } + } } }, "jest-circus": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.4.6.tgz", - "integrity": "sha512-UA7AI5HZrW4wRM72Ro80uRR2Fg+7nR0GESbSI/2M+ambbzVuA63mn5T1p3Z/wlhntzGpIG1xx78GP2YIkf6PhQ==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.1.2.tgz", + "integrity": "sha512-ajQOdxY6mT9GtnfJRZBRYS7toNIJayiiyjDyoZcnvPRUPwJ58JX0ci0PKAKUo2C1RyzlHw0jabjLGKksO42JGA==", "dev": true, "requires": { - "@jest/environment": "^27.4.6", - "@jest/test-result": "^27.4.6", - "@jest/types": "^27.4.2", + "@jest/environment": "^29.1.2", + "@jest/expect": "^29.1.2", + "@jest/test-result": "^29.1.2", + "@jest/types": "^29.1.2", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^0.7.0", - "expect": "^27.4.6", "is-generator-fn": "^2.0.0", - "jest-each": "^27.4.6", - "jest-matcher-utils": "^27.4.6", - "jest-message-util": "^27.4.6", - "jest-runtime": "^27.4.6", - "jest-snapshot": "^27.4.6", - "jest-util": "^27.4.2", - "pretty-format": "^27.4.6", + "jest-each": "^29.1.2", + "jest-matcher-utils": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-runtime": "^29.1.2", + "jest-snapshot": "^29.1.2", + "jest-util": "^29.1.2", + "p-limit": "^3.1.0", + "pretty-format": "^29.1.2", "slash": "^3.0.0", - "stack-utils": "^2.0.3", - "throat": "^6.0.1" - } - }, - "jest-cli": { - "version": "27.4.7", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.4.7.tgz", - "integrity": "sha512-zREYhvjjqe1KsGV15mdnxjThKNDgza1fhDT+iUsXWLCq3sxe9w5xnvyctcYVT5PcdLSjv7Y5dCwTS3FCF1tiuw==", - "dev": true, - "requires": { - "@jest/core": "^27.4.7", - "@jest/test-result": "^27.4.6", - "@jest/types": "^27.4.2", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "import-local": "^3.0.2", - "jest-config": "^27.4.7", - "jest-util": "^27.4.2", - "jest-validate": "^27.4.6", - "prompts": "^2.0.1", - "yargs": "^16.2.0" + "stack-utils": "^2.0.3" + }, + "dependencies": { + "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, + "requires": { + "yocto-queue": "^0.1.0" + } + } } }, "jest-config": { - "version": "27.4.7", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.4.7.tgz", - "integrity": "sha512-xz/o/KJJEedHMrIY9v2ParIoYSrSVY6IVeE4z5Z3i101GoA5XgfbJz+1C8EYPsv7u7f39dS8F9v46BHDhn0vlw==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.1.2.tgz", + "integrity": "sha512-EC3Zi86HJUOz+2YWQcJYQXlf0zuBhJoeyxLM6vb6qJsVmpP7KcCP1JnyF0iaqTaXdBP8Rlwsvs7hnKWQWWLwwA==", "dev": true, "requires": { - "@babel/core": "^7.8.0", - "@jest/test-sequencer": "^27.4.6", - "@jest/types": "^27.4.2", - "babel-jest": "^27.4.6", + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.1.2", + "@jest/types": "^29.1.2", + "babel-jest": "^29.1.2", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.4", - "jest-circus": "^27.4.6", - "jest-environment-jsdom": "^27.4.6", - "jest-environment-node": "^27.4.6", - "jest-get-type": "^27.4.0", - "jest-jasmine2": "^27.4.6", - "jest-regex-util": "^27.4.0", - "jest-resolve": "^27.4.6", - "jest-runner": "^27.4.6", - "jest-util": "^27.4.2", - "jest-validate": "^27.4.6", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.1.2", + "jest-environment-node": "^29.1.2", + "jest-get-type": "^29.0.0", + "jest-regex-util": "^29.0.0", + "jest-resolve": "^29.1.2", + "jest-runner": "^29.1.2", + "jest-util": "^29.1.2", + "jest-validate": "^29.1.2", "micromatch": "^4.0.4", - "pretty-format": "^27.4.6", - "slash": "^3.0.0" + "parse-json": "^5.2.0", + "pretty-format": "^29.1.2", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "jest-diff": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.4.6.tgz", - "integrity": "sha512-zjaB0sh0Lb13VyPsd92V7HkqF6yKRH9vm33rwBt7rPYrpQvS1nCvlIy2pICbKta+ZjWngYLNn4cCK4nyZkjS/w==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.1.2.tgz", + "integrity": "sha512-4GQts0aUopVvecIT4IwD/7xsBaMhKTYoM4/njE/aVw9wpw+pIUVp8Vab/KnSzSilr84GnLBkaP3JLDnQYCKqVQ==", "dev": true, "requires": { "chalk": "^4.0.0", - "diff-sequences": "^27.4.0", - "jest-get-type": "^27.4.0", - "pretty-format": "^27.4.6" + "diff-sequences": "^29.0.0", + "jest-get-type": "^29.0.0", + "pretty-format": "^29.1.2" } }, "jest-docblock": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.4.0.tgz", - "integrity": "sha512-7TBazUdCKGV7svZ+gh7C8esAnweJoG+SvcF6Cjqj4l17zA2q1cMwx2JObSioubk317H+cjcHgP+7fTs60paulg==", + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.0.0.tgz", + "integrity": "sha512-s5Kpra/kLzbqu9dEjov30kj1n4tfu3e7Pl8v+f8jOkeWNqM6Ds8jRaJfZow3ducoQUrf2Z4rs2N5S3zXnb83gw==", "dev": true, "requires": { "detect-newline": "^3.0.0" } }, "jest-each": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.4.6.tgz", - "integrity": "sha512-n6QDq8y2Hsmn22tRkgAk+z6MCX7MeVlAzxmZDshfS2jLcaBlyhpF3tZSJLR+kXmh23GEvS0ojMR8i6ZeRvpQcA==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.1.2.tgz", + "integrity": "sha512-AmTQp9b2etNeEwMyr4jc0Ql/LIX/dhbgP21gHAizya2X6rUspHn2gysMXaj6iwWuOJ2sYRgP8c1P4cXswgvS1A==", "dev": true, "requires": { - "@jest/types": "^27.4.2", + "@jest/types": "^29.1.2", "chalk": "^4.0.0", - "jest-get-type": "^27.4.0", - "jest-util": "^27.4.2", - "pretty-format": "^27.4.6" - } - }, - "jest-environment-jsdom": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.4.6.tgz", - "integrity": "sha512-o3dx5p/kHPbUlRvSNjypEcEtgs6LmvESMzgRFQE6c+Prwl2JLA4RZ7qAnxc5VM8kutsGRTB15jXeeSbJsKN9iA==", - "dev": true, - "requires": { - "@jest/environment": "^27.4.6", - "@jest/fake-timers": "^27.4.6", - "@jest/types": "^27.4.2", - "@types/node": "*", - "jest-mock": "^27.4.6", - "jest-util": "^27.4.2", - "jsdom": "^16.6.0" + "jest-get-type": "^29.0.0", + "jest-util": "^29.1.2", + "pretty-format": "^29.1.2" } }, "jest-environment-node": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.4.6.tgz", - "integrity": "sha512-yfHlZ9m+kzTKZV0hVfhVu6GuDxKAYeFHrfulmy7Jxwsq4V7+ZK7f+c0XP/tbVDMQW7E4neG2u147hFkuVz0MlQ==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.1.2.tgz", + "integrity": "sha512-C59yVbdpY8682u6k/lh8SUMDJPbOyCHOTgLVVi1USWFxtNV+J8fyIwzkg+RJIVI30EKhKiAGNxYaFr3z6eyNhQ==", "dev": true, "requires": { - "@jest/environment": "^27.4.6", - "@jest/fake-timers": "^27.4.6", - "@jest/types": "^27.4.2", + "@jest/environment": "^29.1.2", + "@jest/fake-timers": "^29.1.2", + "@jest/types": "^29.1.2", "@types/node": "*", - "jest-mock": "^27.4.6", - "jest-util": "^27.4.2" + "jest-mock": "^29.1.2", + "jest-util": "^29.1.2" } }, "jest-get-type": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.4.0.tgz", - "integrity": "sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ==", + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.0.0.tgz", + "integrity": "sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw==", "dev": true }, "jest-haste-map": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.4.6.tgz", - "integrity": "sha512-0tNpgxg7BKurZeFkIOvGCkbmOHbLFf4LUQOxrQSMjvrQaQe3l6E8x6jYC1NuWkGo5WDdbr8FEzUxV2+LWNawKQ==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.1.2.tgz", + "integrity": "sha512-xSjbY8/BF11Jh3hGSPfYTa/qBFrm3TPM7WU8pU93m2gqzORVLkHFWvuZmFsTEBPRKndfewXhMOuzJNHyJIZGsw==", "dev": true, "requires": { - "@jest/types": "^27.4.2", - "@types/graceful-fs": "^4.1.2", + "@jest/types": "^29.1.2", + "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "fsevents": "^2.3.2", - "graceful-fs": "^4.2.4", - "jest-regex-util": "^27.4.0", - "jest-serializer": "^27.4.0", - "jest-util": "^27.4.2", - "jest-worker": "^27.4.6", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.0.0", + "jest-util": "^29.1.2", + "jest-worker": "^29.1.2", "micromatch": "^4.0.4", - "walker": "^1.0.7" - } - }, - "jest-jasmine2": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.4.6.tgz", - "integrity": "sha512-uAGNXF644I/whzhsf7/qf74gqy9OuhvJ0XYp8SDecX2ooGeaPnmJMjXjKt0mqh1Rl5dtRGxJgNrHlBQIBfS5Nw==", - "dev": true, - "requires": { - "@jest/environment": "^27.4.6", - "@jest/source-map": "^27.4.0", - "@jest/test-result": "^27.4.6", - "@jest/types": "^27.4.2", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "expect": "^27.4.6", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.4.6", - "jest-matcher-utils": "^27.4.6", - "jest-message-util": "^27.4.6", - "jest-runtime": "^27.4.6", - "jest-snapshot": "^27.4.6", - "jest-util": "^27.4.2", - "pretty-format": "^27.4.6", - "throat": "^6.0.1" + "walker": "^1.0.8" } }, "jest-leak-detector": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.4.6.tgz", - "integrity": "sha512-kkaGixDf9R7CjHm2pOzfTxZTQQQ2gHTIWKY/JZSiYTc90bZp8kSZnUMS3uLAfwTZwc0tcMRoEX74e14LG1WapA==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.1.2.tgz", + "integrity": "sha512-TG5gAZJpgmZtjb6oWxBLf2N6CfQ73iwCe6cofu/Uqv9iiAm6g502CAnGtxQaTfpHECBdVEMRBhomSXeLnoKjiQ==", "dev": true, "requires": { - "jest-get-type": "^27.4.0", - "pretty-format": "^27.4.6" + "jest-get-type": "^29.0.0", + "pretty-format": "^29.1.2" } }, "jest-matcher-utils": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.4.6.tgz", - "integrity": "sha512-XD4PKT3Wn1LQnRAq7ZsTI0VRuEc9OrCPFiO1XL7bftTGmfNF0DcEwMHRgqiu7NGf8ZoZDREpGrCniDkjt79WbA==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.1.2.tgz", + "integrity": "sha512-MV5XrD3qYSW2zZSHRRceFzqJ39B2z11Qv0KPyZYxnzDHFeYZGJlgGi0SW+IXSJfOewgJp/Km/7lpcFT+cgZypw==", "dev": true, "requires": { "chalk": "^4.0.0", - "jest-diff": "^27.4.6", - "jest-get-type": "^27.4.0", - "pretty-format": "^27.4.6" + "jest-diff": "^29.1.2", + "jest-get-type": "^29.0.0", + "pretty-format": "^29.1.2" } }, "jest-message-util": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.4.6.tgz", - "integrity": "sha512-0p5szriFU0U74czRSFjH6RyS7UYIAkn/ntwMuOwTGWrQIOh5NzXXrq72LOqIkJKKvFbPq+byZKuBz78fjBERBA==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.1.2.tgz", + "integrity": "sha512-9oJ2Os+Qh6IlxLpmvshVbGUiSkZVc2FK+uGOm6tghafnB2RyjKAxMZhtxThRMxfX1J1SOMhTn9oK3/MutRWQJQ==", "dev": true, "requires": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^27.4.2", + "@jest/types": "^29.1.2", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", + "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^27.4.6", + "pretty-format": "^29.1.2", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "jest-mock": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.4.6.tgz", - "integrity": "sha512-kvojdYRkst8iVSZ1EJ+vc1RRD9llueBjKzXzeCytH3dMM7zvPV/ULcfI2nr0v0VUgm3Bjt3hBCQvOeaBz+ZTHw==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.1.2.tgz", + "integrity": "sha512-PFDAdjjWbjPUtQPkQufvniXIS3N9Tv7tbibePEjIIprzjgo0qQlyUiVMrT4vL8FaSJo1QXifQUOuPH3HQC/aMA==", "dev": true, "requires": { - "@jest/types": "^27.4.2", - "@types/node": "*" + "@jest/types": "^29.1.2", + "@types/node": "*", + "jest-util": "^29.1.2" } }, "jest-pnp-resolver": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true, - "requires": {} + "dev": true }, "jest-regex-util": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.4.0.tgz", - "integrity": "sha512-WeCpMpNnqJYMQoOjm1nTtsgbR4XHAk1u00qDoNBQoykM280+/TmgA5Qh5giC1ecy6a5d4hbSsHzpBtu5yvlbEg==", + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.0.0.tgz", + "integrity": "sha512-BV7VW7Sy0fInHWN93MMPtlClweYv2qrSCwfeFWmpribGZtQPWNvRSq9XOVgOEjU1iBGRKXUZil0o2AH7Iy9Lug==", "dev": true }, "jest-resolve": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.4.6.tgz", - "integrity": "sha512-SFfITVApqtirbITKFAO7jOVN45UgFzcRdQanOFzjnbd+CACDoyeX7206JyU92l4cRr73+Qy/TlW51+4vHGt+zw==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.1.2.tgz", + "integrity": "sha512-7fcOr+k7UYSVRJYhSmJHIid3AnDBcLQX3VmT9OSbPWsWz1MfT7bcoerMhADKGvKCoMpOHUQaDHtQoNp/P9JMGg==", "dev": true, "requires": { - "@jest/types": "^27.4.2", "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^27.4.6", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.1.2", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^27.4.2", - "jest-validate": "^27.4.6", + "jest-util": "^29.1.2", + "jest-validate": "^29.1.2", "resolve": "^1.20.0", "resolve.exports": "^1.1.0", "slash": "^3.0.0" } }, "jest-resolve-dependencies": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.4.6.tgz", - "integrity": "sha512-W85uJZcFXEVZ7+MZqIPCscdjuctruNGXUZ3OHSXOfXR9ITgbUKeHj+uGcies+0SsvI5GtUfTw4dY7u9qjTvQOw==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.1.2.tgz", + "integrity": "sha512-44yYi+yHqNmH3OoWZvPgmeeiwKxhKV/0CfrzaKLSkZG9gT973PX8i+m8j6pDrTYhhHoiKfF3YUFg/6AeuHw4HQ==", "dev": true, "requires": { - "@jest/types": "^27.4.2", - "jest-regex-util": "^27.4.0", - "jest-snapshot": "^27.4.6" + "jest-regex-util": "^29.0.0", + "jest-snapshot": "^29.1.2" } }, "jest-runner": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.4.6.tgz", - "integrity": "sha512-IDeFt2SG4DzqalYBZRgbbPmpwV3X0DcntjezPBERvnhwKGWTW7C5pbbA5lVkmvgteeNfdd/23gwqv3aiilpYPg==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.1.2.tgz", + "integrity": "sha512-yy3LEWw8KuBCmg7sCGDIqKwJlULBuNIQa2eFSVgVASWdXbMYZ9H/X0tnXt70XFoGf92W2sOQDOIFAA6f2BG04Q==", "dev": true, "requires": { - "@jest/console": "^27.4.6", - "@jest/environment": "^27.4.6", - "@jest/test-result": "^27.4.6", - "@jest/transform": "^27.4.6", - "@jest/types": "^27.4.2", + "@jest/console": "^29.1.2", + "@jest/environment": "^29.1.2", + "@jest/test-result": "^29.1.2", + "@jest/transform": "^29.1.2", + "@jest/types": "^29.1.2", "@types/node": "*", "chalk": "^4.0.0", - "emittery": "^0.8.1", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-docblock": "^27.4.0", - "jest-environment-jsdom": "^27.4.6", - "jest-environment-node": "^27.4.6", - "jest-haste-map": "^27.4.6", - "jest-leak-detector": "^27.4.6", - "jest-message-util": "^27.4.6", - "jest-resolve": "^27.4.6", - "jest-runtime": "^27.4.6", - "jest-util": "^27.4.2", - "jest-worker": "^27.4.6", - "source-map-support": "^0.5.6", - "throat": "^6.0.1" + "emittery": "^0.10.2", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.0.0", + "jest-environment-node": "^29.1.2", + "jest-haste-map": "^29.1.2", + "jest-leak-detector": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-resolve": "^29.1.2", + "jest-runtime": "^29.1.2", + "jest-util": "^29.1.2", + "jest-watcher": "^29.1.2", + "jest-worker": "^29.1.2", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "dependencies": { + "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, + "requires": { + "yocto-queue": "^0.1.0" + } + } } }, "jest-runtime": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.4.6.tgz", - "integrity": "sha512-eXYeoR/MbIpVDrjqy5d6cGCFOYBFFDeKaNWqTp0h6E74dK0zLHzASQXJpl5a2/40euBmKnprNLJ0Kh0LCndnWQ==", - "dev": true, - "requires": { - "@jest/environment": "^27.4.6", - "@jest/fake-timers": "^27.4.6", - "@jest/globals": "^27.4.6", - "@jest/source-map": "^27.4.0", - "@jest/test-result": "^27.4.6", - "@jest/transform": "^27.4.6", - "@jest/types": "^27.4.2", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.1.2.tgz", + "integrity": "sha512-jr8VJLIf+cYc+8hbrpt412n5jX3tiXmpPSYTGnwcvNemY+EOuLNiYnHJ3Kp25rkaAcTWOEI4ZdOIQcwYcXIAZw==", + "dev": true, + "requires": { + "@jest/environment": "^29.1.2", + "@jest/fake-timers": "^29.1.2", + "@jest/globals": "^29.1.2", + "@jest/source-map": "^29.0.0", + "@jest/test-result": "^29.1.2", + "@jest/transform": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", "glob": "^7.1.3", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^27.4.6", - "jest-message-util": "^27.4.6", - "jest-mock": "^27.4.6", - "jest-regex-util": "^27.4.0", - "jest-resolve": "^27.4.6", - "jest-snapshot": "^27.4.6", - "jest-util": "^27.4.2", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-mock": "^29.1.2", + "jest-regex-util": "^29.0.0", + "jest-resolve": "^29.1.2", + "jest-snapshot": "^29.1.2", + "jest-util": "^29.1.2", "slash": "^3.0.0", "strip-bom": "^4.0.0" - } - }, - "jest-serializer": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.4.0.tgz", - "integrity": "sha512-RDhpcn5f1JYTX2pvJAGDcnsNTnsV9bjYPU8xcV+xPwOXnUPOQwf4ZEuiU6G9H1UztH+OapMgu/ckEVwO87PwnQ==", - "dev": true, - "requires": { - "@types/node": "*", - "graceful-fs": "^4.2.4" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "jest-snapshot": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.4.6.tgz", - "integrity": "sha512-fafUCDLQfzuNP9IRcEqaFAMzEe7u5BF7mude51wyWv7VRex60WznZIC7DfKTgSIlJa8aFzYmXclmN328aqSDmQ==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.1.2.tgz", + "integrity": "sha512-rYFomGpVMdBlfwTYxkUp3sjD6usptvZcONFYNqVlaz4EpHPnDvlWjvmOQ9OCSNKqYZqLM2aS3wq01tWujLg7gg==", "dev": true, "requires": { - "@babel/core": "^7.7.2", + "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/traverse": "^7.7.2", - "@babel/types": "^7.0.0", - "@jest/transform": "^27.4.6", - "@jest/types": "^27.4.2", - "@types/babel__traverse": "^7.0.4", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.1.2", + "@jest/transform": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/babel__traverse": "^7.0.6", "@types/prettier": "^2.1.5", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^27.4.6", - "graceful-fs": "^4.2.4", - "jest-diff": "^27.4.6", - "jest-get-type": "^27.4.0", - "jest-haste-map": "^27.4.6", - "jest-matcher-utils": "^27.4.6", - "jest-message-util": "^27.4.6", - "jest-util": "^27.4.2", + "expect": "^29.1.2", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.1.2", + "jest-get-type": "^29.0.0", + "jest-haste-map": "^29.1.2", + "jest-matcher-utils": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-util": "^29.1.2", "natural-compare": "^1.4.0", - "pretty-format": "^27.4.6", - "semver": "^7.3.2" + "pretty-format": "^29.1.2", + "semver": "^7.3.5" }, "dependencies": { "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -7715,31 +2431,31 @@ } }, "jest-util": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.4.2.tgz", - "integrity": "sha512-YuxxpXU6nlMan9qyLuxHaMMOzXAl5aGZWCSzben5DhLHemYQxCc4YK+4L3ZrCutT8GPQ+ui9k5D8rUJoDioMnA==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.1.2.tgz", + "integrity": "sha512-vPCk9F353i0Ymx3WQq3+a4lZ07NXu9Ca8wya6o4Fe4/aO1e1awMMprZ3woPFpKwghEOW+UXgd15vVotuNN9ONQ==", "dev": true, "requires": { - "@jest/types": "^27.4.2", + "@jest/types": "^29.1.2", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", - "graceful-fs": "^4.2.4", + "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "jest-validate": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.4.6.tgz", - "integrity": "sha512-872mEmCPVlBqbA5dToC57vA3yJaMRfIdpCoD3cyHWJOMx+SJwLNw0I71EkWs41oza/Er9Zno9XuTkRYCPDUJXQ==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.1.2.tgz", + "integrity": "sha512-k71pOslNlV8fVyI+mEySy2pq9KdXdgZtm7NHrBX8LghJayc3wWZH0Yr0mtYNGaCU4F1OLPXRkwZR0dBm/ClshA==", "dev": true, "requires": { - "@jest/types": "^27.4.2", + "@jest/types": "^29.1.2", "camelcase": "^6.2.0", "chalk": "^4.0.0", - "jest-get-type": "^27.4.0", + "jest-get-type": "^29.0.0", "leven": "^3.1.0", - "pretty-format": "^27.4.6" + "pretty-format": "^29.1.2" }, "dependencies": { "camelcase": { @@ -7751,27 +2467,29 @@ } }, "jest-watcher": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.4.6.tgz", - "integrity": "sha512-yKQ20OMBiCDigbD0quhQKLkBO+ObGN79MO4nT7YaCuQ5SM+dkBNWE8cZX0FjU6czwMvWw6StWbe+Wv4jJPJ+fw==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.1.2.tgz", + "integrity": "sha512-6JUIUKVdAvcxC6bM8/dMgqY2N4lbT+jZVsxh0hCJRbwkIEnbr/aPjMQ28fNDI5lB51Klh00MWZZeVf27KBUj5w==", "dev": true, "requires": { - "@jest/test-result": "^27.4.6", - "@jest/types": "^27.4.2", + "@jest/test-result": "^29.1.2", + "@jest/types": "^29.1.2", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", - "jest-util": "^27.4.2", + "emittery": "^0.10.2", + "jest-util": "^29.1.2", "string-length": "^4.0.1" } }, "jest-worker": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.6.tgz", - "integrity": "sha512-gHWJF/6Xi5CTG5QCvROr6GcmpIqNYpDJyc8A1h/DyXqH1tD6SnRCM0d3U5msV31D2LB/U+E0M+W4oyvKV44oNw==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.1.2.tgz", + "integrity": "sha512-AdTZJxKjTSPHbXT/AIOjQVmoFx0LHFcVabWu0sxI7PAy7rFf8c0upyvgBKgguVXdM4vY74JdwkyD4hSmpTW8jA==", "dev": true, "requires": { "@types/node": "*", + "jest-util": "^29.1.2", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -7808,55 +2526,23 @@ "esprima": "^4.0.0" } }, - "jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", - "dev": true, - "requires": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" - } - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "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 + }, "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true }, "jsonfile": { "version": "6.1.0", @@ -7879,15 +2565,11 @@ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } + "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 }, "locate-path": { "version": "5.0.0", @@ -7933,12 +2615,12 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, "merge-stream": { "version": "2.0.0", @@ -7952,13 +2634,13 @@ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.2", + "picomatch": "^2.3.1" } }, "mime": { @@ -7967,16 +2649,16 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, "mime-db": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", - "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==" + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" }, "mime-types": { - "version": "2.1.32", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", - "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "requires": { - "mime-db": "1.49.0" + "mime-db": "1.51.0" } }, "mimic-fn": { @@ -7986,50 +2668,45 @@ "dev": true }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "mri": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", - "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, "node-releases": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", - "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", "dev": true }, "normalize-path": { @@ -8047,17 +2724,10 @@ "path-key": "^3.0.0" } }, - "nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", - "dev": true - }, "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" }, "on-exit-leak-free": { "version": "0.2.0", @@ -8065,9 +2735,9 @@ "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==" }, "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "requires": { "ee-first": "1.1.1" } @@ -8089,20 +2759,6 @@ "mimic-fn": "^2.1.0" } }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -8127,11 +2783,17 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, - "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true + "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, + "requires": { + "@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" + } }, "parseurl": { "version": "1.3.3", @@ -8147,7 +2809,8 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true }, "path-key": { "version": "3.1.1", @@ -8164,7 +2827,7 @@ "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, "picocolors": { "version": "1.0.0", @@ -8179,20 +2842,66 @@ "dev": true }, "pino": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/pino/-/pino-7.6.4.tgz", - "integrity": "sha512-ktibPg3ttWONxYQ2Efk1zYbIvofD5zdd/ReoujK84ggEp0REflb9TsXavSjt8u1CdT2mMJe9QQ3ZpyOQxUKipA==", - "requires": { - "fast-redact": "^3.0.0", - "on-exit-leak-free": "^0.2.0", - "pino-abstract-transport": "v0.5.0", - "pino-std-serializers": "^4.0.0", - "process-warning": "^1.0.0", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.6.1.tgz", + "integrity": "sha512-fi+V2K98eMZjQ/uEHHSiMALNrz7HaFdKNYuyA3ZUrbH0f1e8sPFDmeRGzg7ZH2q4QDxGnJPOswmqlEaTAZeDPA==", + "requires": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.0.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", "quick-format-unescaped": "^4.0.3", - "real-require": "^0.1.0", - "safe-stable-stringify": "^2.1.0", - "sonic-boom": "^2.2.1", - "thread-stream": "^0.13.0" + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.1.0", + "thread-stream": "^2.0.0" + }, + "dependencies": { + "on-exit-leak-free": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz", + "integrity": "sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==" + }, + "pino-abstract-transport": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz", + "integrity": "sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==", + "requires": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "pino-std-serializers": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.0.0.tgz", + "integrity": "sha512-mMMOwSKrmyl+Y12Ri2xhH1lbzQxwwpuru9VjyJpgFIH4asSj88F2csdMwN6+M5g1Ll4rmsYghHLQJw81tgZ7LQ==" + }, + "readable-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.2.0.tgz", + "integrity": "sha512-gJrBHsaI3lgBoGMW/jHZsQ/o/TIWiu5ENCJG1BB7fuCKzpFM8GaS2UoBVt9NO+oI+3FcrBNbUkl3ilDe09aY4A==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10" + } + }, + "real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==" + }, + "sonic-boom": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.2.0.tgz", + "integrity": "sha512-SbbZ+Kqj/XIunvIAgUZRlqd6CGQYq71tRRbXR92Za8J/R3Yh4Av+TWENiSiEgnlwckYLyP0YZQWVfyNC0dzLaA==", + "requires": { + "atomic-sleep": "^1.0.0" + } + } } }, "pino-abstract-transport": { @@ -8213,25 +2922,95 @@ "get-caller-file": "^2.0.5", "pino": "^7.0.5", "pino-std-serializers": "^4.0.0" + }, + "dependencies": { + "pino": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-7.11.0.tgz", + "integrity": "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==", + "requires": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.0.0", + "on-exit-leak-free": "^0.2.0", + "pino-abstract-transport": "v0.5.0", + "pino-std-serializers": "^4.0.0", + "process-warning": "^1.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.1.0", + "safe-stable-stringify": "^2.1.0", + "sonic-boom": "^2.2.1", + "thread-stream": "^0.15.1" + } + }, + "process-warning": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", + "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" + }, + "thread-stream": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.15.2.tgz", + "integrity": "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==", + "requires": { + "real-require": "^0.1.0" + } + } } }, "pino-pretty": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-7.5.1.tgz", - "integrity": "sha512-xEOUJiokdBGcZ9d0v7OY6SqEp+rrVH2drE3bHOUsK8elw44eh9V83InZqeL1dFwgD1IDnd6crUoec3hIXxfdBQ==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-9.1.1.tgz", + "integrity": "sha512-iJrnjgR4FWQIXZkUF48oNgoRI9BpyMhaEmihonHeCnZ6F50ZHAS4YGfGBT/ZVNsPmd+hzkIPGzjKdY08+/yAXw==", "requires": { - "args": "^5.0.1", "colorette": "^2.0.7", "dateformat": "^4.6.3", - "fast-safe-stringify": "^2.0.7", + "fast-copy": "^3.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^4.0.1", "joycon": "^3.1.1", - "pino-abstract-transport": "^0.5.0", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", "pump": "^3.0.0", - "readable-stream": "^3.6.0", - "rfdc": "^1.3.0", + "readable-stream": "^4.0.0", "secure-json-parse": "^2.4.0", - "sonic-boom": "^2.2.0", + "sonic-boom": "^3.0.0", "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "on-exit-leak-free": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz", + "integrity": "sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==" + }, + "pino-abstract-transport": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz", + "integrity": "sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==", + "requires": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "readable-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.2.0.tgz", + "integrity": "sha512-gJrBHsaI3lgBoGMW/jHZsQ/o/TIWiu5ENCJG1BB7fuCKzpFM8GaS2UoBVt9NO+oI+3FcrBNbUkl3ilDe09aY4A==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10" + } + }, + "sonic-boom": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.2.0.tgz", + "integrity": "sha512-SbbZ+Kqj/XIunvIAgUZRlqd6CGQYq71tRRbXR92Za8J/R3Yh4Av+TWENiSiEgnlwckYLyP0YZQWVfyNC0dzLaA==", + "requires": { + "atomic-sleep": "^1.0.0" + } + } } }, "pino-std-serializers": { @@ -8240,9 +3019,9 @@ "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==" }, "pirates": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.4.tgz", - "integrity": "sha512-ZIrVPH+A52Dw84R0L3/VS9Op04PuQ2SEoJL6bkshmiTic/HldyW9Tf7oH5mhJZBK7NmDx27vSMrYEXPXclpDKw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", "dev": true }, "pkg-dir": { @@ -8254,21 +3033,15 @@ "find-up": "^4.0.0" } }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, "pretty-format": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.4.6.tgz", - "integrity": "sha512-NblstegA1y/RJW2VyML+3LlpFjzx62cUrtBIKIWDXEDkjNeleA7Od7nrzcs/VLQvAeV4CgSYhrN39DRN88Qi/g==", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.1.2.tgz", + "integrity": "sha512-CGJ6VVGXVRP2o2Dorl4mAwwvDWT25luIsYhkyVQW32E4nL+TgW939J7LlKT/npq5Cpq6j3s+sy+13yk7xYpBmg==", "dev": true, "requires": { - "ansi-regex": "^5.0.1", + "@jest/schemas": "^29.0.0", "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" + "react-is": "^18.0.0" }, "dependencies": { "ansi-styles": { @@ -8279,10 +3052,15 @@ } } }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, "process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.0.0.tgz", + "integrity": "sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww==" }, "prompts": { "version": "2.4.2", @@ -8303,12 +3081,6 @@ "ipaddr.js": "1.9.1" } }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "dev": true - }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -8324,9 +3096,12 @@ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, "qs": { - "version": "6.9.6", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz", - "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==" + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "requires": { + "side-channel": "^1.0.4" + } }, "quick-format-unescaped": { "version": "4.0.4", @@ -8339,20 +3114,20 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raw-body": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz", - "integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", "requires": { - "bytes": "3.1.1", - "http-errors": "1.8.1", + "bytes": "3.1.2", + "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, "readable-stream": { @@ -8373,16 +3148,16 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, "resolve": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", - "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", "dev": true, "requires": { - "is-core-module": "^2.8.0", + "is-core-module": "^2.9.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } @@ -8408,20 +3183,6 @@ "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", "dev": true }, - "rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -8429,28 +3190,19 @@ "dev": true }, "safe-stable-stringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.2.0.tgz", - "integrity": "sha512-C6AuMdYPuPV/P1leplHNu0lgc2LAElq/g3TdoksDCIVtBhr78o/CH03bt/9SKqugFbKU9CUjsNlCu0fjtQzQUw==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==" }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dev": true, - "requires": { - "xmlchars": "^2.2.0" - } - }, "secure-json-parse": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.4.0.tgz", - "integrity": "sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.5.0.tgz", + "integrity": "sha512-ZQruFgZnIWH+WyO9t5rWt4ZEGqCKPwhiw+YbzTwpmT9elgLrLcfuyUiSnwwjUiVy9r4VM3urtbNF1xmEh9IL2w==" }, "semver": { "version": "6.3.0", @@ -8459,23 +3211,23 @@ "dev": true }, "send": { - "version": "0.17.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", - "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "requires": { "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", + "depd": "2.0.0", + "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "1.8.1", + "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "range-parser": "~1.2.1", - "statuses": "~1.5.0" + "statuses": "2.0.1" }, "dependencies": { "ms": { @@ -8486,14 +3238,14 @@ } }, "serve-static": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", - "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.17.2" + "send": "0.18.0" } }, "setprototypeof": { @@ -8520,7 +3272,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, "requires": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -8528,9 +3279,9 @@ } }, "signal-exit": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", - "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", + "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 }, "sisteransi": { @@ -8560,9 +3311,9 @@ "dev": true }, "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -8577,7 +3328,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, "stack-utils": { @@ -8587,33 +3338,26 @@ "dev": true, "requires": { "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } } }, "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, "stream-shift": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, - "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==", - "requires": { - "safe-buffer": "~5.2.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - } - } - }, "string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -8635,6 +3379,21 @@ "strip-ansi": "^6.0.1" } }, + "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==", + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -8662,44 +3421,32 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, "superagent": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.1.tgz", - "integrity": "sha512-CQ2weSS6M+doIwwYFoMatklhRbx6sVNdB99OEJ5czcP3cng76Ljqus694knFWgOj3RkrtxZqIgpe6vhe0J7QWQ==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.2.tgz", + "integrity": "sha512-QtYZ9uaNAMexI7XWl2vAXAh0j4q9H7T0WVEI/y5qaUB3QLwxo+voUgCQ217AokJzUTIVOp0RTo7fhZrwhD7A2Q==", "dev": true, "requires": { "component-emitter": "^1.3.0", "cookiejar": "^2.1.3", - "debug": "^4.3.3", + "debug": "^4.3.4", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.0", "formidable": "^2.0.1", "methods": "^1.1.2", - "mime": "^2.5.0", - "qs": "^6.10.1", - "readable-stream": "^3.6.0", - "semver": "^7.3.5" + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.7" }, "dependencies": { "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" } }, - "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, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, "mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -8713,18 +3460,18 @@ "dev": true }, "qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "dev": true, "requires": { "side-channel": "^1.0.4" } }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -8733,13 +3480,13 @@ } }, "supertest": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.2.2.tgz", - "integrity": "sha512-wCw9WhAtKJsBvh07RaS+/By91NNE0Wh0DN19/hWPlBOU8tAfOtbZoVSV4xXeoKoxgPx0rx2y+y+8660XtE7jzg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.0.tgz", + "integrity": "sha512-QgWju1cNoacP81Rv88NKkQ4oXTzGg0eNZtOoxp1ROpbS4OHY/eK5b8meShuFtdni161o5X0VQvgo7ErVyKK+Ow==", "dev": true, "requires": { "methods": "^1.1.2", - "superagent": "^7.1.0" + "superagent": "^8.0.0" } }, "supports-color": { @@ -8752,9 +3499,9 @@ } }, "supports-hyperlinks": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", - "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", "dev": true, "requires": { "has-flag": "^4.0.0", @@ -8767,12 +3514,6 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, - "symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, "terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -8792,22 +3533,39 @@ "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "thread-stream": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.13.0.tgz", - "integrity": "sha512-kTMZeX4Dzlb1zZ00/01aerGaTw2i8NE4sWF0TvF1uXewRhCiUjCvatQkvxIvFqauWG2ADFS2Wpd3qBeYL9i3dg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.2.0.tgz", + "integrity": "sha512-rUkv4/fnb4rqy/gGy7VuqK6wE1+1DOCOWy4RMeaV69ZHMP11tQKZvZSip1yTgrKCMZzEMcCL/bKfHvSfDHx+iQ==", "requires": { - "real-require": "^0.1.0" + "real-require": "^0.2.0" + }, + "dependencies": { + "real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==" + } } }, - "throat": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", - "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", - "dev": true - }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -8817,7 +3575,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "dev": true }, "to-regex-range": { @@ -8834,57 +3592,6 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, - "tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", - "dev": true, - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.1.2" - }, - "dependencies": { - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "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 - } - } - }, - "tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dev": true, - "requires": { - "punycode": "^2.1.1" - }, - "dependencies": { - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - } - } - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -8906,15 +3613,6 @@ "mime-types": "~2.1.24" } }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -8923,7 +3621,17 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "update-browserslist-db": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz", + "integrity": "sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } }, "util-deprecate": { "version": "1.0.2", @@ -8933,25 +3641,17 @@ "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, "v8-to-istanbul": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.0.tgz", - "integrity": "sha512-/PRhfd8aTNp9Ggr62HPzXg2XasNFGy5PBt0Rp04du7/8GNNSgxFL6WBTkgMKSL9bFjH+8kKEG3f37FmxiTqUUA==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", + "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==", "dev": true, "requires": { + "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } + "convert-source-map": "^1.6.0" } }, "validator": { @@ -8962,25 +3662,7 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "dev": true, - "requires": { - "browser-process-hrtime": "^1.0.0" - } - }, - "w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", - "dev": true, - "requires": { - "xml-name-validator": "^3.0.0" - } + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, "walker": { "version": "1.0.8", @@ -8991,38 +3673,6 @@ "makeerror": "1.0.12" } }, - "webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true - }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.24" - } - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dev": true, - "requires": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - } - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9032,12 +3682,6 @@ "isexe": "^2.0.0" } }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -9055,36 +3699,15 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", "dev": true, "requires": { "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" + "signal-exit": "^3.0.7" } }, - "ws": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", - "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==", - "dev": true, - "requires": {} - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -9098,24 +3721,30 @@ "dev": true }, "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", "dev": true, "requires": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.0.0" } }, "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true } } diff --git a/Samples/UniversalDecoder/package.json b/Samples/UniversalDecoder/package.json index 6b5a8dfc9a..1f178c551f 100644 --- a/Samples/UniversalDecoder/package.json +++ b/Samples/UniversalDecoder/package.json @@ -13,20 +13,24 @@ "codecs": "rm -fR node_modules/lorawan-devices codecs/* && git clone --depth 1 https://github.com/TheThingsNetwork/lorawan-devices.git node_modules/lorawan-devices && node tools/copy-codecs.js" }, "dependencies": { - "express": "^4.17.2", + "esprima-next": "^5.8.4", + "express": "^4.18.1", "express-pino-logger": "^7.0.0", - "express-validator": "^6.14.0", - "fs-extra": "^10.0.0", - "glob": "^7.1.7", - "pino": "^7.6.4", - "pino-pretty": "^7.5.1" + "express-validator": "^6.14.1", + "fs-extra": "^10.1.0", + "glob": "^8.0.3", + "pino": "^8.6.1", + "pino-pretty": "^9.1.1" }, "devDependencies": { - "jest": "^27.4.7", - "supertest": "^6.2.2" + "jest": "^29.1.2", + "supertest": "^6.3.0" }, "jest": { "testEnvironment": "node", + "transformIgnorePatterns": [ + "/codecs/" + ], "coveragePathIgnorePatterns": [ "/node_modules/" ] diff --git a/Samples/UniversalDecoder/tests/app.routes.spec.js b/Samples/UniversalDecoder/tests/app.routes.spec.js index a393f085c8..2cd992602a 100644 --- a/Samples/UniversalDecoder/tests/app.routes.spec.js +++ b/Samples/UniversalDecoder/tests/app.routes.spec.js @@ -112,24 +112,26 @@ describe('tpl110-0292', () => { }); }); -describe('lw001-bg', () => { - it('should decode all 1s', async () => { - const res = await sendRequest('lw001-bg', '1111111111111111', 1); - expect(res.statusCode).toEqual(200); - expect(res.body).toEqual({ - value: { - barometer: 65792.1, - batterylevel: 0.01, - devicestatus: "1", - firmwareversion: 101, - humidity: 25.61, - macversion: 0, - temperature: -19.39, - type: "Device Information Packet" - }, - }); - }); -}); +// The following test has been disabled because decoder script was changed +// A fix in copy-codecs.js is required to re-enable the import of this decoder #1833 +// describe('lw001-bg', () => { +// it('should decode all 1s', async () => { +// const res = await sendRequest('lw001-bg', '1111111111111111', 1); +// expect(res.statusCode).toEqual(200); +// expect(res.body).toEqual({ +// value: { +// barometer: 65792.1, +// batterylevel: 0.01, +// devicestatus: "1", +// firmwareversion: 101, +// humidity: 25.61, +// macversion: 0, +// temperature: -19.39, +// type: "Device Information Packet" +// }, +// }); +// }); +// }); function sendRequest(decoderName, payload, fPort) { return request(app) diff --git a/Samples/UniversalDecoder/tools/copy-codecs.js b/Samples/UniversalDecoder/tools/copy-codecs.js index cbaabd26fe..086dae2bc3 100644 --- a/Samples/UniversalDecoder/tools/copy-codecs.js +++ b/Samples/UniversalDecoder/tools/copy-codecs.js @@ -3,12 +3,14 @@ const glob = require('glob'); const fs = require('fs'); const fse = require('fs-extra'); +const path = require('path'); +const esprima = require('esprima-next'); var args = process.argv.slice(2); const srcDir = args[0] || './node_modules/lorawan-devices/vendor'; const dstDir = args[1] || './codecs'; - -glob.sync(`**/*`, +const indexFilePath = path.join(dstDir, 'index.js'); +const index = glob.sync(`**/*`, { cwd: srcDir, nodir: true, @@ -25,9 +27,20 @@ glob.sync(`**/*`, fse.copySync(srcPath, dstPath); if (f.endsWith(".js")) { - fs.appendFile(dstPath, '\nmodule.exports={decodeUplink};', function (err) { - if (err) throw err; - console.log(`Patching ${dstPath}`); - }); + // Sniff a top-level declaration for a function named "decodeUplink" + // and include the decoder if only one is found. + const tree = esprima.parseScript(fs.readFileSync(srcPath).toString()); + if (tree.body.find(n => n.type === 'FunctionDeclaration' && n.id.name === 'decodeUplink')) { + fs.appendFile(dstPath, '\nmodule.exports={decodeUplink};', function (err) { + if (err) throw err; + console.log(`Patching ${dstPath}`); + }); + return dstPath; + } } -}); + }) + .filter(f => f) + .map(f => [path.basename(f).split('.')[0], path.relative(dstDir, f).replace(/\\/g, '/')]); + +fs.writeFileSync(indexFilePath, + `module.exports = {\n${index.map(([k, v]) => `${JSON.stringify(k)}: require(${JSON.stringify('./' + v)})`).join(',\n ')}\n};\n`); diff --git a/Template/azuredeploy.json b/Template/azuredeploy.json index 47fbad2095..8dfafda51c 100644 --- a/Template/azuredeploy.json +++ b/Template/azuredeploy.json @@ -73,6 +73,13 @@ "metadata": { "description": "Controls whether observability is set up for IoT edge." } + }, + "useDiscoveryService": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Controls whether the standalone discovery service should be deployed." + } } }, "variables": { @@ -85,13 +92,15 @@ "firmwareUpgradesContainerName": "fwupgrades", "functionAppName": "[concat(parameters('uniqueSolutionPrefix'), 'function')]", "gitUsername": "Azure", - "functionZipBinary": "https://github.com/Azure/iotedge-lorawan-starterkit/releases/download/v2.0.0/function-2.0.0.zip", - "gitBranch": "v2.0.0", + "functionZipBinary": "https://github.com/Azure/iotedge-lorawan-starterkit/releases/download/v2.2.0/function-2.2.0.zip", + "discoveryServiceZipBinary": "https://github.com/Azure/iotedge-lorawan-starterkit/releases/download/v2.2.0/discoveryservice-2.2.0.zip", + "gitBranch": "v2.2.0", "storageAccountId": "[concat(resourceGroup().id, '/providers/', 'Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]", "iotHubOwnerPolicyName": "iothubowner", "appInsightName": "[concat(parameters('uniqueSolutionPrefix'), 'insight')]", "redisCacheName": "[concat(parameters('uniqueSolutionPrefix'), 'redis')]", "logAnalyticsName": "[concat(parameters('uniqueSolutionPrefix'), 'log')]", + "discoveryServiceWebAppName": "[concat(parameters('uniqueSolutionPrefix'), 'discovery')]", "deploymentsAPIVersion": "2021-04-01" }, "resources": [ @@ -213,8 +222,8 @@ "value": "[concat('https://raw.githubusercontent.com/',variables('gitUsername'),'/iotedge-lorawan-starterkit/',variables('gitBranch'),'/Template/deviceConfiguration.json')]" }, { - "name": "APPINSIGHTS_INSTRUMENTATIONKEY", - "value": "[reference(concat('microsoft.insights/components/', variables('appInsightName'))).InstrumentationKey]" + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "value": "[reference(concat('microsoft.insights/components/', variables('appInsightName'))).ConnectionString]" }, { "name": "WEBSITE_RUN_FROM_PACKAGE", @@ -290,13 +299,14 @@ "type": "Microsoft.Insights/components", "kind": "web", "name": "[variables('appInsightName')]", - "apiVersion": "2015-05-01", + "apiVersion": "2020-02-02", "location": "[resourceGroup().location]", "scale": null, "properties": { - "ApplicationId": "[variables('appInsightName')]" + "ApplicationId": "[variables('appInsightName')]", + "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces/', variables('logAnalyticsName'))]" }, - "dependsOn": [] + "dependsOn": [ "[resourceId('Microsoft.OperationalInsights/workspaces/', variables('logAnalyticsName'))]" ] }, { "name": "azuremonitoralerts", @@ -351,7 +361,6 @@ { "type": "Microsoft.OperationalInsights/workspaces", "apiVersion": "2021-06-01", - "condition": "[parameters('useAzureMonitorOnEdge')]", "name": "[variables('logAnalyticsName')]", "location": "[resourceGroup().location]", "properties": { @@ -376,6 +385,40 @@ "name": "Basic" } } + }, + { + "name": "discoveryservice", + "type": "Microsoft.Resources/deployments", + "apiVersion": "[variables('deploymentsAPIVersion')]", + "condition": "[parameters('useDiscoveryService')]", + "dependsOn": [ + "[resourceId('microsoft.insights/components/', variables('appInsightName'))]", + "[concat('Microsoft.Devices/iothubs/', variables('iotHubName'))]" + ], + "properties": { + "mode": "Incremental", + "templateLink": { + "uri": "[concat('https://raw.githubusercontent.com/',variables('gitUsername'),'/iotedge-lorawan-starterkit/',variables('gitBranch'),'/Template/discoveryService.json')]", + "contentVersion": "1.0.0.0" + }, + "parameters": { + "webAppName": { + "value": "[variables('discoveryServiceWebAppName')]" + }, + "iotHubName": { + "value": "[variables('iotHubName')]" + }, + "iotHubHostName": { + "value": "[reference(resourceId('Microsoft.Devices/IoTHubs', variables('iotHubName')), providers('Microsoft.Devices', 'IoTHubs').apiVersions[0]).hostName]" + }, + "discoveryZipUrl": { + "value": "[variables('discoveryServiceZipBinary')]" + }, + "applicationInsightsConnectionString": { + "value": "[reference(concat('microsoft.insights/components/', variables('appInsightName'))).ConnectionString]" + } + } + } } ], "outputs": {} diff --git a/Template/deviceConfiguration.json b/Template/deviceConfiguration.json index 4d8245f2d9..8b47deae18 100644 --- a/Template/deviceConfiguration.json +++ b/Template/deviceConfiguration.json @@ -14,14 +14,14 @@ "edgeAgent": { "type": "docker", "settings": { - "image": "mcr.microsoft.com/azureiotedge-agent:1.2.2", + "image": "mcr.microsoft.com/azureiotedge-agent:1.2.6", "createOptions": "{}" } }, "edgeHub": { "type": "docker", "settings": { - "image": "mcr.microsoft.com/azureiotedge-hub:1.2.2", + "image": "mcr.microsoft.com/azureiotedge-hub:1.2.6", "createOptions": "{ \"HostConfig\": { \"PortBindings\": {\"8883/tcp\": [ {\"HostPort\": \"8883\" } ], \"443/tcp\": [ { \"HostPort\": \"443\" } ], \"5671/tcp\": [ { \"HostPort\": \"5671\" }] } }}" }, "env": { @@ -46,7 +46,7 @@ "LoRaWanNetworkSrvModule": { "type": "docker", "settings": { - "image": "loraedge/lorawannetworksrvmodule:2.0.0", + "image": "loraedge/lorawannetworksrvmodule:2.2.0", "createOptions": "{\"ExposedPorts\": { \"5000/tcp\": {}}, \"HostConfig\": { \"PortBindings\": {\"5000/tcp\": [ { \"HostPort\": \"5000\", \"HostIp\":\"172.17.0.1\" } ]}}}" }, "version": "1.0", @@ -64,7 +64,7 @@ "LoRaBasicsStationModule": { "type": "docker", "settings": { - "image": "loraedge/lorabasicsstationmodule:2.0.0", + "image": "loraedge/lorabasicsstationmodule:2.2.0", "createOptions": " {\"HostConfig\": {\"NetworkMode\": \"host\", \"Privileged\": true }, \"NetworkingConfig\": {\"EndpointsConfig\": {\"host\": {} }}}" }, "env": { diff --git a/Template/discoveryService.json b/Template/discoveryService.json new file mode 100644 index 0000000000..2c0683b49e --- /dev/null +++ b/Template/discoveryService.json @@ -0,0 +1,106 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "webAppName": { + "type": "string", + "minLength": 2 + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "sku": { + "type": "string", + "defaultValue": "B1", + "metadata": { + "description": "The SKU of App Service Plan." + } + }, + "iotHubHostName": { + "type": "string" + }, + "discoveryZipUrl": { + "type": "string" + }, + "iotHubName": { + "type": "string" + }, + "applicationInsightsConnectionString": { + "type": "string" + }, + "roleNameGuid": { + "type": "string", + "defaultValue": "[guid(resourceGroup().id, 'twincontributor')]", + "metadata": { + "description": "A new GUID used to identify the role assignment" + } + } + }, + "variables": { + "appServicePlanName": "[concat(parameters('webAppName'), 'plan')]", + "iotHubTwinContributorRoleId": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', '494bdba2-168f-4f31-a0a1-191d2f7c028c')]", + "aspNetCoreUrls": "http://0.0.0.0:80;https://0.0.0.0:443", + "webSitesApiVersion": "2021-03-01" + }, + "resources": [ + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2021-03-01", + "name": "[variables('appServicePlanName')]", + "location": "[parameters('location')]", + "sku": { + "name": "[parameters('sku')]" + } + }, + { + "type": "Microsoft.Web/sites", + "apiVersion": "[variables('webSitesApiVersion')]", + "name": "[parameters('webAppName')]", + "location": "[parameters('location')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]" + ], + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]", + "siteConfig": { + "appSettings": [ + { + "name": "IotHubHostName", + "value": "[parameters('iotHubHostName')]" + }, + { + "name": "WEBSITE_RUN_FROM_PACKAGE", + "value": "[parameters('discoveryZipUrl')]" + }, + { + "name": "ASPNETCORE_URLS", + "value": "[variables('aspNetCoreUrls')]" + }, + { + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "value": "[parameters('applicationInsightsConnectionString')]" + } + ], + "webSocketsEnabled": true + } + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-08-01-preview", + "name": "[parameters('roleNameGuid')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', parameters('webAppName'))]" + ], + "scope": "[concat('Microsoft.Devices/IotHubs/', parameters('iotHubName'))]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), variables('webSitesApiVersion'), 'Full').identity.principalId]", + "roleDefinitionId": "[variables('iotHubTwinContributorRoleId')]" + } + } + ] +} diff --git a/Tests/Common/DisposableValue.cs b/Tests/Common/DisposableValue.cs index e6c6b00629..ff427019d3 100644 --- a/Tests/Common/DisposableValue.cs +++ b/Tests/Common/DisposableValue.cs @@ -6,16 +6,43 @@ namespace LoRaWan.Tests.Common { using System; + using System.Threading.Tasks; + /// + /// Used for setting up/mocking classes for testing, if intermediate disposables need to be disposed at the end of the test. + /// For example, when creating an instance of MessageDispatcher for testing, we need to create many intermediate + /// disposable instances to be passed as arguments (LoRaDevice, etc), which we don't need by their value, but which still need to be disposed of. + /// With this class we execute an arbitrary dispose action after we use some value. + /// public sealed class DisposableValue : IDisposable { - private readonly IDisposable disposable; + private readonly Action dispose; - public DisposableValue(T value, IDisposable disposable) => - (Value, this.disposable) = (value, disposable); + public DisposableValue(T value, IDisposable disposable) + : this(value, () => disposable.Dispose()) + { } + + public DisposableValue(T value, Action dispose) => + (Value, this.dispose) = (value, dispose); + + public T Value { get; } + + public void Dispose() => this.dispose(); + } + + public sealed class AsyncDisposableValue : IAsyncDisposable + { + private readonly Func dispose; + + public AsyncDisposableValue(T value, IAsyncDisposable disposable) + : this(value, () => disposable.DisposeAsync()) + { } + + public AsyncDisposableValue(T value, Func dispose) => + (Value, this.dispose) = (value, dispose); public T Value { get; } - public void Dispose() => this.disposable.Dispose(); + public ValueTask DisposeAsync() => dispose(); } } diff --git a/Tests/Common/EventHubDataCollector.cs b/Tests/Common/EventHubDataCollector.cs index 0ff12e0bc7..18040958b7 100644 --- a/Tests/Common/EventHubDataCollector.cs +++ b/Tests/Common/EventHubDataCollector.cs @@ -6,55 +6,63 @@ namespace LoRaWan.Tests.Common using System; using System.Collections.Concurrent; using System.Collections.Generic; + using System.Linq; using System.Text; + using System.Threading; using System.Threading.Tasks; - using Microsoft.Azure.EventHubs; + using Azure.Messaging.EventHubs; + using Azure.Messaging.EventHubs.Consumer; - public class EventHubDataCollector : IPartitionReceiveHandler, IDisposable + public sealed class EventHubDataCollector : IAsyncDisposable { private readonly ConcurrentQueue events; private readonly string connectionString; - private EventHubClient eventHubClient; - private readonly List receivers; - private readonly HashSet>> subscribers; - - public bool LogToConsole { get; set; } = true; - - public string ConsumerGroupName { get; set; } = "$Default"; + private readonly EventHubConsumerClient consumer; + private readonly string consumerGroupName; + private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + private bool started; + private Task receiveAsync; public EventHubDataCollector(string connectionString) : this(connectionString, null) - { - } + { } public EventHubDataCollector(string connectionString, string consumerGroupName) { this.connectionString = connectionString; - this.eventHubClient = EventHubClient.CreateFromConnectionString(connectionString); + this.consumerGroupName = !string.IsNullOrEmpty(consumerGroupName) ? consumerGroupName : EventHubConsumerClient.DefaultConsumerGroupName; + this.consumer = new EventHubConsumerClient(this.consumerGroupName, connectionString); this.events = new ConcurrentQueue(); - this.receivers = new List(); - if (!string.IsNullOrEmpty(consumerGroupName)) - ConsumerGroupName = consumerGroupName; - - this.subscribers = new HashSet>>(); } public async Task StartAsync() { - if (this.receivers.Count > 0) + if (this.started) throw new InvalidOperationException("Already started"); - if (LogToConsole) + this.started = true; + TestLogger.Log($"Connecting to IoT Hub Event Hub @{this.connectionString} using consumer group {this.consumerGroupName}"); + + var eventPosition = EventPosition.FromEnqueuedTime(DateTimeOffset.Now.Subtract(TimeSpan.FromMinutes(1))); + this.receiveAsync = + Task.WhenAll(from partitionId in await this.consumer.GetPartitionIdsAsync(this.cancellationTokenSource.Token) + select ProcessEventsAsync(partitionId, eventPosition, this.cancellationTokenSource.Token)); + } + + public async Task StopAsync() + { + if (!this.started) + throw new InvalidOperationException("Processing has not yet started."); + + this.cancellationTokenSource.Cancel(); + + try { - TestLogger.Log($"Connecting to IoT Hub Event Hub @{this.connectionString} using consumer group {ConsumerGroupName}"); + await this.receiveAsync; } - - var rti = await this.eventHubClient.GetRuntimeInformationAsync(); - foreach (var partitionId in rti.PartitionIds) + catch (OperationCanceledException) { - var receiver = this.eventHubClient.CreateReceiver(ConsumerGroupName, partitionId, EventPosition.FromEnqueuedTime(DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(1)))); - receiver.SetReceiveHandler(this); - this.receivers.Add(receiver); + // expected cancellation of receive operation. } } @@ -64,95 +72,35 @@ public void ResetEvents() this.events.Clear(); } - public void Subscribe(Action> subscriber) - { - this.subscribers.Add(subscriber); - } - - public void Unsubscribe(Action> subscriber) - { - this.subscribers.Remove(subscriber); - } - public IReadOnlyCollection Events => this.events; - public Task ProcessEventsAsync(IEnumerable events) + public async Task ProcessEventsAsync(string partitionId, EventPosition eventPosition, CancellationToken cancellationToken) { - try + await foreach (var item in this.consumer.ReadEventsFromPartitionAsync(partitionId, eventPosition, cancellationToken)) { - if (this.subscribers.Count > 0) + try { - foreach (var subscriber in this.subscribers) - { - subscriber(events); - } + var eventData = item.Data; + this.events.Enqueue(eventData); + var bodyText = Encoding.UTF8.GetString(eventData.EventBody); + TestLogger.Log($"[IOTHUB] {bodyText}"); } - - foreach (var item in events) + catch (Exception ex) { - this.events.Enqueue(item); - - if (LogToConsole) - { - var bodyText = Encoding.UTF8.GetString(item.Body); - TestLogger.Log($"[IOTHUB] {bodyText}"); - } + TestLogger.Log($"Error processing iot hub event. {ex}"); } } - catch (Exception ex) - { - TestLogger.Log($"Error processing iot hub event. {ex}"); - } - - return Task.FromResult(0); } - public Task ProcessErrorAsync(Exception error) => - Console.Error.WriteLineAsync(error.ToString()); - - public int MaxBatchSize { get; set; } = 32; - - private bool disposedValue; // To detect redundant calls - - protected virtual void Dispose(bool disposing) + public async ValueTask DisposeAsync() { - TestLogger.Log($"{nameof(EventHubDataCollector)} disposed"); + this.cancellationTokenSource.Cancel(); + this.cancellationTokenSource.Dispose(); - if (!this.disposedValue) - { - if (disposing) - { - for (var i = this.receivers.Count - 1; i >= 0; i--) - { - try - { - this.receivers[i].SetReceiveHandler(null); - this.receivers[i].Close(); - } - catch (Exception ex) - { - TestLogger.Log($"Error closing event hub receiver: {ex}"); - } - - this.receivers.RemoveAt(i); - } - - this.eventHubClient.Close(); - this.eventHubClient = null; - } + await this.consumer.CloseAsync(); + await this.consumer.DisposeAsync(); - // TODO: free unmanaged resources(unmanaged objects) and override a finalizer below. - // TODO: set large fields to null. - this.disposedValue = true; - } - } - - // This code added to correctly implement the disposable pattern. - public void Dispose() - { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - Dispose(true); - GC.SuppressFinalize(this); + TestLogger.Log($"{nameof(EventHubDataCollector)} disposed"); } } } diff --git a/Tests/Common/IntegrationTestFixtureBase.Asserts.cs b/Tests/Common/IntegrationTestFixtureBase.Asserts.cs index 3e5a5993b0..170be0c68e 100644 --- a/Tests/Common/IntegrationTestFixtureBase.Asserts.cs +++ b/Tests/Common/IntegrationTestFixtureBase.Asserts.cs @@ -8,7 +8,7 @@ namespace LoRaWan.Tests.Common using System.Linq; using System.Text; using System.Threading.Tasks; - using Microsoft.Azure.EventHubs; + using Azure.Messaging.EventHubs; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; @@ -363,7 +363,7 @@ private async Task SearchIoTHubLogs(Func foreach (var item in IoTHubMessages.Events) { - var bodyText = item.Body.Count > 0 ? Encoding.UTF8.GetString(item.Body) : string.Empty; + var bodyText = Encoding.UTF8.GetString(item.EventBody); var searchLogEvent = new SearchLogEvent { Message = bodyText, @@ -409,7 +409,7 @@ internal async Task SearchIoTHubMessageAsync(Func 0 ? Encoding.UTF8.GetString(item.Body) : string.Empty; + var bodyText = Encoding.UTF8.GetString(item.EventBody); var searchLogEvent = new SearchLogEvent { SourceId = item.GetDeviceId(), diff --git a/Tests/Common/IntegrationTestFixtureBase.cs b/Tests/Common/IntegrationTestFixtureBase.cs index 7e015254a1..c79a1b15a0 100644 --- a/Tests/Common/IntegrationTestFixtureBase.cs +++ b/Tests/Common/IntegrationTestFixtureBase.cs @@ -7,9 +7,12 @@ namespace LoRaWan.Tests.Common using System.Collections.Generic; using System.Globalization; using System.Linq; + using System.Net.Http; using System.Text; using System.Threading.Tasks; + using LoRaTools; using LoRaTools.CommonAPI; + using LoRaTools.IoTHubImpl; using LoRaTools.Utils; using LoRaWan.NetworkServer.BasicsStation; using Microsoft.Azure.Devices; @@ -30,7 +33,7 @@ public abstract partial class IntegrationTestFixtureBase : IDisposable, IAsyncLi private const int C2dExpiryTime = 5; public const string MESSAGE_IDENTIFIER_PROPERTY_NAME = "messageIdentifier"; - private RegistryManager registryManager; + private IDeviceRegistryManager registryManager; private TcpLogListener tcpLogListener; public TestConfiguration Configuration { get; } @@ -98,14 +101,30 @@ public virtual void ClearLogs() this.tcpLogListener?.ResetEvents(); } - public Task DisposeAsync() => Task.FromResult(0); + public async Task DisposeAsync() + { + if (IoTHubMessages is { } iotHubMessages) + { + try + { + await iotHubMessages.StopAsync(); + } + finally + { + await iotHubMessages.DisposeAsync(); + } + } + } - private RegistryManager GetRegistryManager() + private IDeviceRegistryManager GetRegistryManager() { - return this.registryManager ??= RegistryManager.CreateFromConnectionString(Configuration.IoTHubConnectionString); +#pragma warning disable CA2000 // Dispose objects before losing scope + return this.registryManager ??= IoTHubRegistryManager.CreateWithProvider(() => + RegistryManager.CreateFromConnectionString(TestConfiguration.GetConfiguration().IoTHubConnectionString), new MockHttpClientFactory(), null); +#pragma warning restore CA2000 // Dispose objects before losing scope } - public async Task GetTwinAsync(string deviceId) + public async Task GetTwinAsync(string deviceId) { return await GetRegistryManager().GetTwinAsync(deviceId); } @@ -294,7 +313,7 @@ public async Task UpdateExistingConcentratorThumbprint(StationEui stationEui, Fu TestLogger.Log($"Updating IoT Hub twin for concentrator {stationEui}..."); var registryManager = GetRegistryManager(); var stationDeviceId = GetDeviceId(stationEui); - var getDeviceResult = await registryManager.GetDeviceAsync(stationDeviceId); + var getDeviceResult = await registryManager.GetTwinAsync(stationDeviceId); if (getDeviceResult == null) throw new InvalidOperationException("Concentrator should exist in IoT Hub"); var deviceTwin = await registryManager.GetTwinAsync(stationDeviceId); @@ -313,7 +332,7 @@ public async Task UpdateExistingConcentratorCrcValues(StationEui stationEui, uin TestLogger.Log($"Updating IoT Hub twin for concentrator {stationEui}..."); var registryManager = GetRegistryManager(); var stationDeviceId = GetDeviceId(stationEui); - var getDeviceResult = await registryManager.GetDeviceAsync(stationDeviceId); + var getDeviceResult = await registryManager.GetTwinAsync(stationDeviceId); if (getDeviceResult == null) throw new InvalidOperationException("Concentrator should exist in IoT Hub"); var deviceTwin = await registryManager.GetTwinAsync(stationDeviceId); @@ -332,7 +351,7 @@ public async Task UpdateExistingFirmwareUpgradeValues(StationEui stationEui, uin TestLogger.Log($"Updating IoT Hub twin for fw upgrades of concentrator {stationEui}..."); var registryManager = GetRegistryManager(); var stationDeviceId = GetDeviceId(stationEui); - var getDeviceResult = await registryManager.GetDeviceAsync(stationDeviceId); + var getDeviceResult = await registryManager.GetTwinAsync(stationDeviceId); if (getDeviceResult == null) throw new InvalidOperationException("Concentrator should exist in IoT Hub"); var deviceTwin = await registryManager.GetTwinAsync(stationDeviceId); @@ -363,16 +382,15 @@ private async Task CreateOrUpdateDevicesAsync() testDevice.DeviceID = deviceID; } - var getDeviceResult = await registryManager.GetDeviceAsync(testDevice.DeviceID); + var getDeviceResult = await registryManager.GetTwinAsync(testDevice.DeviceID); if (getDeviceResult == null) { TestLogger.Log($"Device {testDevice.DeviceID} does not exist. Creating"); - var device = new Device(testDevice.DeviceID); var twin = new Twin(testDevice.DeviceID); twin.Properties.Desired = new TwinCollection(JsonConvert.SerializeObject(testDevice.GetDesiredProperties())); TestLogger.Log($"Creating device {testDevice.DeviceID}"); - await registryManager.AddDeviceWithTwinAsync(device, twin); + await registryManager.AddDeviceAsync(new IoTHubDeviceTwin(twin)); } else { @@ -400,7 +418,7 @@ private async Task CreateOrUpdateDevicesAsync() var patch = new Twin(); patch.Properties.Desired = new TwinCollection(JsonConvert.SerializeObject(desiredProperties)); - await registryManager.UpdateTwinAsync(testDevice.DeviceID, patch, deviceTwin.ETag); + await registryManager.UpdateTwinAsync(testDevice.DeviceID, new IoTHubDeviceTwin(patch), deviceTwin.ETag); TestLogger.Log($"Update twin for device {testDevice.DeviceID}"); break; } @@ -430,10 +448,7 @@ protected virtual void Dispose(bool disposing) { AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException; - IoTHubMessages?.Dispose(); IoTHubMessages = null; - this.registryManager?.Dispose(); - this.registryManager = null; this.tcpLogListener?.Dispose(); this.tcpLogListener = null; diff --git a/Tests/E2E/LoRaAPIHelper.cs b/Tests/Common/LoRaAPIHelper.cs similarity index 98% rename from Tests/E2E/LoRaAPIHelper.cs rename to Tests/Common/LoRaAPIHelper.cs index a70f2b8f47..53df906559 100644 --- a/Tests/E2E/LoRaAPIHelper.cs +++ b/Tests/Common/LoRaAPIHelper.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace LoRaWan.Tests.E2E +namespace LoRaWan.Tests.Common { using System; using System.Net.Http; diff --git a/Tests/Common/LoRaDeviceTwin.cs b/Tests/Common/LoRaDeviceTwin.cs new file mode 100644 index 0000000000..816681e5b0 --- /dev/null +++ b/Tests/Common/LoRaDeviceTwin.cs @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Common +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using LoRaTools.Regions; + using LoRaWan.NetworkServer; + using Microsoft.Azure.Devices.Shared; + + public static class LoRaDeviceTwin + { + public static Twin Create(LoRaDesiredTwinProperties? desiredProperties = null, LoRaReportedTwinProperties? reportedProperties = null) + { + var twin = new Twin(); + + var zeroProperties = Enumerable.Empty>(); + + var properties = + from ps in new[] + { + from e in desiredProperties ?? zeroProperties + select (twin.Properties.Desired, e.Key, e.Value), + from e in reportedProperties ?? zeroProperties + select (twin.Properties.Reported, e.Key, e.Value), + } + from p in ps + select p; + + foreach (var (target, key, value) in properties) + { +#pragma warning disable IDE0010 // Add missing cases (false positive) + switch (value) + { + case null: + break; + case uint or int or bool or (Enum and not (LoRaRegionType or LoRaDeviceClassType)): + target[key] = value; + break; + case IConvertible convertible: + target[key] = convertible.ToString(CultureInfo.InvariantCulture); + break; + case var obj: + target[key] = obj.ToString(); + break; + } +#pragma warning restore IDE0010 // Add missing cases + } + + return twin; + } + } + + public abstract record LoRaTwinProperties : IEnumerable> + { + public abstract IEnumerator> GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + public sealed record LoRaDesiredTwinProperties : LoRaTwinProperties + { + public DevEui? DevEui { get; init; } + public DevAddr? DevAddr { get; init; } + public JoinEui? JoinEui { get; init; } + public AppKey? AppKey { get; init; } + public AppSessionKey? AppSessionKey { get; init; } + public NetworkSessionKey? NetworkSessionKey { get; init; } + public string? GatewayId { get; init; } + public string? SensorDecoder { get; init; } + public bool? Supports32BitFCnt { get; init; } + public bool? AbpRelaxMode { get; init; } + public uint? FCntUpStart { get; init; } + public uint? FCntDownStart { get; init; } + public uint? FCntResetCounter { get; init; } + public int? Rx1DROffset { get; init; } + public DataRateIndex? Rx2DataRate { get; init; } + public ReceiveWindowNumber? PreferredWindow { get; init; } + public int? RxDelay { get; init; } + public LoRaDeviceClassType? ClassType { get; init; } + public TimeSpan? KeepAliveTimeout { get; init; } + public uint? Version { get; init; } + public bool? DownlinkEnabled { get; init; } + public int? Cn470JoinChannel { get; init; } + + public override IEnumerator> GetEnumerator() + { + yield return KeyValuePair.Create("DevEUI", (object?)DevEui); + yield return KeyValuePair.Create("DevAddr", (object?)DevAddr); + yield return KeyValuePair.Create("AppEUI", (object?)JoinEui); + yield return KeyValuePair.Create("AppKey", (object?)AppKey); + yield return KeyValuePair.Create("AppSKey", (object?)AppSessionKey); + yield return KeyValuePair.Create("NwkSKey", (object?)NetworkSessionKey); + yield return KeyValuePair.Create("GatewayID", (object?)GatewayId); + yield return KeyValuePair.Create("SensorDecoder", (object?)SensorDecoder); + yield return KeyValuePair.Create("Supports32BitFCnt", (object?)Supports32BitFCnt); + yield return KeyValuePair.Create("ABPRelaxMode", (object?)AbpRelaxMode); + yield return KeyValuePair.Create("FCntUpStart", (object?)FCntUpStart); + yield return KeyValuePair.Create("FCntDownStart", (object?)FCntDownStart); + yield return KeyValuePair.Create("FCntResetCounter", (object?)FCntResetCounter); + yield return KeyValuePair.Create("RX1DROffset", (object?)Rx1DROffset); + yield return KeyValuePair.Create("RX2DataRate", (object?)Rx2DataRate); + yield return KeyValuePair.Create("PreferredWindow", PreferredWindow switch + { + ReceiveWindowNumber.ReceiveWindow1 => 1, + ReceiveWindowNumber.ReceiveWindow2 => 2, + _ => (object?)null + }); + yield return KeyValuePair.Create("RXDelay", (object?)RxDelay); + yield return KeyValuePair.Create("ClassType", (object?)ClassType); + yield return KeyValuePair.Create("KeepAliveTimeout", (object?)KeepAliveTimeout?.TotalSeconds); + yield return KeyValuePair.Create("$version", (object?)Version); + yield return KeyValuePair.Create("Downlink", (object?)DownlinkEnabled); + yield return KeyValuePair.Create("CN470JoinChannel", (object?)Cn470JoinChannel); + } + } + + public sealed record LoRaReportedTwinProperties : LoRaTwinProperties + { + public uint? FCntUp { get; init; } + public uint? FCntUpStart { get; init; } + public uint? FCntDown { get; init; } + public uint? FCntDownStart { get; init; } + public AppSessionKey? AppSessionKey { get; init; } + public NetworkSessionKey? NetworkSessionKey { get; init; } + public DevAddr? DevAddr { get; init; } + public DevNonce? DevNonce { get; init; } + public uint? FCntResetCounter { get; init; } + public string? PreferredGatewayId { get; init; } + public LoRaRegionType? Region { get; init; } + public NetId? NetId { get; init; } + public DataRateIndex? Rx2DataRate { get; init; } + public StationEui? LastProcessingStation { get; init; } + public uint? Version { get; init; } + public int? Cn470JoinChannel { get; init; } + + public override IEnumerator> GetEnumerator() + { + yield return KeyValuePair.Create("FCntUp", (object?)FCntUp); + yield return KeyValuePair.Create("FCntUpStart", (object?)FCntUpStart); + yield return KeyValuePair.Create("FCntDown", (object?)FCntDown); + yield return KeyValuePair.Create("FCntDownStart", (object?)FCntDownStart); + yield return KeyValuePair.Create("AppSKey", (object?)AppSessionKey); + yield return KeyValuePair.Create("NwkSKey", (object?)NetworkSessionKey); + yield return KeyValuePair.Create("DevAddr", (object?)DevAddr); + yield return KeyValuePair.Create("DevNonce", (object?)DevNonce); + yield return KeyValuePair.Create("FCntResetCounter", (object?)FCntResetCounter); + yield return KeyValuePair.Create("PreferredGatewayID", (object?)PreferredGatewayId); + yield return KeyValuePair.Create("Region", (object?)Region); + yield return KeyValuePair.Create("NetId", (object?)NetId); + yield return KeyValuePair.Create("RX2DataRate", (object?)Rx2DataRate); + yield return KeyValuePair.Create("LastProcessingStationEui", (object?)LastProcessingStation); + yield return KeyValuePair.Create("$version", (object?)Version); + yield return KeyValuePair.Create("CN470JoinChannel", (object?)Cn470JoinChannel); + } + } + + public static class LoRaDeviceTwinExtensions + { + public static LoRaDesiredTwinProperties GetOtaaDesiredTwinProperties(this TestDeviceInfo testDeviceInfo) => + new LoRaDesiredTwinProperties + { + DevEui = testDeviceInfo.DevEui, + JoinEui = testDeviceInfo.AppEui ?? throw new InvalidOperationException($"{nameof(testDeviceInfo.AppEui)} must not be null."), + AppKey = testDeviceInfo.AppKey ?? throw new InvalidOperationException($"{nameof(testDeviceInfo.AppKey)} must not be null."), + SensorDecoder = testDeviceInfo.SensorDecoder ?? throw new InvalidOperationException($"{nameof(testDeviceInfo.SensorDecoder)} must not be null."), + ClassType = testDeviceInfo.ClassType, + GatewayId = testDeviceInfo.GatewayID, + }; + + public static LoRaReportedTwinProperties GetOtaaReportedTwinProperties(this SimulatedDevice simulatedDevice) => + new LoRaReportedTwinProperties + { + DevAddr = simulatedDevice.DevAddr, + AppSessionKey = simulatedDevice.AppSKey, + NetworkSessionKey = simulatedDevice.NwkSKey, + DevNonce = simulatedDevice.DevNonce, + NetId = simulatedDevice.NetId, + FCntDown = simulatedDevice.FrmCntDown, + FCntUp = simulatedDevice.FrmCntUp, + }; + + public static LoRaDesiredTwinProperties GetAbpDesiredTwinProperties(this TestDeviceInfo testDeviceInfo) => + new LoRaDesiredTwinProperties + { + AppSessionKey = testDeviceInfo.AppSKey ?? throw new InvalidOperationException($"{nameof(testDeviceInfo.AppSKey)} must not be null."), + NetworkSessionKey = testDeviceInfo.NwkSKey ?? throw new InvalidOperationException($"{nameof(testDeviceInfo.NwkSKey)} must not be null."), + DevAddr = testDeviceInfo.DevAddr ?? throw new InvalidOperationException($"{nameof(testDeviceInfo.DevAddr)} must not be null."), + SensorDecoder = testDeviceInfo.SensorDecoder ?? throw new InvalidOperationException($"{nameof(testDeviceInfo.SensorDecoder)} must not be null."), + ClassType = testDeviceInfo.ClassType, + GatewayId = testDeviceInfo.GatewayID, + }; + + public static LoRaReportedTwinProperties GetAbpReportedTwinProperties(this SimulatedDevice simulatedDevice) => + new LoRaReportedTwinProperties + { + FCntDown = simulatedDevice.FrmCntDown, + FCntUp = simulatedDevice.FrmCntUp, + }; + + public static Twin GetDefaultAbpTwin(this SimulatedDevice simulatedDevice) => + LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetAbpDesiredTwinProperties(), simulatedDevice.GetAbpReportedTwinProperties()); + } +} diff --git a/Tests/Common/LoRaWan.Tests.Common.csproj b/Tests/Common/LoRaWan.Tests.Common.csproj index b3835308b2..87e6bb91b3 100644 --- a/Tests/Common/LoRaWan.Tests.Common.csproj +++ b/Tests/Common/LoRaWan.Tests.Common.csproj @@ -1,20 +1,20 @@ - + $(TargetFramework) false + + - - diff --git a/Tests/Common/LoggerMockExtensions.cs b/Tests/Common/LoggerMockExtensions.cs new file mode 100644 index 0000000000..8a036fc414 --- /dev/null +++ b/Tests/Common/LoggerMockExtensions.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Common +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using Microsoft.Extensions.Logging; + using Moq; + + public sealed record LoggerLogInvocation(LogLevel LogLevel, EventId EventId, Exception? Exception, string Message); + + public static class LoggerMockExtensions + { + private static readonly MethodInfo? LogMethodInfo = typeof(ILogger).GetMethod(nameof(ILogger.Log)); + + public static IEnumerable GetLogInvocations(this Mock mock) + { + ArgumentNullException.ThrowIfNull(mock); + return mock.Invocations.GetLoggerLogInvocations(); + } + + public static IEnumerable GetLogInvocations(this Mock> mock) + { + ArgumentNullException.ThrowIfNull(mock); + return mock.Invocations.GetLoggerLogInvocations(); + } + + public static IEnumerable GetLoggerLogInvocations(this IInvocationList invocations) + { + ArgumentNullException.ThrowIfNull(invocations); + + return from i in invocations + where i.Method.IsGenericMethod && i.Method.GetGenericMethodDefinition() == LogMethodInfo + select i.Arguments into args + select new LoggerLogInvocation((LogLevel)args[0], + (EventId)args[1], + (Exception?)args[3], + (string)((Delegate)args[4]).DynamicInvoke(args[2], args[3])!); + } + } +} diff --git a/Tests/Common/MemoryCacheExtensions.cs b/Tests/Common/MemoryCacheExtensions.cs new file mode 100644 index 0000000000..ee363dc5aa --- /dev/null +++ b/Tests/Common/MemoryCacheExtensions.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Common +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.Caching.Memory; + + public static class MemoryCacheExtensions + { + /// + /// Waits for at most 30s for the eviction of an item from the memory cache. + /// + public static async Task WaitForEvictionAsync(this IMemoryCache memoryCache, object key, CancellationToken cancellationToken) + { + if (memoryCache is null) throw new ArgumentNullException(nameof(memoryCache)); + + var waitInterval = TimeSpan.FromSeconds(2); + var timeout = TimeSpan.FromSeconds(30); + using var timeoutTokenSource = new CancellationTokenSource(timeout); + using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, cancellationToken); + + while (true) + { + if (!memoryCache.TryGetValue(key, out var _)) + return; + + try + { + await Task.Delay(waitInterval, linkedTokenSource.Token); + } + catch (OperationCanceledException) + { + throw new OperationCanceledException($"Item with key '{key}' was not evicted after {timeout.TotalSeconds} seconds."); + } + } + } + } +} diff --git a/Tests/Common/MessageProcessorMultipleGatewayBase.cs b/Tests/Common/MessageProcessorMultipleGatewayBase.cs index 0d441f19be..1fb6dbd31b 100644 --- a/Tests/Common/MessageProcessorMultipleGatewayBase.cs +++ b/Tests/Common/MessageProcessorMultipleGatewayBase.cs @@ -5,6 +5,7 @@ namespace LoRaWan.Tests.Common { using System; using System.Globalization; + using System.Threading.Tasks; using LoRaTools.ADR; using LoRaWan.NetworkServer; using LoRaWan.NetworkServer.ADR; @@ -19,6 +20,7 @@ public class MessageProcessorMultipleGatewayBase : MessageProcessorTestBase private readonly MemoryCache cache; private readonly TestOutputLoggerFactory testOutputLoggerFactory; + private readonly MockHttpClientFactory httpClientFactory; public NetworkServerConfiguration SecondServerConfiguration { get; } @@ -59,10 +61,11 @@ public MessageProcessorMultipleGatewayBase(ITestOutputHelper testOutputHelper) : var functionBundlerProvider = new FunctionBundlerProvider(SecondLoRaDeviceApi.Object, this.testOutputLoggerFactory, this.testOutputLoggerFactory.CreateLogger()); SecondConcentratorDeduplication = new ConcentratorDeduplication(this.cache, this.testOutputLoggerFactory.CreateLogger()); + this.httpClientFactory = new MockHttpClientFactory(); SecondRequestHandlerImplementation = new DefaultLoRaDataRequestHandler(SecondServerConfiguration, SecondFrameCounterUpdateStrategyProvider, SecondConcentratorDeduplication, - new LoRaPayloadDecoder(this.testOutputLoggerFactory.CreateLogger()), + new LoRaPayloadDecoder(this.httpClientFactory, this.testOutputLoggerFactory.CreateLogger()), deduplicationStrategyFactory, adrStrategyProvider, loRaAdrManagerFactory, @@ -75,7 +78,7 @@ public MessageProcessorMultipleGatewayBase(ITestOutputHelper testOutputHelper) : var defaultRequestHandler = new DefaultLoRaDataRequestHandler(SecondServerConfiguration, SecondFrameCounterUpdateStrategyProvider, SecondConcentratorDeduplication, - new LoRaPayloadDecoder(this.testOutputLoggerFactory.CreateLogger()), + new LoRaPayloadDecoder(this.httpClientFactory, this.testOutputLoggerFactory.CreateLogger()), deduplicationStrategyFactory, adrStrategyProvider, loRaAdrManagerFactory, @@ -83,18 +86,19 @@ public MessageProcessorMultipleGatewayBase(ITestOutputHelper testOutputHelper) : this.testOutputLoggerFactory.CreateLogger(), meter: null); - SecondConnectionManager = new LoRaDeviceClientConnectionManager(this.cache, this.testOutputLoggerFactory.CreateLogger()); + SecondConnectionManager = new LoRaDeviceClientConnectionManager(this.cache, this.testOutputLoggerFactory, this.testOutputLoggerFactory.CreateLogger()); SecondLoRaDeviceFactory = new TestLoRaDeviceFactory(SecondServerConfiguration, SecondLoRaDeviceClient.Object, SecondConnectionManager, DeviceCache, defaultRequestHandler); } // Protected implementation of Dispose pattern. - protected override void Dispose(bool disposing) + protected override async ValueTask DisposeAsync(bool disposing) { - base.Dispose(disposing); + await base.DisposeAsync(disposing); if (disposing) { this.cache.Dispose(); this.testOutputLoggerFactory.Dispose(); + this.httpClientFactory.Dispose(); } } } diff --git a/Tests/Common/MessageProcessorTestBase.cs b/Tests/Common/MessageProcessorTestBase.cs index 085e9b230c..39fb39c7b4 100644 --- a/Tests/Common/MessageProcessorTestBase.cs +++ b/Tests/Common/MessageProcessorTestBase.cs @@ -4,6 +4,7 @@ namespace LoRaWan.Tests.Common { using System; + using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; @@ -19,14 +20,17 @@ namespace LoRaWan.Tests.Common using Moq; using Xunit.Abstractions; - public class MessageProcessorTestBase : IDisposable + public class MessageProcessorTestBase : IAsyncDisposable { public const string ServerGatewayID = "test-gateway"; +#pragma warning disable CA2213 // Disposable fields should be disposed (false positive) private readonly MemoryCache cache; private readonly TestOutputLoggerFactory testOutputLoggerFactory; +#pragma warning restore CA2213 // Disposable fields should be disposed private readonly byte[] macAddress; private readonly long startTime; + private readonly List valuesToDispose = new List(); private bool disposedValue; public TestDownstreamMessageSender DownstreamMessageSender { get; } @@ -68,7 +72,9 @@ public MessageProcessorTestBase(ITestOutputHelper testOutputHelper) }; this.testOutputLoggerFactory = new TestOutputLoggerFactory(testOutputHelper); - PayloadDecoder = new TestLoRaPayloadDecoder(new LoRaPayloadDecoder(this.testOutputLoggerFactory.CreateLogger())); + var httpClientFactory = new MockHttpClientFactory(); + this.valuesToDispose.Add(httpClientFactory); + PayloadDecoder = new TestLoRaPayloadDecoder(new LoRaPayloadDecoder(httpClientFactory, this.testOutputLoggerFactory.CreateLogger())); DownstreamMessageSender = new TestDownstreamMessageSender(); LoRaDeviceApi = new Mock(MockBehavior.Strict); FrameCounterUpdateStrategyProvider = new LoRaDeviceFrameCounterUpdateStrategyProvider(ServerConfiguration, LoRaDeviceApi.Object); @@ -81,7 +87,7 @@ public MessageProcessorTestBase(ITestOutputHelper testOutputHelper) LoRaDeviceClient = new Mock(); this.cache = new MemoryCache(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(5) }); - ConnectionManager = new LoRaDeviceClientConnectionManager(this.cache, this.testOutputLoggerFactory.CreateLogger()); + ConnectionManager = new LoRaDeviceClientConnectionManager(this.cache, this.testOutputLoggerFactory, this.testOutputLoggerFactory.CreateLogger()); ConcentratorDeduplication = new ConcentratorDeduplication(this.cache, this.testOutputLoggerFactory.CreateLogger()); RequestHandlerImplementation = new DefaultLoRaDataRequestHandler(ServerConfiguration, FrameCounterUpdateStrategyProvider, @@ -95,36 +101,44 @@ public MessageProcessorTestBase(ITestOutputHelper testOutputHelper) null); var requestHandler = CreateDefaultLoRaDataRequestHandler(ServerConfiguration, FrameCounterUpdateStrategyProvider, LoRaDeviceApi.Object, ConcentratorDeduplication); + this.valuesToDispose.Add(requestHandler); DeviceCache = new LoRaDeviceCache(new LoRaDeviceCacheOptions { MaxUnobservedLifetime = TimeSpan.FromMilliseconds(int.MaxValue), RefreshInterval = TimeSpan.FromMilliseconds(int.MaxValue), ValidationInterval = TimeSpan.FromMilliseconds(int.MaxValue) }, new NetworkServerConfiguration { GatewayID = ServerGatewayID }, this.testOutputLoggerFactory.CreateLogger(), TestMeter.Instance); - LoRaDeviceFactory = new TestLoRaDeviceFactory(ServerConfiguration, LoRaDeviceClient.Object, ConnectionManager, DeviceCache, requestHandler); + LoRaDeviceFactory = new TestLoRaDeviceFactory(ServerConfiguration, LoRaDeviceClient.Object, ConnectionManager, DeviceCache, requestHandler.Value); // By default we pick EU868 region. DefaultRegion = Enum.TryParse(Environment.GetEnvironmentVariable("REGION"), out var loraRegionType) ? (RegionManager.TryTranslateToRegion(loraRegionType, out var resolvedRegion) ? resolvedRegion : RegionManager.EU868) : RegionManager.EU868; } - protected DefaultLoRaDataRequestHandler CreateDefaultLoRaDataRequestHandler(NetworkServerConfiguration networkServerConfiguration, - ILoRaDeviceFrameCounterUpdateStrategyProvider frameCounterUpdateStrategyProvider, - LoRaDeviceAPIServiceBase loraDeviceApi, - IConcentratorDeduplication concentratorDeduplication) + protected DisposableValue + CreateDefaultLoRaDataRequestHandler(NetworkServerConfiguration networkServerConfiguration, + ILoRaDeviceFrameCounterUpdateStrategyProvider frameCounterUpdateStrategyProvider, + LoRaDeviceAPIServiceBase loraDeviceApi, + IConcentratorDeduplication concentratorDeduplication) { var deduplicationFactory = new DeduplicationStrategyFactory(this.testOutputLoggerFactory, this.testOutputLoggerFactory.CreateLogger()); var adrStrategyProvider = new LoRaADRStrategyProvider(this.testOutputLoggerFactory); var adrManagerFactory = new LoRAADRManagerFactory(loraDeviceApi, this.testOutputLoggerFactory); var functionBundlerProvider = new FunctionBundlerProvider(loraDeviceApi, this.testOutputLoggerFactory, this.testOutputLoggerFactory.CreateLogger()); - return new DefaultLoRaDataRequestHandler(networkServerConfiguration, - frameCounterUpdateStrategyProvider, - concentratorDeduplication, - new LoRaPayloadDecoder(this.testOutputLoggerFactory.CreateLogger()), - deduplicationFactory, - adrStrategyProvider, - adrManagerFactory, - functionBundlerProvider, - this.testOutputLoggerFactory.CreateLogger(), - meter: TestMeter.Instance); +#pragma warning disable CA2000 // Dispose objects before losing scope (disposed as part of DisposableValue) + var httpClientFactory = new MockHttpClientFactory(); +#pragma warning restore CA2000 // Dispose objects before losing scope + var decoder = new LoRaPayloadDecoder(httpClientFactory, this.testOutputLoggerFactory.CreateLogger()); + return new DisposableValue( + new DefaultLoRaDataRequestHandler(networkServerConfiguration, + frameCounterUpdateStrategyProvider, + concentratorDeduplication, + decoder, + deduplicationFactory, + adrStrategyProvider, + adrManagerFactory, + functionBundlerProvider, + this.testOutputLoggerFactory.CreateLogger(), + meter: TestMeter.Instance), + httpClientFactory); } public static MemoryCache NewMemoryCache() => new MemoryCache(new MemoryCacheOptions()); @@ -177,7 +191,7 @@ public LoRaDevice CreateLoRaDevice(SimulatedDevice simulatedDevice, bool registe useRealTimer, effectiveRegion); } - + protected WaitableLoRaRequest CreateWaitableRequest(RadioMetadata metadata, LoRaPayload loRaPayload, @@ -200,25 +214,29 @@ public LoRaDevice CreateLoRaDevice(SimulatedDevice simulatedDevice, bool registe return request; } - protected virtual void Dispose(bool disposing) + protected virtual async ValueTask DisposeAsync(bool disposing) { if (!this.disposedValue) { if (disposing) { this.cache.Dispose(); - this.DeviceCache.Dispose(); + await this.DeviceCache.DisposeAsync(); this.testOutputLoggerFactory.Dispose(); + foreach (var d in this.valuesToDispose) + { + d.Dispose(); + } } this.disposedValue = true; } } - public void Dispose() + public async ValueTask DisposeAsync() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); + await DisposeAsync(disposing: true); GC.SuppressFinalize(this); } } diff --git a/Tests/Common/MockHttpClientFactory.cs b/Tests/Common/MockHttpClientFactory.cs new file mode 100644 index 0000000000..5e0939c276 --- /dev/null +++ b/Tests/Common/MockHttpClientFactory.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Common +{ + using System; + using System.Net.Http; + + public sealed class MockHttpClientFactory : IHttpClientFactory, IDisposable + { + public HttpClient HttpClient { get; } + + public MockHttpClientFactory() : this(new HttpClient()) { } + + public MockHttpClientFactory(HttpClient httpClient) => HttpClient = httpClient; + + /// + /// Creates an IHttpClientFactory which always returns an HttpClient containing only the httpMessageHandler parameter. + /// + /// + public MockHttpClientFactory(HttpMessageHandler httpMessageHandler) + : this(new HttpClient(httpMessageHandler)) { } + + public HttpClient CreateClient(string name) => HttpClient; + + public void Dispose() => HttpClient.Dispose(); + } +} diff --git a/Tests/Common/SimulatedBasicsStation.cs b/Tests/Common/SimulatedBasicsStation.cs index 9c360f821b..3594bb90d0 100644 --- a/Tests/Common/SimulatedBasicsStation.cs +++ b/Tests/Common/SimulatedBasicsStation.cs @@ -15,6 +15,7 @@ namespace LoRaWan.Tests.Common using LoRaWan.NetworkServer; using LoRaTools.LoRaMessage; using System.Text.Json.Serialization; + using LoRaTools; public sealed class SimulatedBasicsStation : IDisposable { diff --git a/Tests/Common/SimulatedDevice.cs b/Tests/Common/SimulatedDevice.cs index 4ffb4e76a8..7ed1327341 100644 --- a/Tests/Common/SimulatedDevice.cs +++ b/Tests/Common/SimulatedDevice.cs @@ -27,13 +27,14 @@ public sealed class SimulatedDevice private static readonly IJsonReader DevEuiMessageReader = JsonReader.Object(JsonReader.Property("DevEui", from d in JsonReader.String() select DevEui.Parse(d))); - private readonly List simulatedBasicsStations = new List(); private readonly ConcurrentBag receivedMessages = new ConcurrentBag(); private readonly ILogger logger; public IReadOnlyCollection ReceivedMessages => this.receivedMessages; + public IReadOnlyCollection SimulatedBasicsStations { get; set; } + public TestDeviceInfo LoRaDevice { get; internal set; } public uint FrmCntUp { get; set; } @@ -56,7 +57,7 @@ public sealed class SimulatedDevice public JoinEui? AppEui => LoRaDevice.AppEui; - public char ClassType => LoRaDevice.ClassType; + public LoRaDeviceClassType ClassType => LoRaDevice.ClassType; public DevAddr? DevAddr { @@ -78,7 +79,7 @@ public SimulatedDevice(TestDeviceInfo testDeviceInfo, uint frmCntDown = 0, uint FrmCntDown = frmCntDown; FrmCntUp = frmCntUp; this.logger = logger; - this.simulatedBasicsStations = simulatedBasicsStation?.ToList() ?? new List(); + SimulatedBasicsStations = simulatedBasicsStation?.ToList() ?? new List(); void AddToDeviceMessageQueue(string response) { @@ -88,7 +89,7 @@ void AddToDeviceMessageQueue(string response) } } - foreach (var basicsStation in this.simulatedBasicsStations) + foreach (var basicsStation in SimulatedBasicsStations) basicsStation.MessageReceived += (_, eventArgs) => AddToDeviceMessageQueue(eventArgs.Value); } @@ -257,7 +258,7 @@ private bool HandleJoinAccept(LoRaPayloadJoinAccept payload) //// Sends unconfirmed message public Task SendDataMessageAsync(LoRaRequest loRaRequest) => - Task.WhenAll(from basicsStation in this.simulatedBasicsStations + Task.WhenAll(from basicsStation in SimulatedBasicsStations select basicsStation.SendDataMessageAsync(loRaRequest, CancellationToken.None)); // Performs join @@ -305,12 +306,12 @@ void OnMessageReceived(object sender, EventArgs response) } } - foreach (var basicsStation in this.simulatedBasicsStations) + foreach (var basicsStation in SimulatedBasicsStations) basicsStation.MessageReceived += OnMessageReceived; try { - foreach (var basicsStation in this.simulatedBasicsStations) + foreach (var basicsStation in SimulatedBasicsStations) { await basicsStation.SerializeAndSendMessageAsync(new { @@ -342,7 +343,7 @@ void OnMessageReceived(object sender, EventArgs response) } finally { - foreach (var basicsStation in this.simulatedBasicsStations) + foreach (var basicsStation in SimulatedBasicsStations) basicsStation.MessageReceived -= OnMessageReceived; } } diff --git a/Tests/Common/SingleDeviceConnectionManager.cs b/Tests/Common/SingleDeviceConnectionManager.cs index 3d01f21390..1c91f27bcf 100644 --- a/Tests/Common/SingleDeviceConnectionManager.cs +++ b/Tests/Common/SingleDeviceConnectionManager.cs @@ -3,6 +3,8 @@ namespace LoRaWan.Tests.Common { + using System; + using System.Threading.Tasks; using LoRaWan.NetworkServer; /// @@ -17,33 +19,19 @@ public SingleDeviceConnectionManager(ILoRaDeviceClient deviceClient) this.singleDeviceClient = deviceClient; } - public bool EnsureConnected(LoRaDevice loRaDevice) => true; - public ILoRaDeviceClient GetClient(LoRaDevice loRaDevice) => this.singleDeviceClient; public void Register(LoRaDevice loRaDevice, ILoRaDeviceClient loraDeviceClient) { } - public void Release(LoRaDevice loRaDevice) + public IAsyncDisposable BeginDeviceClientConnectionActivity(LoRaDevice loRaDevice) { - this.singleDeviceClient.Dispose(); + throw new NotImplementedException(); } - public void TryScanExpiredItems() - { - } + public Task ReleaseAsync(LoRaDevice loRaDevice) => this.singleDeviceClient.DisposeAsync().AsTask(); - public void Dispose() => this.singleDeviceClient.Dispose(); - - public ILoRaDeviceClient GetClient(DevEui devEui) - { - return this.singleDeviceClient; - } - - public void Release(DevEui devEui) - { - this.singleDeviceClient.Dispose(); - } + public ValueTask DisposeAsync() => this.singleDeviceClient.DisposeAsync(); } } diff --git a/Tests/Common/TestConfiguration.cs b/Tests/Common/TestConfiguration.cs index c77ba25ffe..60080b1085 100644 --- a/Tests/Common/TestConfiguration.cs +++ b/Tests/Common/TestConfiguration.cs @@ -87,6 +87,8 @@ public static TestConfiguration GetConfiguration() // The path of where the station binary is located on local pc public string BasicStationExecutablePath { get; set; } //i.e. "C:\\folder\\station" + public bool IsCorecellBasicStation { get; set; } //when set to true, tests will pick the corecell station.conf + // The path where the SSH Private Key is located (needed for remotely copying needed files and/or executing commands) public string SshPrivateKeyPath { get; set; } //i.e. "~/.ssh/mykey" (on WSL, it's the path in the WSL environment) @@ -104,19 +106,19 @@ public static TestConfiguration GetConfiguration() public string CupsFwDigest { get; set; } //a base 64 encoded digest of the upgrade file - public string CupsBasicStationVersion { get; set; } //i.e. 2.0.5(corecell/std) + public string CupsBasicStationVersion { get; set; } //i.e. 2.0.6(corecell/std) public string CupsBasicStationPackage { get; set; } //i.e. 1.0.0-e2e public Uri CupsFwUrl { get; set; } //url of the blob containing the desired fw update for cups test - public string LoadTestLnsEndpointsString + public string LoadTestLnsEndpoints { get => JsonSerializer.Serialize(LnsEndpointsForSimulator); - set => LnsEndpointsForSimulator = JsonSerializer.Deserialize>(value); + set => LnsEndpointsForSimulator = JsonSerializer.Deserialize>(value); } - public IReadOnlyList LnsEndpointsForSimulator { get; set; } + public IReadOnlyDictionary LnsEndpointsForSimulator { get; set; } public int NumberOfLoadTestDevices { get; set; } = 10; public int NumberOfLoadTestConcentrators { get; set; } = 2; diff --git a/Tests/Common/TestDeviceInfo.cs b/Tests/Common/TestDeviceInfo.cs index eba42afac7..dfc5c653bd 100644 --- a/Tests/Common/TestDeviceInfo.cs +++ b/Tests/Common/TestDeviceInfo.cs @@ -52,7 +52,7 @@ public class TestDeviceInfo public ReceiveWindowNumber PreferredWindow { get; set; } = ReceiveWindow1; - public char ClassType { get; set; } = 'A'; + public LoRaDeviceClassType ClassType { get; set; } = LoRaDeviceClassType.A; public int RX2DataRate { get; set; } @@ -60,7 +60,7 @@ public class TestDeviceInfo public bool Supports32BitFCnt { get; set; } - public DeduplicationMode Deduplication { get; set; } + public DeduplicationMode Deduplication { get; set; } = DeduplicationMode.Drop; // default to drop public ushort RXDelay { get; set; } @@ -102,7 +102,7 @@ public class TestDeviceInfo desiredProperties[TwinProperty.PreferredWindow] = (int)PreferredWindow; - if (char.ToLower(ClassType, CultureInfo.InvariantCulture) != 'a') + if (ClassType != LoRaDeviceClassType.A) desiredProperties[TwinProperty.ClassType] = ClassType.ToString(); desiredProperties[TwinProperty.RX1DROffset] = RX1DROffset; @@ -114,7 +114,7 @@ public class TestDeviceInfo // if (KeepAliveTimeout > 0) desiredProperties[TwinProperty.KeepAliveTimeout] = KeepAliveTimeout; - if (Deduplication is not DeduplicationMode.None) + if (Deduplication is not DeduplicationMode.Drop) desiredProperties[TwinProperty.Deduplication] = Deduplication; return desiredProperties; @@ -123,7 +123,13 @@ public class TestDeviceInfo /// /// Creates a with ABP authentication. /// - public static TestDeviceInfo CreateABPDevice(uint deviceID, string prefix = null, string gatewayID = null, string sensorDecoder = "DecoderValueSensor", int netId = 1, char deviceClassType = 'A', bool supports32BitFcnt = false) + public static TestDeviceInfo CreateABPDevice(uint deviceID, + string prefix = null, + string gatewayID = null, + string sensorDecoder = "DecoderValueSensor", + int netId = 1, + LoRaDeviceClassType deviceClassType = LoRaDeviceClassType.A, + bool supports32BitFcnt = false) { var value8 = deviceID.ToString("00000000", CultureInfo.InvariantCulture); var value16 = deviceID.ToString("0000000000000000", CultureInfo.InvariantCulture); @@ -155,7 +161,11 @@ public static TestDeviceInfo CreateABPDevice(uint deviceID, string prefix = null /// Creates a with OTAA authentication. /// /// Device identifier. It will padded with 0's. - public static TestDeviceInfo CreateOTAADevice(uint deviceID, string prefix = null, string gatewayID = null, string sensorDecoder = "DecoderValueSensor", char deviceClassType = 'A') + public static TestDeviceInfo CreateOTAADevice(uint deviceID, + string prefix = null, + string gatewayID = null, + string sensorDecoder = "DecoderValueSensor", + LoRaDeviceClassType deviceClassType = LoRaDeviceClassType.A) { var value16 = deviceID.ToString("0000000000000000", CultureInfo.InvariantCulture); var value32 = deviceID.ToString("00000000000000000000000000000000", CultureInfo.InvariantCulture); diff --git a/Tests/Common/TestKeys.cs b/Tests/Common/TestKeys.cs index 89e629a62a..3465f1a89a 100644 --- a/Tests/Common/TestKeys.cs +++ b/Tests/Common/TestKeys.cs @@ -3,8 +3,14 @@ namespace LoRaWan.Tests.Common { + using System.Security.Cryptography; + public static class TestKeys { + private static ulong GenerateKey() => (ulong)RandomNumberGenerator.GetInt32(0, int.MaxValue); + + public static NetworkSessionKey CreateNetworkSessionKey() => CreateNetworkSessionKey(GenerateKey()); + public static NetworkSessionKey CreateNetworkSessionKey(ulong value) => CreateNetworkSessionKey(0, value); public static NetworkSessionKey CreateNetworkSessionKey(ulong hi, ulong low) @@ -15,6 +21,8 @@ public static NetworkSessionKey CreateNetworkSessionKey(ulong hi, ulong low) return NetworkSessionKey.Read(buffer); } + public static AppSessionKey CreateAppSessionKey() => CreateAppSessionKey(GenerateKey()); + public static AppSessionKey CreateAppSessionKey(ulong value) => CreateAppSessionKey(0, value); public static AppSessionKey CreateAppSessionKey(ulong hi, ulong low) @@ -25,6 +33,8 @@ public static AppSessionKey CreateAppSessionKey(ulong hi, ulong low) return AppSessionKey.Read(buffer); } + public static AppKey CreateAppKey() => CreateAppKey(GenerateKey()); + public static AppKey CreateAppKey(ulong value) => CreateAppKey(0, value); public static AppKey CreateAppKey(ulong hi, ulong low) diff --git a/Tests/Common/TestLoRaDeviceFactory.cs b/Tests/Common/TestLoRaDeviceFactory.cs index 44299bc2c7..0cd83a571e 100644 --- a/Tests/Common/TestLoRaDeviceFactory.cs +++ b/Tests/Common/TestLoRaDeviceFactory.cs @@ -40,7 +40,8 @@ public TestLoRaDeviceFactory(ILoRaDeviceClient loRaDeviceClient, ILoRaDataReques deviceCache, NullLoggerFactory.Instance, NullLogger.Instance, - meter: null) + meter: null, + new NoopTracing()) { this.loRaDeviceClient = loRaDeviceClient; } diff --git a/Tests/Common/TestMessageDispatcher.cs b/Tests/Common/TestMessageDispatcher.cs new file mode 100644 index 0000000000..dbace95dfa --- /dev/null +++ b/Tests/Common/TestMessageDispatcher.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Common +{ + using LoRaWan.NetworkServer; + using Microsoft.Extensions.Caching.Memory; + using Microsoft.Extensions.Logging.Abstractions; + using Moq; + + public static class TestMessageDispatcher + { + public static MessageDispatcher Create(IMemoryCache memoryCache, + NetworkServerConfiguration configuration, + ILoRaDeviceRegistry deviceRegistry, + ILoRaDeviceFrameCounterUpdateStrategyProvider frameCounterUpdateStrategyProvider) + { + var concentratorDeduplication = new ConcentratorDeduplication(memoryCache, NullLogger.Instance); + + return new MessageDispatcher(configuration, + deviceRegistry, + frameCounterUpdateStrategyProvider, + new JoinRequestMessageHandler(configuration, concentratorDeduplication, deviceRegistry, NullLogger.Instance, Mock.Of(), null), + NullLoggerFactory.Instance, + NullLogger.Instance, + null); + } + } +} diff --git a/Tests/Common/TestMetricListener.cs b/Tests/Common/TestMetricListener.cs new file mode 100644 index 0000000000..d826549d17 --- /dev/null +++ b/Tests/Common/TestMetricListener.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Common +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Diagnostics.Metrics; + using LoRaWan.NetworkServer; + using Microsoft.Extensions.Logging.Abstractions; + + public sealed class TestMetricListener : RegistryMetricExporter + { + private readonly ConcurrentBag<(Instrument Instrument, double Value, KeyValuePair[] Tags)> recordedMetrics = + new ConcurrentBag<(Instrument, double, KeyValuePair[])>(); + + public IReadOnlyCollection<(Instrument Instrument, double Value, KeyValuePair[] Tags)> RecordedMetrics => this.recordedMetrics; + + public TestMetricListener(string metricNamespace) + : base(metricNamespace, MetricRegistry.RegistryLookup, NullLogger.Instance) + { } + + protected override void TrackValue(Instrument instrument, double measurement, ReadOnlySpan> tags, object state) => + this.recordedMetrics.Add((instrument, measurement, tags.ToArray())); + } +} diff --git a/Tests/Common/TestOutputLogger.cs b/Tests/Common/TestOutputLogger.cs index 9ee973bcad..827f1f2ceb 100644 --- a/Tests/Common/TestOutputLogger.cs +++ b/Tests/Common/TestOutputLogger.cs @@ -7,7 +7,7 @@ namespace LoRaWan.Tests.Common { using System; using System.Collections.Concurrent; - using LoRaWan.NetworkServer; + using LoRaTools; using Microsoft.Extensions.Logging; using Xunit.Abstractions; @@ -25,7 +25,7 @@ public class TestOutputLogger : ILogger public TestOutputLogger(ITestOutputHelper testOutputHelper) => this.testOutputHelper = testOutputHelper; - public IDisposable BeginScope(TState state) => NullDisposable.Instance; + public IDisposable BeginScope(TState state) => NoopDisposable.Instance; public bool IsEnabled(LogLevel logLevel) => logLevel >= TestLogLevel; @@ -35,7 +35,16 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except if (!IsEnabled(logLevel)) return; var message = formatter(state, exception); - this.testOutputHelper.WriteLine(message); + + try + { + this.testOutputHelper.WriteLine(message); + } + catch (InvalidOperationException) + { + // best-effort logging in case testOutputHelper has already been disposed. Fixes: + // https://github.com/Azure/iotedge-lorawan-starterkit/issues/1554. + } } } diff --git a/Tests/Common/TestUtils.cs b/Tests/Common/TestUtils.cs index 93764d171d..3a7475132d 100644 --- a/Tests/Common/TestUtils.cs +++ b/Tests/Common/TestUtils.cs @@ -14,7 +14,6 @@ namespace LoRaWan.Tests.Common using LoRaWan.NetworkServer; using LoRaWan.NetworkServer.BasicsStation; using Microsoft.Azure.Devices.Client; - using Microsoft.Azure.Devices.Shared; using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json; using Xunit; @@ -37,7 +36,7 @@ public static class TestUtils NwkSKey = simulatedDevice.LoRaDevice.NwkSKey, GatewayID = simulatedDevice.LoRaDevice.GatewayID, IsOurDevice = true, - ClassType = (simulatedDevice.ClassType is 'C' or 'c') ? LoRaDeviceClassType.C : LoRaDeviceClassType.A, + ClassType = simulatedDevice.ClassType, }; result.SetFcntDown(simulatedDevice.FrmCntDown); @@ -50,112 +49,6 @@ public static class TestUtils return result; } - public static Twin CreateTwin(Dictionary desired = null, Dictionary reported = null) - { - var twin = new Twin(); - if (desired != null) - { - foreach (var kv in desired) - { - twin.Properties.Desired[kv.Key] = kv.Value; - } - } - - if (reported != null) - { - foreach (var kv in reported) - { - twin.Properties.Reported[kv.Key] = kv.Value; - } - } - - return twin; - } - - public static Twin CreateABPTwin( - this SimulatedDevice simulatedDevice, - Dictionary desiredProperties = null, - Dictionary reportedProperties = null) - { - var finalDesiredProperties = new Dictionary - { - { TwinProperty.DevAddr, simulatedDevice.DevAddr.ToString() }, - { TwinProperty.AppSKey, simulatedDevice.AppSKey?.ToString() }, - { TwinProperty.NwkSKey, simulatedDevice.NwkSKey?.ToString() }, - { TwinProperty.GatewayID, simulatedDevice.LoRaDevice.GatewayID }, - { TwinProperty.SensorDecoder, simulatedDevice.LoRaDevice.SensorDecoder }, - { TwinProperty.ClassType, simulatedDevice.ClassType.ToString() }, - }; - - if (desiredProperties != null) - { - foreach (var kv in desiredProperties) - { - finalDesiredProperties[kv.Key] = kv.Value; - } - } - - var finalReportedProperties = new Dictionary - { - { TwinProperty.FCntDown, simulatedDevice.FrmCntDown }, - { TwinProperty.FCntUp, simulatedDevice.FrmCntUp } - }; - - if (reportedProperties != null) - { - foreach (var kv in reportedProperties) - { - finalReportedProperties[kv.Key] = kv.Value; - } - } - - return CreateTwin(desired: finalDesiredProperties, reported: finalReportedProperties); - } - - public static Twin CreateOTAATwin( - this SimulatedDevice simulatedDevice, - Dictionary desiredProperties = null, - Dictionary reportedProperties = null) - { - var finalDesiredProperties = new Dictionary - { - { TwinProperty.AppEui, simulatedDevice.AppEui?.ToString() }, - { TwinProperty.AppKey, simulatedDevice.AppKey?.ToString() }, - { TwinProperty.GatewayID, simulatedDevice.LoRaDevice.GatewayID }, - { TwinProperty.SensorDecoder, simulatedDevice.LoRaDevice.SensorDecoder }, - { TwinProperty.ClassType, simulatedDevice.ClassType.ToString() }, - }; - - if (desiredProperties != null) - { - foreach (var kv in desiredProperties) - { - finalDesiredProperties[kv.Key] = kv.Value; - } - } - - var finalReportedProperties = new Dictionary - { - { TwinProperty.DevAddr, simulatedDevice.DevAddr?.ToString() }, - { TwinProperty.AppSKey, simulatedDevice.AppSKey?.ToString() }, - { TwinProperty.NwkSKey, simulatedDevice.NwkSKey?.ToString() }, - { TwinProperty.DevNonce, simulatedDevice.DevNonce.ToString() }, - { TwinProperty.NetId, simulatedDevice.NetId?.ToString() }, - { TwinProperty.FCntDown, simulatedDevice.FrmCntDown }, - { TwinProperty.FCntUp, simulatedDevice.FrmCntUp } - }; - - if (reportedProperties != null) - { - foreach (var kv in reportedProperties) - { - finalReportedProperties[kv.Key] = kv.Value; - } - } - - return CreateTwin(desired: finalDesiredProperties, reported: finalReportedProperties); - } - /// /// Helper to create a from a . /// @@ -247,7 +140,8 @@ public static void StartBasicsStation(TestConfiguration config, Dictionary From(IEnumerable data) result.Add(a, b, c, d, e); return result; } + + public static TheoryData From(IEnumerable<(T1, T2, T3, T4, T5, T6)> data) + { + if (data is null) throw new ArgumentNullException(nameof(data)); + + var result = new TheoryData(); + foreach (var (a, b, c, d, e, f) in data) + result.Add(a, b, c, d, e, f); + return result; + } } } diff --git a/Tests/Common/Utility.cs b/Tests/Common/Utility.cs index 95ee76a6cd..7581472537 100644 --- a/Tests/Common/Utility.cs +++ b/Tests/Common/Utility.cs @@ -5,7 +5,7 @@ namespace LoRaWan.Tests.Common using System.Collections.Generic; using System.Net.NetworkInformation; using System.Text; - using Microsoft.Azure.EventHubs; + using Azure.Messaging.EventHubs; /// /// Utility class. diff --git a/Tests/Common/WaitableLoRaRequest.cs b/Tests/Common/WaitableLoRaRequest.cs index 3dc24051a7..7c38b0d41c 100644 --- a/Tests/Common/WaitableLoRaRequest.cs +++ b/Tests/Common/WaitableLoRaRequest.cs @@ -77,8 +77,9 @@ private WaitableLoRaRequest(RadioMetadata radioMetadata, IDownstreamMessageSende return Create(radioMetadata, new[] { c2dMessageCheckTimeSpan, c2dMessageCheckTimeSpan, additionalMessageCheckTimeSpan, downlinkDeliveryTimeSpan }, downstreamMessageSender: downstreamMessageSender, loRaPayload: loRaPayloadData); } public static WaitableLoRaRequest CreateWaitableRequest(LoRaPayload loRaPayload, - IDownstreamMessageSender downstreamMessageSender = null) => - Create(TestUtils.GenerateTestRadioMetadata(), + RadioMetadata radioMetadata = null, + IDownstreamMessageSender downstreamMessageSender = null) => + Create(radioMetadata ?? TestUtils.GenerateTestRadioMetadata(), loRaPayload, downstreamMessageSender); diff --git a/Tests/E2E/ABPTest.cs b/Tests/E2E/ABPTest.cs index ae9ac93827..5a92987f75 100644 --- a/Tests/E2E/ABPTest.cs +++ b/Tests/E2E/ABPTest.cs @@ -106,7 +106,7 @@ private async Task Test_ABP_Confirmed_And_Unconfirmed_Message_With_ADR(TestDevic var sending = await TestFixtureCi.SearchNetworkServerModuleAsync((log) => log.StartsWith(searchTokenSending, StringComparison.OrdinalIgnoreCase), new SearchLogOptions(searchTokenSending)); Assert.NotNull(sending.MatchedEvent); - var searchTokenAlreadySent = $"{device.DeviceID}: another gateway has already sent ack or downlink msg"; + var searchTokenAlreadySent = $"{device.DeviceID}: duplication strategy indicated to not process message"; var ignored = await TestFixtureCi.SearchNetworkServerModuleAsync((log) => log.StartsWith(searchTokenAlreadySent, StringComparison.OrdinalIgnoreCase), new SearchLogOptions(searchTokenAlreadySent)); Assert.NotNull(ignored.MatchedEvent); @@ -162,7 +162,7 @@ private async Task Test_ABP_Confirmed_And_Unconfirmed_Message_With_ADR(TestDevic if (device.IsMultiGw) { - var searchTokenADRAlreadySent = $"{device.DeviceID}: another gateway has already sent ack or downlink msg"; + var searchTokenADRAlreadySent = $"{device.DeviceID}: duplication strategy indicated to not process message"; var ignored = await TestFixtureCi.SearchNetworkServerModuleAsync((log) => log.StartsWith(searchTokenADRAlreadySent, StringComparison.OrdinalIgnoreCase), new SearchLogOptions(searchTokenADRAlreadySent)); Assert.NotNull(ignored.MatchedEvent); @@ -360,7 +360,7 @@ public async Task Test_ABP_Device_With_Connection_Timeout() var expectedLog = $"{device25.DeviceID}: processing time"; await TestFixtureCi.SearchNetworkServerModuleAsync((log) => log.StartsWith(expectedLog, StringComparison.Ordinal), new SearchLogOptions(expectedLog)); - // wait 61 seconds + // wait 120 seconds await Task.Delay(TimeSpan.FromSeconds(120)); // Send 1 message from device 26 diff --git a/Tests/E2E/C2DMessageTest.cs b/Tests/E2E/C2DMessageTest.cs index 6bfe7caa76..621966e619 100644 --- a/Tests/E2E/C2DMessageTest.cs +++ b/Tests/E2E/C2DMessageTest.cs @@ -24,7 +24,7 @@ public sealed class C2DMessageTest : IntegrationTestBaseCi /// private const int CloudToDeviceMessageReceiveCountThreshold = 2; - private static readonly Random random = new Random(); + private static readonly Random Random = new Random(); public C2DMessageTest(IntegrationTestFixtureCi testFixture) : base(testFixture) @@ -95,7 +95,7 @@ public async Task Test_OTAA_Confirmed_Receives_C2D_Message_With_RX_Delay_2() } // sends C2D - between 10 and 99 - var c2dMessageBody = (100 + random.Next(90)).ToString(CultureInfo.InvariantCulture); + var c2dMessageBody = (100 + Random.Next(90)).ToString(CultureInfo.InvariantCulture); var c2dMessage = new LoRaCloudToDeviceMessage() { Payload = c2dMessageBody, @@ -111,7 +111,7 @@ public async Task Test_OTAA_Confirmed_Receives_C2D_Message_With_RX_Delay_2() var expectedRxSerial = $"+CMSG: PORT: 1; RX: \"{ToHexString(c2dMessageBody)}\""; Log($"Expected C2D received log is: {expectedRxSerial}"); - var c2dLogMessage = $"{device.DeviceID}: done completing cloud to device message, id: {c2dMessage.MessageId}"; + var c2dLogMessage = $"{device.DeviceID}: done processing 'Complete' on cloud to device message, id: '{c2dMessage.MessageId}'"; Log($"Expected C2D network server log is: {expectedRxSerial}"); // Sends 8x confirmed messages, stopping if C2D message is found @@ -216,7 +216,7 @@ public async Task Test_OTAA_Unconfirmed_Receives_C2D_Message() } // sends C2D - between 10 and 99 - var c2dMessageBody = (100 + random.Next(90)).ToString(CultureInfo.InvariantCulture); + var c2dMessageBody = (100 + Random.Next(90)).ToString(CultureInfo.InvariantCulture); var c2dMessage = new LoRaCloudToDeviceMessage() { Payload = c2dMessageBody, @@ -301,7 +301,6 @@ public Task Test_OTAA_Unconfirmed_Receives_Confirmed_FPort_2_Message_Single() return Test_OTAA_Unconfirmed_Receives_Confirmed_FPort_2_Message(device); } - /* Commented multi gateway tests as they make C2D tests flaky for now [RetryFact] public Task Test_OTAA_Unconfirmed_Receives_Confirmed_FPort_2_Message_MultiGw() { @@ -309,7 +308,6 @@ public Task Test_OTAA_Unconfirmed_Receives_Confirmed_FPort_2_Message_MultiGw() LogTestStart(device); return Test_OTAA_Unconfirmed_Receives_Confirmed_FPort_2_Message(device); } - */ // Ensures that C2D messages are received when working with unconfirmed messages // Uses Device15_OTAA @@ -354,7 +352,7 @@ private async Task Test_OTAA_Unconfirmed_Receives_Confirmed_FPort_2_Message(Test } // sends C2D - between 10 and 99 - var c2dMessageBody = (100 + random.Next(90)).ToString(CultureInfo.InvariantCulture); + var c2dMessageBody = (100 + Random.Next(90)).ToString(CultureInfo.InvariantCulture); var c2dMessage = new LoRaCloudToDeviceMessage() { Payload = c2dMessageBody, @@ -439,7 +437,6 @@ public Task Test_OTAA_Unconfirmed_Receives_Confirmed_C2D_Message_Single() return Test_OTAA_Unconfirmed_Receives_Confirmed_C2D_Message(device); } - /* Commented multi gateway tests as they make C2D tests flaky for now [RetryFact] public Task Test_OTAA_Unconfirmed_Receives_Confirmed_C2D_Message_MultiGw() { @@ -447,7 +444,7 @@ public Task Test_OTAA_Unconfirmed_Receives_Confirmed_C2D_Message_MultiGw() LogTestStart(device); return Test_OTAA_Unconfirmed_Receives_Confirmed_C2D_Message(device); } - */ + // Ensures that C2D messages are received when working with unconfirmed messages // Uses Device10_OTAA @@ -490,7 +487,7 @@ private async Task Test_OTAA_Unconfirmed_Receives_Confirmed_C2D_Message(TestDevi } // sends C2D - between 10 and 99 - var c2dMessageBody = (100 + random.Next(90)).ToString(CultureInfo.InvariantCulture); + var c2dMessageBody = (100 + Random.Next(90)).ToString(CultureInfo.InvariantCulture); var msgId = Guid.NewGuid().ToString(); var c2dMessage = new LoRaCloudToDeviceMessage() { @@ -603,7 +600,7 @@ public async Task C2D_When_Device_Has_Preferred_Windows_2_Should_Receive_In_2nd_ } // sends C2D - between 10 and 99 - var c2dMessageBody = (100 + random.Next(90)).ToString(CultureInfo.InvariantCulture); + var c2dMessageBody = (100 + Random.Next(90)).ToString(CultureInfo.InvariantCulture); var msgId = Guid.NewGuid().ToString(); var c2dMessage = new LoRaCloudToDeviceMessage() { diff --git a/Tests/E2E/IntegrationTestFixtureCI.cs b/Tests/E2E/IntegrationTestFixtureCI.cs index e508c4cd4f..4a6289ccec 100644 --- a/Tests/E2E/IntegrationTestFixtureCI.cs +++ b/Tests/E2E/IntegrationTestFixtureCI.cs @@ -510,7 +510,7 @@ public override void SetupTestDevices() NwkSKey = GetNetworkSessionKey(24), DevAddr = new DevAddr(0x00000024), IsIoTHubDevice = true, - ClassType = 'C', + ClassType = LoRaDeviceClassType.C, }; // Device25_ABP: Connection timeout @@ -550,8 +550,7 @@ public override void SetupTestDevices() AppSKey = GetAppSessionKey(28), NwkSKey = GetNetworkSessionKey(28), DevAddr = new DevAddr(0x00000027), - IsIoTHubDevice = true, - Deduplication = DeduplicationMode.Drop + IsIoTHubDevice = true }; Device29_ABP = new TestDeviceInfo() @@ -577,8 +576,7 @@ public override void SetupTestDevices() DeviceID = "0000000000000031", AppEui = JoinEui.Parse("0000000000000031"), AppKey = GetAppKey(31), - IsIoTHubDevice = true, - Deduplication = DeduplicationMode.Drop + IsIoTHubDevice = true }; Device32_ABP = new TestDeviceInfo() diff --git a/Tests/E2E/LnsDiscoveryTests.cs b/Tests/E2E/LnsDiscoveryTests.cs new file mode 100644 index 0000000000..a9248ec566 --- /dev/null +++ b/Tests/E2E/LnsDiscoveryTests.cs @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.E2E +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.IO; + using System.Linq; + using System.Net.WebSockets; + using System.Security.Cryptography; + using System.Text; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + using LoRaTools; + using LoRaWan.Tests.Common; + using Microsoft.AspNetCore.Mvc.Testing; + using Microsoft.AspNetCore.TestHost; + using Microsoft.Azure.Devices; + using Microsoft.Azure.Devices.Common.Exceptions; + using Microsoft.Azure.Devices.Shared; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Hosting; + using Xunit; + + public sealed class LnsDiscoveryFixture : IAsyncLifetime + { + private const string LnsModuleName = "LoRaWanNetworkSrvModule"; + private const string FirstNetworkName = "network1"; + private const string SecondNetworkName = "network2"; + + public sealed record Lns(string DeviceId, string HostAddress, string NetworkId); + public sealed record Station(StationEui StationEui, string NetworkId); + + private static readonly Lns[] LnsInfo = new[] + { + new Lns("discoverylns1", "wss://lns1:5001", FirstNetworkName), new Lns("discoverylns2", "wss://lns2:5001", FirstNetworkName), + new Lns("discoverylns3", "wss://lns3:5001", SecondNetworkName), new Lns("discoverylns4", "wss://lns4:5001", SecondNetworkName), + }; + + public static readonly ImmutableArray StationInfo = new[] + { + new Station(new StationEui(1213148791), FirstNetworkName), + new Station(new StationEui(1213148792), FirstNetworkName), + new Station(new StationEui(1213148793), SecondNetworkName) + }.ToImmutableArray(); + + public static readonly ImmutableArray NetworkIds = + LnsInfo.Select(l => l.NetworkId) + .Distinct() + .ToImmutableArray(); + + public static readonly ImmutableDictionary> LnsInfoByStation = + StationInfo.GroupJoin(LnsInfo, station => station.NetworkId, lns => lns.NetworkId, (s, ls) => (s.StationEui, LnsInfo: ls)) + .ToImmutableDictionary(x => x.StationEui, x => x.LnsInfo.ToImmutableArray()); + + private readonly RegistryManager registryManager; + + public LnsDiscoveryFixture() + { + this.registryManager = RegistryManager.CreateFromConnectionString(TestConfiguration.GetConfiguration().IoTHubConnectionString); + } + + public Task CleanupDevicesAsync() => + Task.WhenAll(from deviceId in LnsInfo.Select(l => l.DeviceId.ToString()) + .Concat(StationInfo.Select(s => s.StationEui.ToString())) + select this.registryManager.RemoveDeviceAsync(deviceId)); + + public Task DisposeAsync() => CleanupDevicesAsync(); + + public async Task InitializeAsync() + { + try + { + // Try to cleanup existing devices from previous runs + await CleanupDevicesAsync(); + } + catch (DeviceNotFoundException) + { + TestLogger.Log("No device to cleanup was found"); + } + + await WaitBetweenRegistryOperations(); + + foreach (var lns in LnsInfo) + { + await this.registryManager.AddDeviceAsync(new Device(lns.DeviceId) { Capabilities = new DeviceCapabilities { IotEdge = true } }); + + var moduleTwin = new Twin() + { + Properties = new TwinProperties { Desired = new TwinCollection(JsonSerializer.Serialize(new { hostAddress = lns.HostAddress })) }, + Tags = GetNetworkTags(lns.NetworkId) + }; + await this.registryManager.AddModuleAsync(new Module(lns.DeviceId, LnsModuleName)); + await this.registryManager.UpdateTwinAsync(lns.DeviceId, LnsModuleName, moduleTwin, "*", CancellationToken.None); + } + + foreach (var station in StationInfo) + { + var deviceId = station.StationEui.ToString(); + await this.registryManager.AddDeviceAsync(new Device(deviceId)); + var twin = new Twin(deviceId) { Tags = GetNetworkTags(station.NetworkId) }; + await this.registryManager.UpdateTwinAsync(deviceId, twin, "*", CancellationToken.None); + } + + await WaitBetweenRegistryOperations(); + + static async Task WaitBetweenRegistryOperations() { + var waitTime = TimeSpan.FromSeconds(60); + TestLogger.Log($"Waiting for {waitTime.TotalSeconds} seconds."); + await Task.Delay(waitTime); + }; + + static TwinCollection GetNetworkTags(string networkId) => new TwinCollection(JsonSerializer.Serialize(new { network = networkId })); + } + } + + internal sealed class LnsDiscoveryApplication : WebApplicationFactory + { + private const string IotHubConnectionStringConfigurationName = "ConnectionStrings:IotHub"; + + protected override IHost CreateHost(IHostBuilder builder) + { + var iotHubConnectionString = TestConfiguration.GetConfiguration().IoTHubConnectionString; + builder.ConfigureAppConfiguration(config => config.AddInMemoryCollection(new Dictionary + { + [IotHubConnectionStringConfigurationName] = iotHubConnectionString + })); + + return base.CreateHost(builder); + } + } + + [Trait("Category", "SkipWhenLiveUnitTesting")] + public sealed class LnsDiscoveryTests : IClassFixture, IDisposable + { + private static readonly IJsonReader<(Uri LnsUri, string Muxs, StationEui StationEui)> RouterInfoResponseReader = + JsonReader.Object(JsonReader.Property("uri", from u in JsonReader.String() + select new Uri(u)), + JsonReader.Property("muxs", JsonReader.String()), + JsonReader.Property("router", from r in JsonReader.String() + select StationEui.Parse(r)), + (uri, muxs, router) => (uri, muxs, router)); + + private readonly LnsDiscoveryApplication subject; + private readonly WebSocketClient webSocketClient; + private readonly StationEui firstStation = LnsDiscoveryFixture.StationInfo[0].StationEui; + private readonly StationEui secondStation = LnsDiscoveryFixture.StationInfo[1].StationEui; + private readonly StationEui thirdStation = LnsDiscoveryFixture.StationInfo[2].StationEui; + + public LnsDiscoveryTests(LnsDiscoveryFixture _) + { + this.subject = new LnsDiscoveryApplication(); + this.webSocketClient = this.subject.Server.CreateWebSocketClient(); + } + + [Fact] + public async Task Discovery_Requests_Should_Be_Distributed_Between_Lns() + { + LogTestStart(nameof(Discovery_Requests_Should_Be_Distributed_Between_Lns)); + + // arrange + var station = this.firstStation; + var lnsInfo = LnsDiscoveryFixture.LnsInfoByStation[station]; + + // act + assert + var responses = new List(); + for (var i = 0; i < lnsInfo.Length * 2; ++i) + responses.Add(await GetLnsAddressAndAssertAsync(station, CancellationToken.None)); + + // assert + AssertLnsResponsesForStation(station, lnsInfo.Concat(lnsInfo).ToList(), responses); + } + + [Fact] + public async Task Discovery_Requests_Should_Distinguish_Between_Stations() + { + LogTestStart(nameof(Discovery_Requests_Should_Distinguish_Between_Stations)); + + // arrange + var cancellationToken = CancellationToken.None; + Assert.Equal(LnsDiscoveryFixture.LnsInfoByStation[this.firstStation].AsEnumerable(), LnsDiscoveryFixture.LnsInfoByStation[this.secondStation].AsEnumerable()); + + // act + var firstResult = await GetLnsAddressAndAssertAsync(this.firstStation, cancellationToken); + var secondResult = await GetLnsAddressAndAssertAsync(this.secondStation, cancellationToken); + + // assert + Assert.Equal(firstResult.Host, secondResult.Host); + } + + [Fact] + public async Task Discovery_Requests_Should_Distinguish_Between_Networks() + { + LogTestStart(nameof(Discovery_Requests_Should_Distinguish_Between_Networks)); + + // arrange + var cancellationToken = CancellationToken.None; + Assert.NotEqual(LnsDiscoveryFixture.LnsInfoByStation[this.firstStation].AsEnumerable(), LnsDiscoveryFixture.LnsInfoByStation[this.thirdStation].AsEnumerable()); + var firstNetworkLnsCount = LnsDiscoveryFixture.LnsInfoByStation[this.firstStation].Length; + Assert.Equal(firstNetworkLnsCount, LnsDiscoveryFixture.LnsInfoByStation[this.thirdStation].Length); + + // act + var firstStationResponses = new List(); + var thirdStationResponses = new List(); + for (var i = 0; i < firstNetworkLnsCount; ++i) + { + firstStationResponses.Add(await GetLnsAddressAndAssertAsync(this.firstStation, cancellationToken)); + thirdStationResponses.Add(await GetLnsAddressAndAssertAsync(this.thirdStation, cancellationToken)); + } + + // assert + AssertLnsResponsesForStation(this.firstStation, LnsDiscoveryFixture.LnsInfoByStation[this.firstStation], firstStationResponses); + AssertLnsResponsesForStation(this.thirdStation, LnsDiscoveryFixture.LnsInfoByStation[this.thirdStation], thirdStationResponses); + } + + [Fact] + public async Task Discovery_Requests_Should_Indicate_Error_Reason_For_Unknown_Station() + { + LogTestStart(nameof(Discovery_Requests_Should_Indicate_Error_Reason_For_Unknown_Station)); + + var response = await SendSingleMessageAsync(new StationEui((ulong)RandomNumberGenerator.GetInt32(int.MaxValue)), CancellationToken.None); + Assert.Contains("could not find twin for station", response, StringComparison.OrdinalIgnoreCase); + } + + private static void AssertLnsResponsesForStation(StationEui station, IReadOnlyCollection expected, IReadOnlyCollection actual) => + Assert.Equal(expected.Select(l => new Uri(new Uri(l.HostAddress), $"router-data/{station}")).OrderBy(l => l.AbsoluteUri), + actual.OrderBy(l => l.AbsoluteUri)); + + private async Task GetLnsAddressAndAssertAsync(StationEui station, CancellationToken cancellationToken) + { + var json = await SendSingleMessageAsync(station, cancellationToken); + var (lnsUri, muxs, router) = RouterInfoResponseReader.Read(json); + Assert.NotEmpty(muxs); + Assert.Equal(router, station); + return lnsUri; + } + + private async Task SendSingleMessageAsync(StationEui stationEui, CancellationToken cancellationToken) + { + var webSocket = await this.webSocketClient.ConnectAsync(new Uri(this.subject.Server.BaseAddress, "router-info"), cancellationToken); + await webSocket.SendAsync(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new { router = stationEui.AsUInt64 })), WebSocketMessageType.Text, endOfMessage: true, cancellationToken); + var e = webSocket.ReadTextMessages(cancellationToken); + var result = !await e.MoveNextAsync() ? throw new InvalidOperationException("No response received.") : e.Current; + + try + { + await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Normal closure", cancellationToken); + } + catch (IOException) + { + // Connection already closed. + } + + return result; + } + + private static void LogTestStart(string testName) => TestLogger.Log($"Starting test '{testName}'."); + + public void Dispose() => this.subject.Dispose(); + } +} diff --git a/Tests/E2E/LoRaWan.Tests.E2E.csproj b/Tests/E2E/LoRaWan.Tests.E2E.csproj index 26fe63b448..5c4ad7a70c 100644 --- a/Tests/E2E/LoRaWan.Tests.E2E.csproj +++ b/Tests/E2E/LoRaWan.Tests.E2E.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) false @@ -11,14 +11,13 @@ - - + all @@ -27,6 +26,9 @@ + + PreserveNewest + PreserveNewest diff --git a/Tests/E2E/MultiGatewayTests.cs b/Tests/E2E/MultiGatewayTests.cs index 43bf3eb378..743dcf244b 100644 --- a/Tests/E2E/MultiGatewayTests.cs +++ b/Tests/E2E/MultiGatewayTests.cs @@ -22,78 +22,39 @@ public MultiGatewayTests(IntegrationTestFixtureCi testFixture) } [RetryFact] - public async Task Test_MultiGW_OTTA_Join_Single() + public Task Test_OTAA_Deduplication_Strategy_Drop() { - var device = TestFixtureCi.Device27_OTAA; - LogTestStart(device); - - await ArduinoDevice.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWOTAA); - await ArduinoDevice.setIdAsync(device.DevAddr, device.DeviceID, device.AppEui); - await ArduinoDevice.setKeyAsync(device.NwkSKey, device.AppSKey, device.AppKey); - - await ArduinoDevice.SetupLora(TestFixtureCi.Configuration); - - var joinSucceeded = await ArduinoDevice.setOTAAJoinAsyncWithRetry(LoRaArduinoSerial._otaa_join_cmd_t.JOIN, 20000, 5); - - Assert.True(joinSucceeded, "Join failed"); - await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_JOIN); - - // validate that one GW refused the join - const string joinRefusedMsg = "join refused"; - var joinRefused = await TestFixtureCi.AssertNetworkServerModuleLogExistsAsync((s) => s.IndexOf(joinRefusedMsg, StringComparison.Ordinal) != -1, new SearchLogOptions(joinRefusedMsg)); - Assert.True(joinRefused.Found); - - await TestFixtureCi.WaitForTwinSyncAfterJoinAsync(ArduinoDevice.SerialLogs, device.DevEui); - - // expecting both gw to start picking up messages - // and sending to IoT hub. - var bothReported = false; - for (var i = 0; i < 5; i++) - { - var msg = PayloadGenerator.Next().ToString(CultureInfo.InvariantCulture); - await ArduinoDevice.transferPacketAsync(msg, 10); - - await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_SENDING_PACKET); - - // After transferPacket: Expectation from serial - // +MSG: Done - await AssertUtils.ContainsWithRetriesAsync("+MSG: Done", ArduinoDevice.SerialLogs); - - var expectedPayload = $"{{\"value\":{msg}}}"; - await TestFixtureCi.AssertIoTHubDeviceMessageExistsAsync(device.DeviceID, expectedPayload, new SearchLogOptions(expectedPayload)); - - bothReported = await TestFixtureCi.ValidateMultiGatewaySources((log) => log.StartsWith($"{device.DeviceID}: sending message", StringComparison.OrdinalIgnoreCase)); - if (bothReported) - { - break; - } - } - - Assert.True(bothReported); + return Test_Deduplication_Strategies(TestFixtureCi.Device27_OTAA, true); } [RetryFact] - public Task Test_Deduplication_Strategies_Mark() + public Task Test_ABP_Deduplication_Strategy_Mark() { - return Test_Deduplication_Strategies("Device29_ABP", "Mark"); + return Test_Deduplication_Strategies(TestFixtureCi.Device29_ABP, false); } [RetryFact] - public Task Test_Deduplication_Strategies_Drop() + public Task Test_ABP_Deduplication_Strategy_Drop() { - return Test_Deduplication_Strategies("Device28_ABP", "Drop"); + return Test_Deduplication_Strategies(TestFixtureCi.Device28_ABP, false); } - private async Task Test_Deduplication_Strategies(string devicePropertyName, string strategy) + private async Task Test_Deduplication_Strategies(TestDeviceInfo device, bool isOtaa) { - var device = TestFixtureCi.GetDeviceByPropertyName(devicePropertyName); LogTestStart(device); - await ArduinoDevice.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWABP); - await ArduinoDevice.setIdAsync(device.DevAddr, device.DeviceID, null); - await ArduinoDevice.setKeyAsync(device.NwkSKey, device.AppSKey, null); + if (isOtaa) + { + await OtaaSetupAndJoinAssertAsync(device); + } + else + { + await ArduinoDevice.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWABP); + await ArduinoDevice.setIdAsync(device.DevAddr, device.DeviceID, null); + await ArduinoDevice.setKeyAsync(device.NwkSKey, device.AppSKey, null); - await ArduinoDevice.SetupLora(TestFixtureCi.Configuration); + await ArduinoDevice.SetupLora(TestFixtureCi.Configuration); + } for (var i = 0; i < 10; i++) { @@ -105,7 +66,7 @@ private async Task Test_Deduplication_Strategies(string devicePropertyName, stri // +CMSG: ACK Received await AssertUtils.ContainsWithRetriesAsync("+CMSG: ACK Received", ArduinoDevice.SerialLogs); - var allGwGotIt = await TestFixtureCi.ValidateMultiGatewaySources((log) => log.IndexOf($"deduplication Strategy: {strategy}", StringComparison.OrdinalIgnoreCase) != -1); + var allGwGotIt = await TestFixtureCi.ValidateMultiGatewaySources((log) => log.IndexOf($"deduplication Strategy: {device.Deduplication}", StringComparison.OrdinalIgnoreCase) != -1); if (allGwGotIt) { var notDuplicate = "\"IsDuplicate\":false"; @@ -119,14 +80,14 @@ private async Task Test_Deduplication_Strategies(string devicePropertyName, stri Assert.NotEqual(duplicateResult.MatchedEvent.SourceId, notDuplicateResult.MatchedEvent.SourceId); - switch (strategy) + switch (device.Deduplication) { - case "Mark": + case NetworkServer.DeduplicationMode.Mark: var expectedProperty = "dupmsg"; await TestFixture.AssertIoTHubDeviceMessageExistsAsync(device.DeviceID, expectedProperty, "true", new SearchLogOptions(expectedProperty) { SourceIdFilter = duplicateResult.MatchedEvent.SourceId, TreatAsError = true }); await TestFixture.AssertIoTHubDeviceMessageExistsAsync(device.DeviceID, expectedProperty, "true", new SearchLogOptions(expectedProperty) { SourceIdFilter = notDuplicateResult.MatchedEvent.SourceId, TreatAsError = true }); break; - case "Drop": + case NetworkServer.DeduplicationMode.Drop: var logMsg = $"{device.DeviceID}: duplication strategy indicated to not process message"; var droppedLog = await TestFixtureCi.SearchNetworkServerModuleAsync((log) => log.StartsWith(logMsg, StringComparison.Ordinal), new SearchLogOptions(logMsg) { SourceIdFilter = duplicateResult.MatchedEvent.SourceId }); Assert.NotNull(droppedLog.MatchedEvent); @@ -134,6 +95,7 @@ private async Task Test_Deduplication_Strategies(string devicePropertyName, stri var expectedPayload = $"{{\"value\":{msg}}}"; await TestFixtureCi.AssertIoTHubDeviceMessageExistsAsync(device.DeviceID, expectedPayload, new SearchLogOptions(expectedPayload) { TreatAsError = true }); break; + case NetworkServer.DeduplicationMode.None: default: throw new SwitchExpressionException(); } @@ -142,5 +104,26 @@ private async Task Test_Deduplication_Strategies(string devicePropertyName, stri TestFixtureCi.ClearLogs(); } } + + private async Task OtaaSetupAndJoinAssertAsync(TestDeviceInfo device) + { + await ArduinoDevice.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWOTAA); + await ArduinoDevice.setIdAsync(device.DevAddr, device.DeviceID, device.AppEui); + await ArduinoDevice.setKeyAsync(device.NwkSKey, device.AppSKey, device.AppKey); + + await ArduinoDevice.SetupLora(TestFixtureCi.Configuration); + + var joinSucceeded = await ArduinoDevice.setOTAAJoinAsyncWithRetry(LoRaArduinoSerial._otaa_join_cmd_t.JOIN, 20000, 5); + + Assert.True(joinSucceeded, "Join failed"); + await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_JOIN); + + // validate that one GW refused the join + const string joinRefusedMsg = "join refused"; + var joinRefused = await TestFixtureCi.AssertNetworkServerModuleLogExistsAsync((s) => s.IndexOf(joinRefusedMsg, StringComparison.Ordinal) != -1, new SearchLogOptions(joinRefusedMsg)); + Assert.True(joinRefused.Found); + + await TestFixtureCi.WaitForTwinSyncAfterJoinAsync(ArduinoDevice.SerialLogs, device.DevEui); + } } } diff --git a/Tests/E2E/OTAATest.cs b/Tests/E2E/OTAATest.cs index 1eba804ae0..f5161b5fea 100644 --- a/Tests/E2E/OTAATest.cs +++ b/Tests/E2E/OTAATest.cs @@ -113,7 +113,7 @@ private async Task Test_OTAA_Confirmed_And_Unconfirmed_Message_With_Custom_RX1_D var sending = await TestFixtureCi.SearchNetworkServerModuleAsync((log) => log.StartsWith(searchTokenSending, StringComparison.OrdinalIgnoreCase), new SearchLogOptions(searchTokenSending)); Assert.NotNull(sending.MatchedEvent); - var searchTokenAlreadySent = $"{device.DeviceID}: another gateway has already sent ack or downlink msg"; + var searchTokenAlreadySent = $"{device.DeviceID}: duplication strategy indicated to not process message"; var ignored = await TestFixtureCi.SearchNetworkServerModuleAsync((log) => log.StartsWith(searchTokenAlreadySent, StringComparison.OrdinalIgnoreCase), new SearchLogOptions(searchTokenAlreadySent)); Assert.NotNull(ignored.MatchedEvent); diff --git a/Tests/E2E/appsettings.json b/Tests/E2E/appsettings.json index bf6072cb27..fe8b9ee1b2 100644 --- a/Tests/E2E/appsettings.json +++ b/Tests/E2E/appsettings.json @@ -32,6 +32,7 @@ "CupsSigKeyChecksum": "#{E2ETESTS_CupsSigKeyChecksum}#", "CupsBasicStationVersion": "#{E2ETESTS_CupsBasicStationVersion}#", "CupsBasicStationPackage": "#{E2ETESTS_CupsBasicStationPackage}#", - "CupsFwUrl": "#{E2ETESTS_CupsFwUrl}#" + "CupsFwUrl": "#{E2ETESTS_CupsFwUrl}#", + "IsCorecellBasicStation": "#{E2ETESTS_IsCorecellBasicStation}#" } } diff --git a/Tests/Integration/ClassCCloudToDeviceMessageSizeLimitTests.cs b/Tests/Integration/ClassCCloudToDeviceMessageSizeLimitTests.cs index fcd18da203..176ec051c5 100644 --- a/Tests/Integration/ClassCCloudToDeviceMessageSizeLimitTests.cs +++ b/Tests/Integration/ClassCCloudToDeviceMessageSizeLimitTests.cs @@ -24,7 +24,7 @@ namespace LoRaWan.Tests.Integration // End to end tests without external dependencies (IoT Hub, Service Facade Function) // Class CCloud to device message processing max payload size tests (Join tests are handled in other class) [Collection(TestConstants.C2D_Size_Limit_TestCollectionName)] - public sealed class ClassCCloudToDeviceMessageSizeLimitTests : IDisposable + public sealed class ClassCCloudToDeviceMessageSizeLimitTests : IAsyncDisposable { private const string ServerGatewayID = "test-gateway"; @@ -56,7 +56,7 @@ public ClassCCloudToDeviceMessageSizeLimitTests(ITestOutputHelper testOutputHelp this.testOutputLoggerFactory = new TestOutputLoggerFactory(testOutputHelper); this.cache = new MemoryCache(new MemoryCacheOptions()); - this.connectionManager = new LoRaDeviceClientConnectionManager(this.cache, testOutputLoggerFactory.CreateLogger()); + this.connectionManager = new LoRaDeviceClientConnectionManager(this.cache, this.testOutputLoggerFactory, this.testOutputLoggerFactory.CreateLogger()); this.loRaDeviceFactory = new TestLoRaDeviceFactory(this.deviceClient.Object, this.deviceCache, this.connectionManager); this.loRaDeviceRegistry = new LoRaDeviceRegistry(this.serverConfiguration, this.cache, this.deviceApi.Object, this.loRaDeviceFactory, this.deviceCache); @@ -83,18 +83,19 @@ private static void EnsureDownlinkIsCorrect(DownlinkMessage downlink, SimulatedD { var simulatedDevice = new SimulatedDevice( TestDeviceInfo.CreateABPDevice( - 1, deviceClassType: 'c', gatewayID: this.serverConfiguration.GatewayID)); + 1, deviceClassType: LoRaDeviceClassType.C, gatewayID: this.serverConfiguration.GatewayID)); var devEUI = simulatedDevice.DevEUI; this.deviceApi.Setup(x => x.GetPrimaryKeyByEuiAsync(devEUI)) .ReturnsAsync("123"); - var twin = simulatedDevice.CreateABPTwin(reportedProperties: new Dictionary - { - { TwinProperty.Region, LoRaRegionType.EU868.ToString() }, - { TwinProperty.LastProcessingStationEui, new StationEui(ulong.MaxValue).ToString() } - }); + var twin = LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetAbpDesiredTwinProperties(), + simulatedDevice.GetAbpReportedTwinProperties() with + { + Region = LoRaRegionType.EU868, + LastProcessingStation = new StationEui(ulong.MaxValue) + }); this.deviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); @@ -103,7 +104,7 @@ private static void EnsureDownlinkIsCorrect(DownlinkMessage downlink, SimulatedD var c2dPayloadSize = this.loRaRegion.GetMaxPayloadSize(this.loRaRegion.GetDefaultRX2ReceiveWindow(default).DataRate) - c2dMessageMacCommandSize - - Constants.LoraProtocolOverheadSize; + - NetworkServer.Constants.LoraProtocolOverheadSize; var c2dMsgPayload = GeneratePayload("123457890", (int)c2dPayloadSize); var c2d = new ReceivedLoRaCloudToDeviceMessage() @@ -152,8 +153,7 @@ private static void EnsureDownlinkIsCorrect(DownlinkMessage downlink, SimulatedD if (expectedMacCommandsCount > 0) { - var macCommands = MacCommand.CreateServerMacCommandFromBytes( - simulatedDevice.DevEUI, payloadDataDown.Fopts); + var macCommands = MacCommand.CreateServerMacCommandFromBytes(payloadDataDown.Fopts); Assert.Equal(expectedMacCommandsCount, macCommands.Count); } else @@ -172,7 +172,7 @@ private static void EnsureDownlinkIsCorrect(DownlinkMessage downlink, SimulatedD { var simulatedDevice = new SimulatedDevice( TestDeviceInfo.CreateABPDevice( - 1, deviceClassType: 'c', gatewayID: this.serverConfiguration.GatewayID)); + 1, deviceClassType: LoRaDeviceClassType.C, gatewayID: this.serverConfiguration.GatewayID)); var devEUI = simulatedDevice.DevEUI; @@ -180,7 +180,7 @@ private static void EnsureDownlinkIsCorrect(DownlinkMessage downlink, SimulatedD .ReturnsAsync("123"); this.deviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(simulatedDevice.CreateABPTwin()); + .ReturnsAsync(simulatedDevice.GetDefaultAbpTwin()); var c2dMessageMacCommand = new DevStatusRequest(); var c2dMessageMacCommandSize = hasMacInC2D ? c2dMessageMacCommand.Length : 0; @@ -188,7 +188,7 @@ private static void EnsureDownlinkIsCorrect(DownlinkMessage downlink, SimulatedD var c2dPayloadSize = this.loRaRegion.GetMaxPayloadSize(this.loRaRegion.GetDefaultRX2ReceiveWindow(default).DataRate) - c2dMessageMacCommandSize + 1 // make message too long on purpose - - Constants.LoraProtocolOverheadSize; + - NetworkServer.Constants.LoraProtocolOverheadSize; var c2dMsgPayload = GeneratePayload("123457890", (int)c2dPayloadSize); var c2d = new ReceivedLoRaCloudToDeviceMessage() @@ -239,13 +239,14 @@ private static string GeneratePayload(string allowedChars, int length) return new string(chars, 0, length); } - public void Dispose() + public async ValueTask DisposeAsync() { - this.loRaDeviceRegistry.Dispose(); - this.connectionManager.Dispose(); + await this.loRaDeviceRegistry.DisposeAsync(); + await this.connectionManager.DisposeAsync(); + await this.deviceCache.DisposeAsync(); + this.cache.Dispose(); this.testOutputLoggerFactory.Dispose(); - this.deviceCache.Dispose(); } } } diff --git a/Tests/Integration/ClassCIntegrationTests.cs b/Tests/Integration/ClassCIntegrationTests.cs index 878e43e38e..dab6029003 100644 --- a/Tests/Integration/ClassCIntegrationTests.cs +++ b/Tests/Integration/ClassCIntegrationTests.cs @@ -60,15 +60,13 @@ public async Task When_ABP_Sends_Upstream_Followed_By_DirectMethod_Should_Send_U { const uint payloadFcnt = 2; // to avoid relax mode reset - var simDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, gatewayID: deviceGatewayID, deviceClassType: 'c'), frmCntDown: fcntDownFromTwin); + var simDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, gatewayID: deviceGatewayID, deviceClassType: LoRaDeviceClassType.C), frmCntDown: fcntDownFromTwin); LoRaDeviceClient.Setup(x => x.UpdateReportedPropertiesAsync(It.IsNotNull(), It.IsAny())) .ReturnsAsync(true); - var twin = simDevice.CreateABPTwin(reportedProperties: new Dictionary - { - { TwinProperty.Region, region.LoRaRegion.ToString() } - }); + var twin = LoRaDeviceTwin.Create(simDevice.LoRaDevice.GetAbpDesiredTwinProperties(), + simDevice.GetAbpReportedTwinProperties() with { Region = region.LoRaRegion }); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); @@ -89,9 +87,10 @@ public async Task When_ABP_Sends_Upstream_Followed_By_DirectMethod_Should_Send_U } using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -130,7 +129,7 @@ public async Task When_ABP_Sends_Upstream_Followed_By_DirectMethod_Should_Send_U if (string.IsNullOrEmpty(deviceGatewayID)) { - LoRaDeviceApi.Setup(x => x.NextFCntDownAsync(simDevice.DevEUI, fcntDownFromTwin + fcntDelta, 0, ServerConfiguration.GatewayID)) + LoRaDeviceApi.Setup(x => x.NextFCntDownAsync(simDevice.DevEUI, fcntDownFromTwin + fcntDelta, simDevice.FrmCntUp, ServerConfiguration.GatewayID)) .ReturnsAsync((ushort)expectedFcntDown); } @@ -158,10 +157,11 @@ public async Task When_ABP_Sends_Upstream_Followed_By_DirectMethod_Should_Send_U [InlineData(ServerGatewayID)] public async Task When_OTAA_Join_Then_Sends_Upstream_DirectMethod_Should_Send_Downstream(string deviceGatewayID) { - var simDevice = new SimulatedDevice(TestDeviceInfo.CreateOTAADevice(1, deviceClassType: 'c', gatewayID: deviceGatewayID)); + var simDevice = new SimulatedDevice(TestDeviceInfo.CreateOTAADevice(1, deviceClassType: LoRaDeviceClassType.C, gatewayID: deviceGatewayID)); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(simDevice.CreateOTAATwin()); + .ReturnsAsync(LoRaDeviceTwin.Create(simDevice.LoRaDevice.GetOtaaDesiredTwinProperties(), + simDevice.GetOtaaReportedTwinProperties())); AppSessionKey? savedAppSKey = null; NetworkSessionKey? savedNwkSKey = null; @@ -191,9 +191,10 @@ public async Task When_OTAA_Join_Then_Sends_Upstream_DirectMethod_Should_Send_Do .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(simDevice.DevAddr, simDevice.DevEUI, "123").AsList())); using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -233,7 +234,7 @@ public async Task When_OTAA_Join_Then_Sends_Upstream_DirectMethod_Should_Send_Do if (string.IsNullOrEmpty(deviceGatewayID)) { - LoRaDeviceApi.Setup(x => x.NextFCntDownAsync(simDevice.DevEUI, simDevice.FrmCntDown, 0, ServerConfiguration.GatewayID)) + LoRaDeviceApi.Setup(x => x.NextFCntDownAsync(simDevice.DevEUI, simDevice.FrmCntDown, simDevice.FrmCntUp, ServerConfiguration.GatewayID)) .ReturnsAsync((ushort)(simDevice.FrmCntDown + 1)); } @@ -293,8 +294,8 @@ public async Task Unconfirmed_Cloud_To_Device_From_Decoder_Should_Call_ClassC_Me PayloadDecoder.SetDecoder(payloadDecoder.Object); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); using var c2dMessageSent = new SemaphoreSlim(0); var classCMessageSender = new Mock(MockBehavior.Strict); @@ -304,7 +305,8 @@ public async Task Unconfirmed_Cloud_To_Device_From_Decoder_Should_Call_ClassC_Me RequestHandlerImplementation.SetClassCMessageSender(classCMessageSender.Object); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -346,18 +348,16 @@ public async Task Unconfirmed_Cloud_To_Device_From_Decoder_Should_Call_ClassC_Me [CombinatorialValues(null, ServerGatewayID, "another-gateway")] string initialPreferredGatewayID, [CombinatorialValues(null, LoRaRegionType.EU868, LoRaRegionType.US915)] LoRaRegionType? initialLoRaRegion) { - var simDevice = new SimulatedDevice(TestDeviceInfo.CreateOTAADevice(1, deviceClassType: 'c', gatewayID: deviceGatewayID)); - - var customReportedProperties = new Dictionary(); - // reported: { 'PreferredGateway': '' } -> if device is for multiple gateways and one initial was defined - if (string.IsNullOrEmpty(deviceGatewayID) && !string.IsNullOrEmpty(initialPreferredGatewayID)) - customReportedProperties[TwinProperty.PreferredGatewayID] = initialPreferredGatewayID; - - if (initialLoRaRegion.HasValue) - customReportedProperties[TwinProperty.Region] = initialLoRaRegion.Value.ToString(); + var simDevice = new SimulatedDevice(TestDeviceInfo.CreateOTAADevice(1, deviceClassType: LoRaDeviceClassType.C, gatewayID: deviceGatewayID)); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(simDevice.CreateOTAATwin(reportedProperties: customReportedProperties)); + .ReturnsAsync(LoRaDeviceTwin.Create(simDevice.LoRaDevice.GetOtaaDesiredTwinProperties(), + new LoRaReportedTwinProperties + { + // reported: { 'PreferredGateway': '' } -> if device is for multiple gateways and one initial was defined + PreferredGatewayId = string.IsNullOrEmpty(deviceGatewayID) ? initialPreferredGatewayID : null, + Region = initialLoRaRegion + })); var shouldSavePreferredGateway = string.IsNullOrEmpty(deviceGatewayID) && initialPreferredGatewayID != ServerGatewayID; var shouldSaveRegion = !initialLoRaRegion.HasValue || initialLoRaRegion.Value != LoRaRegionType.EU868; @@ -380,9 +380,10 @@ public async Task Unconfirmed_Cloud_To_Device_From_Decoder_Should_Call_ClassC_Me .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(simDevice.DevAddr, simDevice.DevEUI, "123").AsList())); using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -434,7 +435,7 @@ public async Task Unconfirmed_Cloud_To_Device_From_Decoder_Should_Call_ClassC_Me const uint InitialDeviceFcntDown = 20; var simulatedDevice = new SimulatedDevice( - TestDeviceInfo.CreateABPDevice(1, gatewayID: deviceGatewayID, deviceClassType: 'c'), + TestDeviceInfo.CreateABPDevice(1, gatewayID: deviceGatewayID, deviceClassType: LoRaDeviceClassType.C), frmCntUp: InitialDeviceFcntUp, frmCntDown: InitialDeviceFcntDown); @@ -450,7 +451,6 @@ public async Task Unconfirmed_Cloud_To_Device_From_Decoder_Should_Call_ClassC_Me { PreferredGatewayResult = new PreferredGatewayResult() { - DevEUI = simulatedDevice.DevEUI, PreferredGatewayID = preferredGatewayID, CurrentFcntUp = PayloadFcnt, RequestFcntUp = PayloadFcnt, @@ -482,11 +482,12 @@ public async Task Unconfirmed_Cloud_To_Device_From_Decoder_Should_Call_ClassC_Me .ReturnsAsync((Message)null); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -534,7 +535,7 @@ public async Task When_Updating_PreferredGateway_And_FcntUp_Should_Save_Twin_Onc const uint InitialDeviceFcntDown = 20; var simulatedDevice = new SimulatedDevice( - TestDeviceInfo.CreateABPDevice(1, deviceClassType: 'c'), + TestDeviceInfo.CreateABPDevice(1, deviceClassType: LoRaDeviceClassType.C), frmCntUp: InitialDeviceFcntUp, frmCntDown: InitialDeviceFcntDown); @@ -545,7 +546,6 @@ public async Task When_Updating_PreferredGateway_And_FcntUp_Should_Save_Twin_Onc { PreferredGatewayResult = new PreferredGatewayResult() { - DevEUI = simulatedDevice.DevEUI, PreferredGatewayID = ServerGatewayID, CurrentFcntUp = PayloadFcnt, RequestFcntUp = PayloadFcnt, @@ -571,11 +571,12 @@ public async Task When_Updating_PreferredGateway_And_FcntUp_Should_Save_Twin_Onc .ReturnsAsync((Message)null); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); diff --git a/Tests/Integration/CloudToDeviceMessageSizeLimitShouldAbandonTests.cs b/Tests/Integration/CloudToDeviceMessageSizeLimitShouldAbandonTests.cs index 0ce5397ccd..10c094a637 100644 --- a/Tests/Integration/CloudToDeviceMessageSizeLimitShouldAbandonTests.cs +++ b/Tests/Integration/CloudToDeviceMessageSizeLimitShouldAbandonTests.cs @@ -59,7 +59,7 @@ public class CloudToDeviceMessageSizeLimitShouldAbandonTests : CloudToDeviceMess var c2dPayloadSize = euRegion.GetMaxPayloadSize(euRegion.GetDefaultRX2ReceiveWindow(default).DataRate) - c2dMessageMacCommandSize + 1 // make message too long on purpose - - Constants.LoraProtocolOverheadSize; + - NetworkServer.Constants.LoraProtocolOverheadSize; var c2dMessagePayload = TestUtils.GeneratePayload("123457890", (int)c2dPayloadSize); @@ -88,11 +88,12 @@ public class CloudToDeviceMessageSizeLimitShouldAbandonTests : CloudToDeviceMess .ReturnsAsync(true); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -131,7 +132,7 @@ public class CloudToDeviceMessageSizeLimitShouldAbandonTests : CloudToDeviceMess { // Possible problem: manually casting frmPayload to array. No reversal. var frmPayload = payloadDataDown.Frmpayload.ToArray(); - var macCommands = MacCommand.CreateServerMacCommandFromBytes(simulatedDevice.DevEUI, frmPayload); + var macCommands = MacCommand.CreateServerMacCommandFromBytes(frmPayload); Assert.Single(macCommands); Assert.IsType(macCommands.First()); } diff --git a/Tests/Integration/CloudToDeviceMessageSizeLimitShouldAcceptTests.cs b/Tests/Integration/CloudToDeviceMessageSizeLimitShouldAcceptTests.cs index f259aa1344..148f681c15 100644 --- a/Tests/Integration/CloudToDeviceMessageSizeLimitShouldAcceptTests.cs +++ b/Tests/Integration/CloudToDeviceMessageSizeLimitShouldAcceptTests.cs @@ -72,7 +72,7 @@ public class CloudToDeviceMessageSizeLimitShouldAcceptTests : CloudToDeviceMessa var c2dPayloadSize = euRegion.GetMaxPayloadSize(expectedDownlinkDatr) - c2dMessageMacCommandSize - upstreamMessageMacCommandSize - - Constants.LoraProtocolOverheadSize; + - NetworkServer.Constants.LoraProtocolOverheadSize; var c2dMessagePayload = TestUtils.GeneratePayload("123457890", (int)c2dPayloadSize); @@ -97,11 +97,12 @@ public class CloudToDeviceMessageSizeLimitShouldAcceptTests : CloudToDeviceMessa .ReturnsAsync(true); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -139,7 +140,7 @@ public class CloudToDeviceMessageSizeLimitShouldAcceptTests : CloudToDeviceMessa if (expectedMacCommandsCount > 0) { - var macCommands = MacCommand.CreateServerMacCommandFromBytes(simulatedDevice.DevEUI, payloadDataDown.Fopts); + var macCommands = MacCommand.CreateServerMacCommandFromBytes(payloadDataDown.Fopts); Assert.Equal(expectedMacCommandsCount, macCommands.Count); } else diff --git a/Tests/Integration/CloudToDeviceMessageSizeLimitShouldRejectTests.cs b/Tests/Integration/CloudToDeviceMessageSizeLimitShouldRejectTests.cs index f0dc73288f..3490649648 100644 --- a/Tests/Integration/CloudToDeviceMessageSizeLimitShouldRejectTests.cs +++ b/Tests/Integration/CloudToDeviceMessageSizeLimitShouldRejectTests.cs @@ -52,7 +52,7 @@ public class CloudToDeviceMessageSizeLimitShouldRejectTests : CloudToDeviceMessa var c2dPayloadSize = region.GetMaxPayloadSize(expectedDownlinkDatr) - c2dMessageMacCommandSize + 1 // make message too long on purpose - - Constants.LoraProtocolOverheadSize; + - NetworkServer.Constants.LoraProtocolOverheadSize; var c2dMessagePayload = TestUtils.GeneratePayload("123457890", (int)c2dPayloadSize); @@ -76,11 +76,12 @@ public class CloudToDeviceMessageSizeLimitShouldRejectTests : CloudToDeviceMessa .ReturnsAsync(true); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); diff --git a/Tests/Integration/CloudToDeviceMessageTests.cs b/Tests/Integration/CloudToDeviceMessageTests.cs index 14fa9bef6f..7f4b51b089 100644 --- a/Tests/Integration/CloudToDeviceMessageTests.cs +++ b/Tests/Integration/CloudToDeviceMessageTests.cs @@ -20,6 +20,7 @@ namespace LoRaWan.Tests.Integration using Xunit; using Xunit.Abstractions; using static LoRaWan.ReceiveWindowNumber; + using IoTHubDeviceInfo = NetworkServer.IoTHubDeviceInfo; // End to end tests without external dependencies (IoT Hub, Service Facade Function) // Cloud to device message processing tests (Join tests are handled in other class) @@ -51,10 +52,11 @@ public async Task When_Device_With_Downlink_Disabled_Received_Unconfirmed_Data_S DeviceCache.Register(cachedDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -120,10 +122,11 @@ public async Task When_Device_With_Downlink_Disabled_Received_Confirmed_Data_Sho DeviceCache.Register(cachedDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -152,7 +155,7 @@ public async Task When_Device_With_Downlink_Disabled_Received_Confirmed_Data_Sho public async Task OTAA_Unconfirmed_With_Cloud_To_Device_Message_Returns_Downstream_Message(uint initialDeviceFcntUp, uint payloadFcnt) { const uint InitialDeviceFcntDown = 20; - var needsToSaveFcnt = payloadFcnt - initialDeviceFcntUp >= Constants.MaxFcntUnsavedDelta; + var needsToSaveFcnt = payloadFcnt - initialDeviceFcntUp >= NetworkServer.Constants.MaxFcntUnsavedDelta; var simulatedDevice = new SimulatedDevice( TestDeviceInfo.CreateABPDevice(1, gatewayID: ServerConfiguration.GatewayID), @@ -185,11 +188,12 @@ public async Task OTAA_Unconfirmed_With_Cloud_To_Device_Message_Returns_Downstre .ReturnsAsync(true); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -235,7 +239,7 @@ public async Task OTAA_Unconfirmed_With_Cloud_To_Device_Message_Returns_Downstre public async Task OTAA_Confirmed_With_Cloud_To_Device_Message_Returns_Downstream_Message(uint initialDeviceFcntUp, uint payloadFcnt) { const uint InitialDeviceFcntDown = 20; - var needsToSaveFcnt = payloadFcnt - initialDeviceFcntUp >= Constants.MaxFcntUnsavedDelta; + var needsToSaveFcnt = payloadFcnt - initialDeviceFcntUp >= NetworkServer.Constants.MaxFcntUnsavedDelta; var simulatedDevice = new SimulatedDevice( TestDeviceInfo.CreateABPDevice(1, gatewayID: ServerConfiguration.GatewayID), @@ -264,11 +268,12 @@ public async Task OTAA_Confirmed_With_Cloud_To_Device_Message_Returns_Downstream .ReturnsAsync(true); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -324,8 +329,7 @@ public async Task When_In_Time_For_First_Window_Should_Send_Downstream_In_First_ var devEUI = simulatedDevice.DevEUI; // Will get twin to initialize LoRaDevice - var deviceTwin = TestUtils.CreateABPTwin(simulatedDevice); - LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(deviceTwin); + LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(simulatedDevice.GetDefaultAbpTwin()); LoRaDeviceClient.Setup(x => x.SendEventAsync(It.IsNotNull(), null)) .ReturnsAsync(true); @@ -347,10 +351,11 @@ public async Task When_In_Time_For_First_Window_Should_Send_Downstream_In_First_ .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEUI, "adad").AsList())); using var cache = NewMemoryCache(); - using var loRaDeviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var loRaDeviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, loRaDeviceRegistry, FrameCounterUpdateStrategyProvider); @@ -386,7 +391,7 @@ public async Task When_In_Time_For_First_Window_Should_Send_Downstream_In_First_ Assert.Equal(PayloadFcnt, loRaDevice.FCntUp); // 5. Frame counter down is updated - var expectedFcntDown = InitialDeviceFcntDown + Constants.MaxFcntUnsavedDelta; // adding 10 as buffer when creating a new device instance + var expectedFcntDown = InitialDeviceFcntDown + NetworkServer.Constants.MaxFcntUnsavedDelta; // adding 10 as buffer when creating a new device instance Assert.Equal(expectedFcntDown, loRaDevice.FCntDown); Assert.Equal(expectedFcntDown, payloadDataDown.Fcnt); @@ -410,8 +415,7 @@ public async Task When_Too_Late_For_First_Window_Should_Send_Downstream_In_Secon var devEUI = simulatedDevice.DevEUI; // Will get twin to initialize LoRaDevice - var deviceTwin = TestUtils.CreateABPTwin(simulatedDevice); - LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(deviceTwin); + LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(simulatedDevice.GetDefaultAbpTwin()); LoRaDeviceClient.Setup(x => x.SendEventAsync(It.IsNotNull(), null)) .ReturnsAsync(true); @@ -433,10 +437,11 @@ public async Task When_Too_Late_For_First_Window_Should_Send_Downstream_In_Secon .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEUI, "adad").AsList())); using var cache = NewMemoryCache(); - using var loRaDeviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var loRaDeviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, loRaDeviceRegistry, FrameCounterUpdateStrategyProvider); @@ -471,7 +476,7 @@ public async Task When_Too_Late_For_First_Window_Should_Send_Downstream_In_Secon Assert.Equal(PayloadFcnt, loRaDevice.FCntUp); // 5. Frame counter down is updated - var expectedFcntDown = InitialDeviceFcntDown + Constants.MaxFcntUnsavedDelta; // adding 10 as buffer when creating a new device instance + var expectedFcntDown = InitialDeviceFcntDown + NetworkServer.Constants.MaxFcntUnsavedDelta; // adding 10 as buffer when creating a new device instance Assert.Equal(expectedFcntDown, loRaDevice.FCntDown); Assert.Equal(expectedFcntDown, payloadDataDown.Fcnt); @@ -495,14 +500,10 @@ public async Task When_Device_Prefers_Second_Window_Should_Send_Downstream_In_Se var devEUI = simulatedDevice.DevEUI; // Will get twin to initialize LoRaDevice - var deviceTwin = TestUtils.CreateABPTwin( - simulatedDevice, - desiredProperties: new Dictionary - { - { TwinProperty.PreferredWindow, 2 } - }); + var twin = LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetAbpDesiredTwinProperties() with { PreferredWindow = ReceiveWindow2 }, + simulatedDevice.GetAbpReportedTwinProperties()); - LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(deviceTwin); + LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(twin); LoRaDeviceClient.Setup(x => x.UpdateReportedPropertiesAsync(It.IsNotNull(), It.IsAny())) .ReturnsAsync(true); @@ -524,10 +525,11 @@ public async Task When_Device_Prefers_Second_Window_Should_Send_Downstream_In_Se .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEUI, "adad").AsList())); using var cache = NewMemoryCache(); - using var loRaDeviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var loRaDeviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, loRaDeviceRegistry, FrameCounterUpdateStrategyProvider); @@ -562,7 +564,7 @@ public async Task When_Device_Prefers_Second_Window_Should_Send_Downstream_In_Se Assert.Equal(PayloadFcnt, loRaDevice.FCntUp); // 5. Frame counter down is updated - var expectedFcntDown = InitialDeviceFcntDown + Constants.MaxFcntUnsavedDelta - 1 + 1; // adding 9 as buffer when creating a new device instance + var expectedFcntDown = InitialDeviceFcntDown + NetworkServer.Constants.MaxFcntUnsavedDelta - 1 + 1; // adding 9 as buffer when creating a new device instance Assert.Equal(expectedFcntDown, loRaDevice.FCntDown); Assert.Equal(expectedFcntDown, payloadDataDown.Fcnt); Assert.Equal(0U, loRaDevice.FCntDown - loRaDevice.LastSavedFCntDown); @@ -623,11 +625,12 @@ public async Task When_Device_Prefers_Second_Window_Should_Send_Downstream_In_Se loRaDevice.PreferredWindow = preferredWindow; using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loRaDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loRaDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -699,11 +702,12 @@ public async Task OTAA_Unconfirmed_With_Cloud_To_Device_Mac_Command_Returns_Down .ReturnsAsync(true); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -803,11 +807,12 @@ public async Task OTAA_Unconfirmed_With_Cloud_To_Device_Mac_Command_Fails_Due_To .ReturnsAsync(true); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -862,11 +867,12 @@ public async Task Unconfirmed_Cloud_To_Device_From_Decoder_Should_Send_Downstrea PayloadDecoder.SetDecoder(payloadDecoder.Object); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -924,9 +930,8 @@ public async Task When_Takes_Too_Long_Processing_C2D_Should_Abandon_Message() var devEUI = simulatedDevice.DevEUI; // Will get twin to initialize LoRaDevice - var deviceTwin = TestUtils.CreateABPTwin(simulatedDevice); - this.LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(deviceTwin); - this.LoRaDeviceClient.Setup(x => x.UpdateReportedPropertiesAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); + LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(simulatedDevice.GetDefaultAbpTwin()); + LoRaDeviceClient.Setup(x => x.UpdateReportedPropertiesAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); LoRaDeviceClient.Setup(x => x.SendEventAsync(It.IsNotNull(), null)) .ReturnsAsync(true); @@ -944,10 +949,11 @@ public async Task When_Takes_Too_Long_Processing_C2D_Should_Abandon_Message() .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEUI, "adad").AsList())); using var cache = NewMemoryCache(); - using var loRaDeviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var loRaDeviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, loRaDeviceRegistry, FrameCounterUpdateStrategyProvider); @@ -975,7 +981,7 @@ public async Task When_Takes_Too_Long_Processing_C2D_Should_Abandon_Message() // 3. Device FcntDown did change Assert.True(DeviceCache.TryGetForPayload(request.Payload, out var loRaDevice)); - Assert.Equal(InitialDeviceFcntDown + Constants.MaxFcntUnsavedDelta, loRaDevice.FCntDown); + Assert.Equal(InitialDeviceFcntDown + NetworkServer.Constants.MaxFcntUnsavedDelta, loRaDevice.FCntDown); } } } diff --git a/Tests/Integration/ConcentratorDeduplicationDataMessagesIntegrationTests.cs b/Tests/Integration/ConcentratorDeduplicationDataMessagesIntegrationTests.cs index 72c941b90b..9aa4996f80 100644 --- a/Tests/Integration/ConcentratorDeduplicationDataMessagesIntegrationTests.cs +++ b/Tests/Integration/ConcentratorDeduplicationDataMessagesIntegrationTests.cs @@ -110,12 +110,12 @@ public ConcentratorDeduplicationDataMessagesIntegrationTests(ITestOutputHelper t #region UnconfirmedDataMessage [Theory] - [DeduplicationTestData("11-11-11-11-11-11-11-11", "11-11-11-11-11-11-11-11", DeduplicationMode.Drop, expectedFrameCounterResets: 1, expectedBundlerCalls: 2, expectedFrameCounterDownCalls: 0, expectedMessagesUp: 1, expectedMessagesDown: 0, expectedTwinSaves: 2)] // resubmission for unconfirmed first messages can happen when the device was reset + sends the same payload within the cache retention timewindow, - [DeduplicationTestData("11-11-11-11-11-11-11-11", "11-11-11-11-11-11-11-11", DeduplicationMode.Mark, expectedFrameCounterResets: 1, expectedBundlerCalls: 2, expectedFrameCounterDownCalls: 0, expectedMessagesUp: 2, expectedMessagesDown: 0, expectedTwinSaves: 2)] // [cont] we have no reliable way of knowing that - [DeduplicationTestData("11-11-11-11-11-11-11-11", "11-11-11-11-11-11-11-11", DeduplicationMode.None, expectedFrameCounterResets: 1, expectedBundlerCalls: 2, expectedFrameCounterDownCalls: 0, expectedMessagesUp: 2, expectedMessagesDown: 0, expectedTwinSaves: 2)] // [cont] so deduplication here is on a best-effort case - [DeduplicationTestData("11-11-11-11-11-11-11-11", "22-22-22-22-22-22-22-22", DeduplicationMode.Drop, expectedFrameCounterResets: 1, expectedBundlerCalls: 1, expectedFrameCounterDownCalls: 0, expectedMessagesUp: 1, expectedMessagesDown: 0, expectedTwinSaves: 1)] // duplicate - [DeduplicationTestData("11-11-11-11-11-11-11-11", "22-22-22-22-22-22-22-22", DeduplicationMode.Mark, expectedFrameCounterResets: 1, expectedBundlerCalls: 1, expectedFrameCounterDownCalls: 0, expectedMessagesUp: 2, expectedMessagesDown: 0, expectedTwinSaves: 2)] // soft duplicate - [DeduplicationTestData("11-11-11-11-11-11-11-11", "22-22-22-22-22-22-22-22", DeduplicationMode.None, expectedFrameCounterResets: 1, expectedBundlerCalls: 1, expectedFrameCounterDownCalls: 0, expectedMessagesUp: 2, expectedMessagesDown: 0, expectedTwinSaves: 2)] // soft duplicate + [DeduplicationTestData("11-11-11-11-11-11-11-11", "11-11-11-11-11-11-11-11", DeduplicationMode.Drop, expectedFrameCounterResets: 1, expectedBundlerCalls: 2, expectedFrameCounterDownCalls: 0, expectedMessagesUp: 1, expectedMessagesDown: 0, expectedTwinSaves: 4)] // resubmission for unconfirmed first messages can happen when the device was reset + sends the same payload within the cache retention timewindow, + [DeduplicationTestData("11-11-11-11-11-11-11-11", "11-11-11-11-11-11-11-11", DeduplicationMode.Mark, expectedFrameCounterResets: 1, expectedBundlerCalls: 2, expectedFrameCounterDownCalls: 0, expectedMessagesUp: 2, expectedMessagesDown: 0, expectedTwinSaves: 4)] // [cont] we have no reliable way of knowing that + [DeduplicationTestData("11-11-11-11-11-11-11-11", "11-11-11-11-11-11-11-11", DeduplicationMode.None, expectedFrameCounterResets: 1, expectedBundlerCalls: 2, expectedFrameCounterDownCalls: 0, expectedMessagesUp: 2, expectedMessagesDown: 0, expectedTwinSaves: 4)] // [cont] so deduplication here is on a best-effort case + [DeduplicationTestData("11-11-11-11-11-11-11-11", "22-22-22-22-22-22-22-22", DeduplicationMode.Drop, expectedFrameCounterResets: 1, expectedBundlerCalls: 1, expectedFrameCounterDownCalls: 0, expectedMessagesUp: 1, expectedMessagesDown: 0, expectedTwinSaves: 2)] // duplicate + [DeduplicationTestData("11-11-11-11-11-11-11-11", "22-22-22-22-22-22-22-22", DeduplicationMode.Mark, expectedFrameCounterResets: 1, expectedBundlerCalls: 1, expectedFrameCounterDownCalls: 0, expectedMessagesUp: 2, expectedMessagesDown: 0, expectedTwinSaves: 4)] // soft duplicate + [DeduplicationTestData("11-11-11-11-11-11-11-11", "22-22-22-22-22-22-22-22", DeduplicationMode.None, expectedFrameCounterResets: 1, expectedBundlerCalls: 1, expectedFrameCounterDownCalls: 0, expectedMessagesUp: 2, expectedMessagesDown: 0, expectedTwinSaves: 4)] // soft duplicate public async Task When_First_Unconfirmed_Data_Message_Test_All_Different_DeduplicationModes( string station1, string station2, @@ -156,12 +156,12 @@ public ConcentratorDeduplicationDataMessagesIntegrationTests(ITestOutputHelper t #region ConfirmedDataMessage [Theory] - [DeduplicationTestData("11-11-11-11-11-11-11-11", "11-11-11-11-11-11-11-11", DeduplicationMode.Drop, expectedFrameCounterResets: 1, expectedBundlerCalls: 2, expectedFrameCounterDownCalls: 2, expectedMessagesUp: 1, expectedMessagesDown: 2, expectedTwinSaves: 2)] // resubmission with drop - [DeduplicationTestData("11-11-11-11-11-11-11-11", "11-11-11-11-11-11-11-11", DeduplicationMode.Mark, expectedFrameCounterResets: 1, expectedBundlerCalls: 2, expectedFrameCounterDownCalls: 2, expectedMessagesUp: 2, expectedMessagesDown: 2, expectedTwinSaves: 2)] // resubmission - [DeduplicationTestData("11-11-11-11-11-11-11-11", "11-11-11-11-11-11-11-11", DeduplicationMode.None, expectedFrameCounterResets: 1, expectedBundlerCalls: 2, expectedFrameCounterDownCalls: 2, expectedMessagesUp: 2, expectedMessagesDown: 2, expectedTwinSaves: 2)] // resubmission - [DeduplicationTestData("11-11-11-11-11-11-11-11", "22-22-22-22-22-22-22-22", DeduplicationMode.Drop, expectedFrameCounterResets: 1, expectedBundlerCalls: 1, expectedFrameCounterDownCalls: 1, expectedMessagesUp: 1, expectedMessagesDown: 1, expectedTwinSaves: 1)] // duplicate - [DeduplicationTestData("11-11-11-11-11-11-11-11", "22-22-22-22-22-22-22-22", DeduplicationMode.Mark, expectedFrameCounterResets: 1, expectedBundlerCalls: 1, expectedFrameCounterDownCalls: 1, expectedMessagesUp: 2, expectedMessagesDown: 1, expectedTwinSaves: 2)] // soft duplicate - [DeduplicationTestData("11-11-11-11-11-11-11-11", "22-22-22-22-22-22-22-22", DeduplicationMode.None, expectedFrameCounterResets: 1, expectedBundlerCalls: 1, expectedFrameCounterDownCalls: 1, expectedMessagesUp: 2, expectedMessagesDown: 1, expectedTwinSaves: 2)] // soft duplicate + [DeduplicationTestData("11-11-11-11-11-11-11-11", "11-11-11-11-11-11-11-11", DeduplicationMode.Drop, expectedFrameCounterResets: 1, expectedBundlerCalls: 2, expectedFrameCounterDownCalls: 2, expectedMessagesUp: 1, expectedMessagesDown: 2, expectedTwinSaves: 4)] // resubmission with drop + [DeduplicationTestData("11-11-11-11-11-11-11-11", "11-11-11-11-11-11-11-11", DeduplicationMode.Mark, expectedFrameCounterResets: 1, expectedBundlerCalls: 2, expectedFrameCounterDownCalls: 2, expectedMessagesUp: 2, expectedMessagesDown: 2, expectedTwinSaves: 4)] // resubmission + [DeduplicationTestData("11-11-11-11-11-11-11-11", "11-11-11-11-11-11-11-11", DeduplicationMode.None, expectedFrameCounterResets: 1, expectedBundlerCalls: 2, expectedFrameCounterDownCalls: 2, expectedMessagesUp: 2, expectedMessagesDown: 2, expectedTwinSaves: 4)] // resubmission + [DeduplicationTestData("11-11-11-11-11-11-11-11", "22-22-22-22-22-22-22-22", DeduplicationMode.Drop, expectedFrameCounterResets: 1, expectedBundlerCalls: 1, expectedFrameCounterDownCalls: 1, expectedMessagesUp: 1, expectedMessagesDown: 1, expectedTwinSaves: 2)] // duplicate + [DeduplicationTestData("11-11-11-11-11-11-11-11", "22-22-22-22-22-22-22-22", DeduplicationMode.Mark, expectedFrameCounterResets: 1, expectedBundlerCalls: 1, expectedFrameCounterDownCalls: 1, expectedMessagesUp: 2, expectedMessagesDown: 1, expectedTwinSaves: 4)] // soft duplicate + [DeduplicationTestData("11-11-11-11-11-11-11-11", "22-22-22-22-22-22-22-22", DeduplicationMode.None, expectedFrameCounterResets: 1, expectedBundlerCalls: 1, expectedFrameCounterDownCalls: 1, expectedMessagesUp: 2, expectedMessagesDown: 1, expectedTwinSaves: 4)] // soft duplicate public async Task When_First_Confirmed_Data_Message_Test_All_Different_DeduplicationModes( string station1, string station2, @@ -226,7 +226,7 @@ public ConcentratorDeduplicationDataMessagesIntegrationTests(ITestOutputHelper t var request1 = CreateOTAARequest(dataPayload, station1); var request2 = CreateOTAARequest(dataPayload, station2); - using var loraOTAADevice = new LoRaDevice(simulatedOTAADevice.DevAddr, simulatedOTAADevice.DevEUI, ConnectionManager); + await using var loraOTAADevice = new LoRaDevice(simulatedOTAADevice.DevAddr, simulatedOTAADevice.DevEUI, ConnectionManager); loraOTAADevice.AppKey = AppKey.Parse(value32); loraOTAADevice.Deduplication = deduplicationMode; @@ -247,8 +247,8 @@ LoRaRequest CreateOTAARequest(LoRaPayloadData payload, string station) } [Theory] - [InlineData("11-11-11-11-11-11-11-11", "11-11-11-11-11-11-11-11", 2, 2, 2)] - [InlineData("11-11-11-11-11-11-11-11", "22-22-22-22-22-22-22-22", 1, 2, 1)] + [InlineData("11-11-11-11-11-11-11-11", "11-11-11-11-11-11-11-11", 0, 2, 2)] + [InlineData("11-11-11-11-11-11-11-11", "22-22-22-22-22-22-22-22", 0, 2, 1)] public async Task When_SingleGateway_Deduplication_Should_Work_The_Same_Way( string station1, string station2, @@ -432,14 +432,14 @@ WaitableLoRaRequest CreateRequest(string stationEui) this.dataRequestHandlerMock.Verify(x => x.SaveChangesToDeviceAsyncAssert(), Times.Exactly(twinSaves)); } - protected override void Dispose(bool disposing) + protected override async ValueTask DisposeAsync(bool disposing) { - base.Dispose(disposing); + await base.DisposeAsync(disposing); if (disposing) { this.cache.Dispose(); - this.loraABPDevice.Dispose(); this.testOutputLoggerFactory.Dispose(); + await this.loraABPDevice.DisposeAsync(); } } } diff --git a/Tests/Integration/ConcentratorDeduplicationJoinRequestsIntegrationTests.cs b/Tests/Integration/ConcentratorDeduplicationJoinRequestsIntegrationTests.cs deleted file mode 100644 index 4632035b35..0000000000 --- a/Tests/Integration/ConcentratorDeduplicationJoinRequestsIntegrationTests.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace LoRaWan.Tests.Integration -{ - using System.Threading; - using System.Threading.Tasks; - using Common; - using LoRaTools.Regions; - using LoRaWan.NetworkServer; - using Microsoft.Azure.Devices.Shared; - using Microsoft.Extensions.Caching.Memory; - using Microsoft.Extensions.Logging; - using Moq; - using Xunit; - using Xunit.Abstractions; - - public sealed class ConcentratorDeduplicationJoinRequestsIntegrationTests : MessageProcessorTestBase - { - private readonly MemoryCache cache; - private readonly JoinRequestMessageHandler joinRequestHandler; - private readonly SimulatedDevice simulatedDevice; - private readonly Mock deviceMock; - private readonly TestOutputLoggerFactory testOutputLoggerFactory; - private bool _disposedValue; - - public ConcentratorDeduplicationJoinRequestsIntegrationTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) - { - this.simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateOTAADevice(0)); - this.deviceMock = new Mock(MockBehavior.Default, - this.simulatedDevice.DevAddr, - this.simulatedDevice.DevEUI, - ConnectionManager) - { - CallBase = true - }; - this.deviceMock.Object.AppKey = this.simulatedDevice.AppKey; - this.deviceMock.Object.AppEui = this.simulatedDevice.AppEui; - this.deviceMock.Object.IsOurDevice = true; - this.testOutputLoggerFactory = new TestOutputLoggerFactory(testOutputHelper); - - this.cache = new MemoryCache(new MemoryCacheOptions()); - var concentratorDeduplication = new ConcentratorDeduplication(this.cache, this.testOutputLoggerFactory.CreateLogger()); - var deviceRegistryMock = new Mock(); - _ = deviceRegistryMock.Setup(x => x.GetDeviceForJoinRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(this.deviceMock.Object); - - var clientMock = new Mock(); - _ = clientMock.Setup(x => x.UpdateReportedPropertiesAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); - ConnectionManager.Register(this.deviceMock.Object, clientMock.Object); - - this.joinRequestHandler = new JoinRequestMessageHandler( - ServerConfiguration, - concentratorDeduplication, - deviceRegistryMock.Object, - this.testOutputLoggerFactory.CreateLogger(), - null); - } - - [Fact] - public async Task When_Same_Join_Request_Received_Multiple_Times_Succeeds_Only_Once() - { - var joinRequest = this.simulatedDevice.CreateJoinRequest(); - var loraRequest = CreateWaitableRequest(joinRequest); - loraRequest.SetPayload(joinRequest); - loraRequest.SetRegion(new RegionEU868()); - - // first request - await this.joinRequestHandler.ProcessJoinRequestAsync(loraRequest); - - // repeat - await this.joinRequestHandler.ProcessJoinRequestAsync(loraRequest); - - // assert - this.deviceMock.Verify(x => x.UpdateAfterJoinAsync(It.IsAny(), It.IsAny()), Times.Once()); - - // do another request - joinRequest = this.simulatedDevice.CreateJoinRequest(); - loraRequest.SetPayload(joinRequest); - await this.joinRequestHandler.ProcessJoinRequestAsync(loraRequest); - this.deviceMock.Verify(x => x.UpdateAfterJoinAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); - } - - protected override void Dispose(bool disposing) - { - if (!this._disposedValue) - { - if (disposing) - { - this.cache.Dispose(); - this.testOutputLoggerFactory.Dispose(); - } - - this._disposedValue = true; - } - - // Call base class implementation. - base.Dispose(disposing); - } - } -} diff --git a/Tests/Integration/DecoderTests.cs b/Tests/Integration/DecoderTests.cs index dd540d53c5..e8c2554847 100644 --- a/Tests/Integration/DecoderTests.cs +++ b/Tests/Integration/DecoderTests.cs @@ -22,7 +22,9 @@ namespace LoRaWan.Tests.Integration // Decoder tests tests public class DecoderTests : MessageProcessorTestBase { - public DecoderTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } + private readonly ITestOutputHelper testOutputHelper; + + public DecoderTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) => this.testOutputHelper = testOutputHelper; /// /// SensorDecoder: none @@ -84,11 +86,12 @@ public async Task When_No_Decoder_Is_Defined_Sends_Raw_Payload(string deviceGate } using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loRaDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loRaDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -173,11 +176,12 @@ public async Task When_Using_DecoderValueSensor_Should_Send_Decoded_Value(string .ReturnsAsync((Message)null); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loRaDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loRaDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -251,11 +255,12 @@ public async Task When_Using_Custom_Decoder_Returns_String_Should_Send_Decoded_V .ReturnsAsync((Message)null); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loRaDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loRaDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -269,8 +274,8 @@ public async Task When_Using_Custom_Decoder_Returns_String_Should_Send_Decoded_V }; }); - using var httpClient = new HttpClient(httpMessageHandler); - PayloadDecoder.SetDecoder(new LoRaPayloadDecoder(httpClient)); + using var httpClientFactory = new MockHttpClientFactory(httpMessageHandler); + PayloadDecoder.SetDecoder(new LoRaPayloadDecoder(httpClientFactory, new TestOutputLogger(this.testOutputHelper))); // sends unconfirmed message var unconfirmedMessagePayload = simulatedDevice.CreateUnconfirmedDataUpMessage("1", fcnt: 1); @@ -338,11 +343,12 @@ public async Task When_Using_Custom_Decoder_Returns_Empty_Should_Send_Decoded_Va .ReturnsAsync((Message)null); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loRaDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loRaDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -356,8 +362,8 @@ public async Task When_Using_Custom_Decoder_Returns_Empty_Should_Send_Decoded_Va }; }); - using var httpClient = new HttpClient(httpMessageHandler); - PayloadDecoder.SetDecoder(new LoRaPayloadDecoder(httpClient)); + using var mockHttpClientFactory = new MockHttpClientFactory(httpMessageHandler); + PayloadDecoder.SetDecoder(new LoRaPayloadDecoder(mockHttpClientFactory, new TestOutputLogger(this.testOutputHelper))); // sends unconfirmed message var unconfirmedMessagePayload = simulatedDevice.CreateUnconfirmedDataUpMessage("1", fcnt: 1); @@ -427,11 +433,12 @@ public async Task When_Using_Custom_Decoder_Returns_JsonString_Should_Send_Decod .ReturnsAsync((Message)null); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loRaDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loRaDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -445,8 +452,8 @@ public async Task When_Using_Custom_Decoder_Returns_JsonString_Should_Send_Decod }; }); - using var httpClient = new HttpClient(httpMessageHandler); - PayloadDecoder.SetDecoder(new LoRaPayloadDecoder(httpClient)); + using var mockHttpClientFactory = new MockHttpClientFactory(httpMessageHandler); + PayloadDecoder.SetDecoder(new LoRaPayloadDecoder(mockHttpClientFactory, new TestOutputLogger(this.testOutputHelper))); // sends unconfirmed message var unconfirmedMessagePayload = simulatedDevice.CreateUnconfirmedDataUpMessage("1", fcnt: 1); @@ -516,11 +523,12 @@ public async Task When_Using_Custom_Decoder_Returns_Complex_Object_Should_Send_D .ReturnsAsync((Message)null); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loRaDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loRaDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -536,8 +544,8 @@ public async Task When_Using_Custom_Decoder_Returns_Complex_Object_Should_Send_D }; }); - using var httpClient = new HttpClient(httpMessageHandler); - PayloadDecoder.SetDecoder(new LoRaPayloadDecoder(httpClient)); + using var mockHttpClientFactory = new MockHttpClientFactory(httpMessageHandler); + PayloadDecoder.SetDecoder(new LoRaPayloadDecoder(mockHttpClientFactory, new TestOutputLogger(this.testOutputHelper))); // sends unconfirmed message var unconfirmedMessagePayload = simulatedDevice.CreateUnconfirmedDataUpMessage("1", fcnt: 1); @@ -605,11 +613,12 @@ public async Task When_Using_Custom_Fails_Returns_Sets_Error_Information_In_Valu .ReturnsAsync((Message)null); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loRaDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loRaDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -623,8 +632,8 @@ public async Task When_Using_Custom_Fails_Returns_Sets_Error_Information_In_Valu }; }); - using var httpClient = new HttpClient(httpMessageHandler); - PayloadDecoder.SetDecoder(new LoRaPayloadDecoder(httpClient)); + using var mockHttpClientFactory = new MockHttpClientFactory(httpMessageHandler); + PayloadDecoder.SetDecoder(new LoRaPayloadDecoder(mockHttpClientFactory, new TestOutputLogger(this.testOutputHelper))); // sends unconfirmed message var unconfirmedMessagePayload = simulatedDevice.CreateUnconfirmedDataUpMessage("1", fcnt: 1); @@ -667,11 +676,12 @@ public async Task When_Resent_Message_Using_Custom_Decoder_Returns_Complex_Objec // LoRaDeviceClient.Setup(x => x.ReceiveAsync(It.IsNotNull())) // .ReturnsAsync((Message)null); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loRaDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loRaDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -687,8 +697,8 @@ public async Task When_Resent_Message_Using_Custom_Decoder_Returns_Complex_Objec }; }); - using var httpClient = new HttpClient(httpMessageHandler); - PayloadDecoder.SetDecoder(new LoRaPayloadDecoder(httpClient)); + using var mockHttpClientFactory = new MockHttpClientFactory(httpMessageHandler); + PayloadDecoder.SetDecoder(new LoRaPayloadDecoder(mockHttpClientFactory, new TestOutputLogger(this.testOutputHelper))); // sends confirmed message var confirmedMessagePayload = simulatedDevice.CreateConfirmedDataUpMessage("1", fcnt: 10); diff --git a/Tests/Integration/DeduplicationStrategyIntegrationTests.cs b/Tests/Integration/DeduplicationStrategyIntegrationTests.cs index 94e610453b..9815fe080c 100644 --- a/Tests/Integration/DeduplicationStrategyIntegrationTests.cs +++ b/Tests/Integration/DeduplicationStrategyIntegrationTests.cs @@ -4,6 +4,7 @@ namespace LoRaWan.Tests.Integration { using System; + using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -37,52 +38,27 @@ public DeduplicationStrategyIntegrationTests(ITestOutputHelper testOutputHelper) public async Task When_Different_Strategies_Are_Used_Ensures_Correct_Upstream_And_Downstream_Processing(DeduplicationMode mode, bool confirmedMessages) { var messageProcessed = false; - var counterFctnDown = 0; foreach (var api in new[] { LoRaDeviceApi, SecondLoRaDeviceApi }) { api.Setup(x => x.ExecuteFunctionBundlerAsync(this.simulatedDevice.DevEUI, It.IsNotNull())) .ReturnsAsync((DevEui _, FunctionBundlerRequest _) => { - if (mode != DeduplicationMode.None) + var result = new FunctionBundlerResult(); + lock (functionLock) { - lock (this.functionLock) - { - var isDup = messageProcessed; - messageProcessed = true; - return new FunctionBundlerResult() - { - DeduplicationResult = new DeduplicationResult { IsDuplicate = isDup } - }; - } + result.NextFCntDown = messageProcessed ? (uint)0 : 1; + result.DeduplicationResult = (mode != DeduplicationMode.None) ? new DeduplicationResult { IsDuplicate = messageProcessed } : null; + messageProcessed = true; } - - // when DeduplicationMode is None, the Bundler deduplication is not invoked - return new FunctionBundlerResult(); + return result; }); - if (confirmedMessages) - { - api.Setup(x => x.NextFCntDownAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(() => - { - lock (this.functionLock) - { - counterFctnDown++; - return counterFctnDown switch - { - 1 => this.simulatedDevice.FrmCntDown + 1, // the first time the message is encountered, it gets a valid frame counter down - _ => 0 // any other time, it does not - }; - } - }); - } - api.Setup(x => x.ABPFcntCacheResetAsync(It.IsAny(), It.IsAny(), It.IsNotNull())) .ReturnsAsync(true); } - var actualDeviceTelemetries = new List(); + var actualDeviceTelemetries = new ConcurrentBag(); LoRaDeviceClient .Setup(x => x.SendEventAsync(It.IsNotNull(), null)) .ReturnsAsync(true) @@ -121,20 +97,16 @@ public async Task When_Different_Strategies_Are_Used_Ensures_Correct_Upstream_An private async Task SendTwoMessages(DeduplicationMode mode, bool confirmedMessages) { - var (messageProcessor1, dispose1) = CreateMessageDispatcher(ServerConfiguration, FrameCounterUpdateStrategyProvider, LoRaDeviceApi.Object); - var (messageProcessor2, dispose2) = CreateMessageDispatcher(SecondServerConfiguration, SecondFrameCounterUpdateStrategyProvider, SecondLoRaDeviceApi.Object); - using (messageProcessor1) - using (dispose1) - using (messageProcessor2) - using (dispose2) + await using var messageProcessor1 = CreateMessageDispatcher(ServerConfiguration, FrameCounterUpdateStrategyProvider, LoRaDeviceApi.Object); + await using var messageProcessor2 = CreateMessageDispatcher(SecondServerConfiguration, SecondFrameCounterUpdateStrategyProvider, SecondLoRaDeviceApi.Object); { var payload = confirmedMessages ? this.simulatedDevice.CreateConfirmedDataUpMessage("1234", fcnt: 1) : this.simulatedDevice.CreateUnconfirmedDataUpMessage("1234", fcnt: 1); using var request1 = CreateWaitableRequest(payload); using var request2 = CreateWaitableRequest(payload); - messageProcessor1.DispatchRequest(request1); - messageProcessor2.DispatchRequest(request2); + messageProcessor1.Value.DispatchRequest(request1); + messageProcessor2.Value.DispatchRequest(request2); await Task.WhenAll(request1.WaitCompleteAsync(Timeout.Infinite), request2.WaitCompleteAsync(Timeout.Infinite)); @@ -165,41 +137,34 @@ private async Task SendTwoMessages(DeduplicationMode mode, bool confirmedMessage } } - (MessageDispatcher, IDisposable) CreateMessageDispatcher(NetworkServerConfiguration networkServerConfiguration, - ILoRaDeviceFrameCounterUpdateStrategyProvider frameCounterUpdateStrategyProvider, - LoRaDeviceAPIServiceBase loRaDeviceApi) + AsyncDisposableValue CreateMessageDispatcher(NetworkServerConfiguration networkServerConfiguration, + ILoRaDeviceFrameCounterUpdateStrategyProvider frameCounterUpdateStrategyProvider, + LoRaDeviceAPIServiceBase loRaDeviceApi) { #pragma warning disable CA2000 // Dispose objects before losing scope (ownership transferred to caller) var cache = EmptyMemoryCache(); - var connectionManager = new LoRaDeviceClientConnectionManager(cache, new TestOutputLogger(this.testOutputHelper)); + var connectionManager = new LoRaDeviceClientConnectionManager(cache, new TestOutputLoggerFactory(this.testOutputHelper), new TestOutputLogger(this.testOutputHelper)); var concentratorDeduplication = new ConcentratorDeduplication(cache, new TestOutputLogger(this.testOutputHelper)); var requestHandler = CreateDefaultLoRaDataRequestHandler(networkServerConfiguration, frameCounterUpdateStrategyProvider, loRaDeviceApi, concentratorDeduplication); - var loRaDevice = TestUtils.CreateFromSimulatedDevice(this.simulatedDevice, connectionManager, requestHandler); + var loRaDevice = TestUtils.CreateFromSimulatedDevice(this.simulatedDevice, connectionManager, requestHandler.Value); loRaDevice.Deduplication = mode; connectionManager.Register(loRaDevice, LoRaDeviceClient.Object); var loraDeviceCache = CreateDeviceCache(loRaDevice); - var loraDeviceFactory = new TestLoRaDeviceFactory(networkServerConfiguration, LoRaDeviceClient.Object, connectionManager, loraDeviceCache, requestHandler); + var loraDeviceFactory = new TestLoRaDeviceFactory(networkServerConfiguration, LoRaDeviceClient.Object, connectionManager, loraDeviceCache, requestHandler.Value); var loRaDeviceRegistry = new LoRaDeviceRegistry(networkServerConfiguration, cache, loRaDeviceApi, loraDeviceFactory, loraDeviceCache); - return (new MessageDispatcher(networkServerConfiguration, loRaDeviceRegistry, frameCounterUpdateStrategyProvider), - new DisposableHolder(() => + return new AsyncDisposableValue( + TestMessageDispatcher.Create(cache, networkServerConfiguration, loRaDeviceRegistry, frameCounterUpdateStrategyProvider), + async () => #pragma warning restore CA2000 // Dispose objects before losing scope - { - cache.Dispose(); - connectionManager.Dispose(); - loRaDevice.Dispose(); - loraDeviceCache.Dispose(); - loRaDeviceRegistry.Dispose(); - })); + { + cache.Dispose(); + await connectionManager.DisposeAsync(); + await loRaDevice.DisposeAsync(); + await loraDeviceCache.DisposeAsync(); + await loRaDeviceRegistry.DisposeAsync(); + requestHandler.Dispose(); + }); } } - - private sealed class DisposableHolder : IDisposable - { - private readonly Action dispose; - - public DisposableHolder(Action dispose) => this.dispose = dispose; - - public void Dispose() => this.dispose(); - } } } diff --git a/Tests/Integration/DeduplicationWithRedisIntegrationTests.cs b/Tests/Integration/DeduplicationWithRedisIntegrationTests.cs new file mode 100644 index 0000000000..ec19fa1186 --- /dev/null +++ b/Tests/Integration/DeduplicationWithRedisIntegrationTests.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Integration +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using LoraKeysManagerFacade; + using LoraKeysManagerFacade.FunctionBundler; + using LoRaTools; + using LoRaWan.Tests.Common; + using Microsoft.ApplicationInsights.Extensibility; + using Microsoft.Azure.Devices; + using Moq; + using Xunit; + + /// + /// Tests to run against a real Redis instance. + /// + [Collection(RedisFixture.CollectionName)] + public sealed class DeduplicationTestWithRedis : IClassFixture, IDisposable + { + private readonly ILoRaDeviceCacheStore cache; + private readonly Mock serviceClientMock; + private readonly TelemetryConfiguration telemetryConfiguration; + private readonly Mock edgeDeviceGetter; + private readonly Mock channelPublisher; + private readonly DeduplicationExecutionItem deduplicationExecutionItem; + + public DeduplicationTestWithRedis(RedisFixture redis) + { + if (redis is null) throw new ArgumentNullException(nameof(redis)); + + this.cache = new LoRaDeviceCacheRedisStore(redis.Database); + this.serviceClientMock = new Mock(); + this.telemetryConfiguration = new TelemetryConfiguration(); + this.edgeDeviceGetter = new Mock(); + this.channelPublisher = new Mock(); + this.deduplicationExecutionItem = new DeduplicationExecutionItem(this.cache, + this.serviceClientMock.Object, + this.edgeDeviceGetter.Object, + this.channelPublisher.Object, + this.telemetryConfiguration); + } + + [Theory] + [InlineData("gateway1", 1, "gateway1", 1, true)] + [InlineData("gateway1", 1, "gateway1", 2, true)] + [InlineData("gateway1", 1, "gateway2", 1, true)] + [InlineData("gateway1", 1, "gateway2", 2, true)] + [InlineData("gateway1", 1, "gateway1", 1, false)] + [InlineData("gateway1", 1, "gateway1", 2, false)] + [InlineData("gateway1", 1, "gateway2", 1, false)] + [InlineData("gateway1", 1, "gateway2", 2, false)] + public async Task When_Called_Multiple_Times_With_Same_Device_Should_Detect_Duplicates_Direct_Method_Or_Pub_Sub(string gateway1, uint fcnt1, string gateway2, uint fcnt2, bool isEdgeDevice) + { + this.edgeDeviceGetter.Setup(m => m.IsEdgeDeviceAsync(It.IsAny(), It.IsAny())).ReturnsAsync(isEdgeDevice); + this.serviceClientMock.Setup( + x => x.InvokeDeviceMethodAsync(It.IsAny(), Constants.NetworkServerModuleId, It.IsAny(), It.IsAny())) + .ReturnsAsync(new CloudToDeviceMethodResult() { Status = 200 }); + + var devEUI = TestEui.GenerateDevEui(); + + var req1 = new FunctionBundlerRequest() { GatewayId = gateway1, ClientFCntUp = fcnt1, ClientFCntDown = fcnt1 }; + var pipeline1 = new FunctionBundlerPipelineExecuter(new IFunctionBundlerExecutionItem[] { this.deduplicationExecutionItem }, devEUI, req1); + var res1 = await this.deduplicationExecutionItem.ExecuteAsync(pipeline1); + + Assert.Equal(FunctionBundlerExecutionState.Continue, res1); + Assert.NotNull(pipeline1.Result.DeduplicationResult); + Assert.False(pipeline1.Result.DeduplicationResult.IsDuplicate); + + var req2 = new FunctionBundlerRequest() { GatewayId = gateway2, ClientFCntUp = fcnt2, ClientFCntDown = fcnt2 }; + var pipeline2 = new FunctionBundlerPipelineExecuter(new IFunctionBundlerExecutionItem[] { this.deduplicationExecutionItem }, devEUI, req2); + var res2 = await this.deduplicationExecutionItem.ExecuteAsync(pipeline2); + + Assert.NotNull(pipeline2.Result.DeduplicationResult); + + // same gateway -> no duplicate + if (gateway1 == gateway2) + { + Assert.Equal(FunctionBundlerExecutionState.Continue, res2); + Assert.False(pipeline2.Result.DeduplicationResult.IsDuplicate); + } + // different gateway, the same fcnt -> duplicate + else if (fcnt1 == fcnt2) + { + Assert.Equal(FunctionBundlerExecutionState.Abort, res2); + Assert.True(pipeline2.Result.DeduplicationResult.IsDuplicate); + } + // different gateway, higher fcnt -> no duplicate + else + { + Assert.Equal(FunctionBundlerExecutionState.Continue, res2); + Assert.False(pipeline2.Result.DeduplicationResult.IsDuplicate); + + if (isEdgeDevice) + { + // gateway1 should be notified that it needs to drop connection for the device + this.serviceClientMock.Verify(x => x.InvokeDeviceMethodAsync(gateway1, Constants.NetworkServerModuleId, + It.Is(m => m.MethodName == LoraKeysManagerFacadeConstants.CloudToDeviceCloseConnection + && m.GetPayloadAsJson().Contains(devEUI.ToString())), It.IsAny())); + } + else + { + this.channelPublisher.Verify(x => x.PublishAsync(gateway1, It.Is(c => c.Kind == RemoteCallKind.CloseConnection))); + this.serviceClientMock.VerifyNoOtherCalls(); + } + } + } + + [Fact] + public async Task When_Called_With_Different_Devices_Should_Detect_No_Duplicates() + { + var devEUI1 = TestEui.GenerateDevEui(); + var devEUI2 = TestEui.GenerateDevEui(); + const uint fcnt = 1; + + var req1 = new FunctionBundlerRequest() { GatewayId = "gateway1", ClientFCntUp = fcnt, ClientFCntDown = fcnt }; + var pipeline1 = new FunctionBundlerPipelineExecuter(new IFunctionBundlerExecutionItem[] { this.deduplicationExecutionItem }, devEUI1, req1); + var res1 = await this.deduplicationExecutionItem.ExecuteAsync(pipeline1); + + Assert.Equal(FunctionBundlerExecutionState.Continue, res1); + Assert.NotNull(pipeline1.Result.DeduplicationResult); + Assert.False(pipeline1.Result.DeduplicationResult.IsDuplicate); + + var req2 = new FunctionBundlerRequest() { GatewayId = "gateway2", ClientFCntUp = fcnt, ClientFCntDown = fcnt }; + var pipeline2 = new FunctionBundlerPipelineExecuter(new IFunctionBundlerExecutionItem[] { this.deduplicationExecutionItem }, devEUI2, req2); + var res2 = await this.deduplicationExecutionItem.ExecuteAsync(pipeline2); + + Assert.Equal(FunctionBundlerExecutionState.Continue, res2); + Assert.NotNull(pipeline2.Result.DeduplicationResult); + Assert.False(pipeline2.Result.DeduplicationResult.IsDuplicate); + } + + public void Dispose() => + this.telemetryConfiguration.Dispose(); + } +} diff --git a/Tests/Integration/DefaultLoRaDataRequestHandlerExceptionTests.cs b/Tests/Integration/DefaultLoRaDataRequestHandlerExceptionTests.cs new file mode 100644 index 0000000000..f4a8e6faa1 --- /dev/null +++ b/Tests/Integration/DefaultLoRaDataRequestHandlerExceptionTests.cs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Integration +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using LoRaTools.ADR; + using LoRaWan.NetworkServer; + using LoRaWan.NetworkServer.ADR; + using LoRaWan.Tests.Common; + using Microsoft.Extensions.Logging; + using Moq; + using Xunit; + using Xunit.Abstractions; + + public sealed class DefaultLoRaDataRequestHandlerExceptionTests : MessageProcessorTestBase + { + private readonly Mock mockTestDefaultLoRaRequestHandler; + private readonly TestOutputLoggerFactory testOutputLoggerFactory; + private readonly Mock> loggerMock = new(); + + private TestDefaultLoRaRequestHandler Subject => this.mockTestDefaultLoRaRequestHandler.Object; + + public DefaultLoRaDataRequestHandlerExceptionTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + this.testOutputLoggerFactory = new TestOutputLoggerFactory(testOutputHelper); + this.mockTestDefaultLoRaRequestHandler = new Mock(MockBehavior.Default, + ServerConfiguration, + FrameCounterUpdateStrategyProvider, + ConcentratorDeduplication, + PayloadDecoder, + new DeduplicationStrategyFactory(this.testOutputLoggerFactory, this.testOutputLoggerFactory.CreateLogger()), + new LoRaADRStrategyProvider(this.testOutputLoggerFactory), + new LoRAADRManagerFactory(LoRaDeviceApi.Object, this.testOutputLoggerFactory), + new FunctionBundlerProvider(LoRaDeviceApi.Object, this.testOutputLoggerFactory, this.testOutputLoggerFactory.CreateLogger()), + loggerMock.Object) + { CallBase = true }; + LoRaDeviceApi.Setup(api => api.ABPFcntCacheResetAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + LoRaDeviceApi.Setup(api => api.NextFCntDownAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(1U); + } + + private IEnumerable LoggedExceptions => + from e in this.loggerMock.GetLogInvocations() + where e.Exception is not null + select e.Exception; + + public static TheoryData Secondary_Task_Exceptions_TheoryData() => + TheoryDataFactory.From(new (Exception?, Exception?, Exception)[] + { + (new OperationCanceledException("Secondary task canceled"), null, new OperationCanceledException("Secondary task canceled")), + (null, new LoRaProcessingException(), new LoRaProcessingException()), + (new InvalidOperationException("A"), new LoRaProcessingException("B"), new AggregateException(new Exception[] { new InvalidOperationException("A"), new LoRaProcessingException("B") })), + }); + + [Theory] + [MemberData(nameof(Secondary_Task_Exceptions_TheoryData))] + public async Task Logs_Secondary_Task_Exceptions_Even_If_Main_Processing_Fails(Exception? cloudToDeviceException, Exception? saveChangesException, Exception expected) + { + // arrange + var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(0)); + using var request = CreateWaitableRequest(simulatedDevice.CreateConfirmedDataUpMessage("foo")); + SetupMainProcessingFailure(new InvalidOperationException()); + + if (cloudToDeviceException is { } someCloudToDeviceException) + SetupCloudToDeviceFailure(someCloudToDeviceException); + + if (saveChangesException is { } someSaveChangesException) + SetupSaveDeviceChangeFailure(someSaveChangesException); + + // act + assert + _ = await Assert.ThrowsAsync(() => Subject.ProcessRequestAsync(request, CreateLoRaDevice(simulatedDevice))); + AssertSecondaryTaskException(expected); + } + + [Theory] + [MemberData(nameof(Secondary_Task_Exceptions_TheoryData))] + public async Task Logs_Secondary_Task_Exceptions(Exception? cloudToDeviceException, Exception? saveChangesException, Exception expected) + { + // arrange + var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(0)); + using var request = CreateWaitableRequest(simulatedDevice.CreateConfirmedDataUpMessage("foo")); + + if (cloudToDeviceException is { } someCloudToDeviceException) + SetupCloudToDeviceFailure(someCloudToDeviceException); + + if (saveChangesException is { } someSaveChangesException) + SetupSaveDeviceChangeFailure(someSaveChangesException); + + // act + _ = await Subject.ProcessRequestAsync(request, CreateLoRaDevice(simulatedDevice)); + + // assert + AssertSecondaryTaskException(expected); + } + + private void AssertSecondaryTaskException(Exception expected) + { + var ex = Assert.Single(LoggedExceptions); + if (ex is AggregateException someAggregateException) + { + var expectedAggregateException = Assert.IsType(expected); + Assert.Equal(expectedAggregateException.InnerExceptions.Count, someAggregateException.InnerExceptions.Count); + foreach (var (expectedInner, actualInner) in expectedAggregateException.InnerExceptions.Zip(someAggregateException.InnerExceptions)) + { + Assert.Equal(expectedInner.GetType(), actualInner.GetType()); + Assert.Equal(expectedInner.Message, actualInner.Message); + } + } + else + { + Assert.Equal(expected.Message, ex!.Message); + } + } + + private void SetupMainProcessingFailure(Exception exception) + { + this.mockTestDefaultLoRaRequestHandler.Setup(c => c.DownlinkMessageBuilderResponse(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Throws(exception); + } + + private void SetupCloudToDeviceFailure(Exception exception) + { + var receivedCloudToDeviceMessage = new Mock(); + receivedCloudToDeviceMessage.Setup(m => m.RejectAsync()).ThrowsAsync(exception); + this.mockTestDefaultLoRaRequestHandler.SetupSequence(c => c.ReceiveCloudToDeviceAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(receivedCloudToDeviceMessage.Object) + .ReturnsAsync((IReceivedLoRaCloudToDeviceMessage?)null); + } + + private void SetupSaveDeviceChangeFailure(Exception exception) + { + this.mockTestDefaultLoRaRequestHandler.Setup(h => h.SaveChangesToDeviceAsync(It.IsAny(), false)) + .ThrowsAsync(exception); + } + + protected override async ValueTask DisposeAsync(bool disposing) + { + if (disposing) + { + this.testOutputLoggerFactory.Dispose(); + } + + await base.DisposeAsync(disposing); + } + } +} diff --git a/Tests/Integration/DevAddrCacheTest.cs b/Tests/Integration/DevAddrCacheTest.cs index 911352c769..4cb43e49bd 100644 --- a/Tests/Integration/DevAddrCacheTest.cs +++ b/Tests/Integration/DevAddrCacheTest.cs @@ -8,18 +8,24 @@ namespace LoRaWan.Tests.Integration using System.Linq; using System.Security.Cryptography; using System.Text; + using System.Threading; using System.Threading.Tasks; using LoraKeysManagerFacade; + using LoRaTools; + using LoRaTools.IoTHubImpl; using LoRaWan.Tests.Common; - using Microsoft.Azure.Devices; using Microsoft.Azure.Devices.Shared; + using Microsoft.Extensions.Logging.Abstractions; using Moq; using Newtonsoft.Json; using StackExchange.Redis; using Xunit; + using Xunit.Abstractions; [Collection(RedisFixture.CollectionName)] +#pragma warning disable xUnit1033 // False positive: Test classes decorated with 'Xunit.IClassFixture' or 'Xunit.ICollectionFixture' should add a constructor argument of type TFixture public class DevAddrCacheTest : FunctionTestBase, IClassFixture +#pragma warning restore xUnit1033 // False positive: Test classes decorated with 'Xunit.IClassFixture' or 'Xunit.ICollectionFixture' should add a constructor argument of type TFixture { private const string FullUpdateKey = "fullUpdateKey"; private const string GlobalDevAddrUpdateKey = "globalUpdateKey"; @@ -28,36 +34,38 @@ public class DevAddrCacheTest : FunctionTestBase, IClassFixture private const string PrimaryKey = "ABCDEFGH1234567890"; private readonly ILoRaDeviceCacheStore cache; + private readonly ITestOutputHelper testOutputHelper; - public DevAddrCacheTest(RedisFixture redis) + public DevAddrCacheTest(RedisFixture redis, ITestOutputHelper testOutputHelper) { if (redis is null) throw new ArgumentNullException(nameof(redis)); this.cache = new LoRaDeviceCacheRedisStore(redis.Database); + this.testOutputHelper = testOutputHelper; } - private static Mock InitRegistryManager(List deviceIds, int numberOfDeviceDeltaUpdates = 2) + private static Mock InitRegistryManager(List deviceIds) { var currentDevAddrContext = new List(); var currentDevices = deviceIds; - var mockRegistryManager = new Mock(MockBehavior.Strict); + var mockRegistryManager = new Mock(MockBehavior.Strict); var hasMoreShouldReturn = true; var primaryKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(PrimaryKey)); mockRegistryManager - .Setup(x => x.GetDeviceAsync(It.IsAny())) - .ReturnsAsync((string deviceId) => new Device(deviceId) { Authentication = new AuthenticationMechanism() { SymmetricKey = new SymmetricKey() { PrimaryKey = primaryKey } } }); + .Setup(x => x.GetDevicePrimaryKeyAsync(It.IsAny())) + .ReturnsAsync((string _) => primaryKey); mockRegistryManager - .Setup(x => x.GetTwinAsync(It.IsNotNull())) - .ReturnsAsync((string deviceId) => new Twin(deviceId)); + .Setup(x => x.GetTwinAsync(It.IsNotNull(), It.IsAny())) + .ReturnsAsync((string deviceId, CancellationToken _) => new IoTHubLoRaDeviceTwin(new Twin(deviceId))); var numberOfDevices = deviceIds.Count; - // CacheMiss query - var cacheMissQueryMock = new Mock(MockBehavior.Strict); + // mock Page Result + var mockPageResult = new Mock>(); // we only want to run hasmoreresult once - cacheMissQueryMock + mockPageResult .Setup(x => x.HasMoreResults) .Returns(() => { @@ -70,12 +78,12 @@ private static Mock InitRegistryManager(List return false; }); - cacheMissQueryMock - .Setup(x => x.GetNextAsTwinAsync()) + mockPageResult + .Setup(x => x.GetNextPageAsync()) .ReturnsAsync(() => { var devAddressesToConsider = currentDevAddrContext; - var twins = new List(); + var twins = new List(); foreach (var devaddrItem in devAddressesToConsider) { var deviceTwin = new Twin @@ -83,42 +91,43 @@ private static Mock InitRegistryManager(List DeviceId = devaddrItem.DevEUI.Value.ToString(), Properties = new TwinProperties() { - Desired = new TwinCollection($"{{\"{LoraKeysManagerFacadeConstants.TwinProperty_DevAddr}\": \"{devaddrItem.DevAddr}\", \"{LoraKeysManagerFacadeConstants.TwinProperty_GatewayID}\": \"{devaddrItem.GatewayId}\"}}", $"{{\"$lastUpdated\": \"{devaddrItem.LastUpdatedTwins.ToString(LoraKeysManagerFacadeConstants.RoundTripDateTimeStringFormat)}\"}}"), + Desired = new TwinCollection($"{{\"{TwinPropertiesConstants.DevAddr}\": \"{devaddrItem.DevAddr}\", \"{TwinPropertiesConstants.GatewayID}\": \"{devaddrItem.GatewayId}\"}}", $"{{\"$lastUpdated\": \"{devaddrItem.LastUpdatedTwins.ToString(Constants.RoundTripDateTimeStringFormat)}\"}}"), + Reported = new TwinCollection($"{{}}", $"{{\"$lastUpdated\": \"0001-01-01T00:00:00Z\"}}"), } }; - twins.Add(deviceTwin); + twins.Add(new IoTHubLoRaDeviceTwin(deviceTwin)); } return twins; }); mockRegistryManager - .Setup(x => x.CreateQuery(It.Is(z => z.Contains("SELECT * FROM devices WHERE properties.desired.DevAddr =", StringComparison.Ordinal)), 100)) - .Returns((string query, int pageSize) => + .Setup(x => x.FindLoRaDeviceByDevAddr(It.IsAny())) + .Returns((DevAddr someDevAddr) => { hasMoreShouldReturn = true; - currentDevAddrContext = currentDevices.Where(v => v.DevAddr.ToString() == query.Split('\'')[1]).ToList(); - return cacheMissQueryMock.Object; + currentDevAddrContext = currentDevices.Where(v => v.DevAddr == someDevAddr).ToList(); + return mockPageResult.Object; }); mockRegistryManager - .Setup(x => x.CreateQuery(It.Is(z => z.Contains("SELECT * FROM devices WHERE is_defined(properties.desired.AppKey) ", StringComparison.Ordinal)))) - .Returns((string query) => + .Setup(x => x.GetAllLoRaDevices()) + .Returns(() => { hasMoreShouldReturn = true; currentDevAddrContext = currentDevices; - return cacheMissQueryMock.Object; + return mockPageResult.Object; }); mockRegistryManager - .Setup(x => x.CreateQuery(It.Is(z => z.Contains("SELECT * FROM devices where properties.desired.$metadata.$lastUpdated >=", StringComparison.Ordinal)))) - .Returns((string query) => + .Setup(x => x.GetLastUpdatedLoRaDevices(It.IsAny())) + .Returns((DateTime lastDeltaUpdate) => { - currentDevAddrContext = currentDevices.Take(numberOfDeviceDeltaUpdates).ToList(); + currentDevAddrContext = currentDevices.Where(d => d.LastUpdatedTwins >= lastDeltaUpdate).ToList(); // reset device count in case HasMoreResult is called more than once hasMoreShouldReturn = true; - return cacheMissQueryMock.Object; + return mockPageResult.Object; }); return mockRegistryManager; } @@ -144,7 +153,8 @@ public async Task When_PerformNeededSyncs_Fails_Should_Release_Lock(string lockT await LockDevAddrHelper.PrepareLocksForTests(this.cache, lockToTake == null ? null : new[] { lockToTake }); var managerInput = new List { new DevAddrCacheInfo() { DevEUI = TestEui.GenerateDevEui(), DevAddr = CreateDevAddr() } }; var registryManagerMock = InitRegistryManager(managerInput); - registryManagerMock.Setup(x => x.CreateQuery(It.IsAny())).Throws(new RedisException(string.Empty)); + registryManagerMock.Setup(x => x.GetLastUpdatedLoRaDevices(It.IsAny())).Throws(new RedisException(string.Empty)); + registryManagerMock.Setup(x => x.GetAllLoRaDevices()).Throws(new RedisException(string.Empty)); await devAddrcache.PerformNeededSyncs(registryManagerMock.Object); // When doing a full update, the FullUpdateKey lock should be reset to 1min, the GlobalDevAddrUpdateKey should be gone @@ -192,7 +202,7 @@ public async Task When_DevAddr_Is_Not_In_Cache_Query_Iot_Hub_And_Save_In_Cache() var lockToTake = new string[2] { FullUpdateKey, GlobalDevAddrUpdateKey }; await LockDevAddrHelper.PrepareLocksForTests(this.cache, lockToTake); - var deviceGetter = new DeviceGetter(registryManagerMock.Object, this.cache); + var deviceGetter = SetupDeviceGetter(registryManagerMock.Object); items = await deviceGetter.GetDeviceList(null, gatewayId, new DevNonce(0xABCD), devAddrJoining); Assert.Single(items); @@ -205,9 +215,10 @@ public async Task When_DevAddr_Is_Not_In_Cache_Query_Iot_Hub_And_Save_In_Cache() Assert.Equal(managerInput[0].GatewayId ?? string.Empty, resultObject.GatewayId); Assert.Equal(managerInput[0].DevEUI, resultObject.DevEUI); - registryManagerMock.Verify(x => x.CreateQuery(It.IsAny(), It.IsAny()), Times.Once); - registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny()), Times.Never); - registryManagerMock.Verify(x => x.GetDeviceAsync(It.IsAny()), Times.Once); + registryManagerMock.Verify(x => x.FindLoRaDeviceByDevAddr(It.IsAny()), Times.Once); + registryManagerMock.Verify(x => x.GetAllLoRaDevices(), Times.Never); + registryManagerMock.Verify(x => x.GetLastUpdatedLoRaDevices(It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetDevicePrimaryKeyAsync(It.IsAny()), Times.Once); } [Fact] @@ -236,7 +247,7 @@ public async Task Multi_Gateway_When_DevAddr_Is_Not_In_Cache_Query_Iot_Hub_Only_ var lockToTake = new string[2] { FullUpdateKey, GlobalDevAddrUpdateKey }; await LockDevAddrHelper.PrepareLocksForTests(this.cache, lockToTake); - var deviceGetter = new DeviceGetter(registryManagerMock.Object, this.cache); + var deviceGetter = SetupDeviceGetter(registryManagerMock.Object); // Simulate three queries var tasks = from gw in new[] { gateway1, gateway2 } @@ -255,9 +266,10 @@ public async Task Multi_Gateway_When_DevAddr_Is_Not_In_Cache_Query_Iot_Hub_Only_ Assert.Equal(managerInput[0].GatewayId ?? string.Empty, resultObject.GatewayId); Assert.Equal(managerInput[0].DevEUI, resultObject.DevEUI); - registryManagerMock.Verify(x => x.CreateQuery(It.IsAny(), It.IsAny()), Times.Once); - registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny()), Times.Never); - registryManagerMock.Verify(x => x.GetDeviceAsync(It.IsAny()), Times.Once); + registryManagerMock.Verify(x => x.FindLoRaDeviceByDevAddr(It.IsAny()), Times.Once); + registryManagerMock.Verify(x => x.GetAllLoRaDevices(), Times.Never); + registryManagerMock.Verify(x => x.GetLastUpdatedLoRaDevices(It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetDevicePrimaryKeyAsync(It.IsAny()), Times.Once); } [Fact] @@ -288,7 +300,7 @@ public async Task When_DevAddr_Is_In_Cache_Without_Key_Should_Not_Query_Iot_Hub_ var lockToTake = new string[2] { FullUpdateKey, GlobalDevAddrUpdateKey }; await LockDevAddrHelper.PrepareLocksForTests(this.cache, lockToTake); - var deviceGetter = new DeviceGetter(registryManagerMock.Object, this.cache); + var deviceGetter = SetupDeviceGetter(registryManagerMock.Object); items = await deviceGetter.GetDeviceList(null, gatewayId, new DevNonce(0xABCD), devAddrJoining); Assert.Single(items); @@ -299,11 +311,11 @@ public async Task When_DevAddr_Is_In_Cache_Without_Key_Should_Not_Query_Iot_Hub_ Assert.NotNull(resultObject.PrimaryKey); // Iot hub should never have been called. - registryManagerMock.Verify(x => x.CreateQuery(It.IsAny()), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); - registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny()), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); + registryManagerMock.Verify(x => x.GetLastUpdatedLoRaDevices(It.IsAny()), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); + registryManagerMock.Verify(x => x.GetAllLoRaDevices(), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); + registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny(), It.IsAny()), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); // Should query for the key as key is missing - registryManagerMock.Verify(x => x.GetDeviceAsync(It.IsAny()), Times.Once); - registryManagerMock.Verify(x => x.CreateQuery(It.IsAny(), It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetDevicePrimaryKeyAsync(It.IsAny()), Times.Once); } [Fact] @@ -333,18 +345,18 @@ public async Task Multi_Gateway_When_DevAddr_Is_In_Cache_Without_Key_Should_Not_ var lockToTake = new string[2] { FullUpdateKey, GlobalDevAddrUpdateKey }; await LockDevAddrHelper.PrepareLocksForTests(this.cache, lockToTake); - var deviceGetter = new DeviceGetter(registryManagerMock.Object, this.cache); + var deviceGetter = SetupDeviceGetter(registryManagerMock.Object); var tasks = from gw in Enumerable.Repeat(gatewayId, 3) select deviceGetter.GetDeviceList(null, gw, new DevNonce(0xABCD), devAddrJoining); await Task.WhenAll(tasks); // Iot hub should never have been called. - registryManagerMock.Verify(x => x.CreateQuery(It.IsAny()), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); - registryManagerMock.Verify(x => x.CreateQuery(It.IsAny(), It.IsAny()), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); - registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny()), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); + registryManagerMock.Verify(x => x.GetLastUpdatedLoRaDevices(It.IsAny()), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); + registryManagerMock.Verify(x => x.GetAllLoRaDevices(), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); + registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny(), It.IsAny()), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); // Should query for the key as key is missing - registryManagerMock.Verify(x => x.GetDeviceAsync(It.IsAny()), Times.Once); + registryManagerMock.Verify(x => x.GetDevicePrimaryKeyAsync(It.IsAny()), Times.Once); var queryResult = this.cache.GetHashObject(string.Concat(CacheKeyPrefix, devAddrJoining)); Assert.Single(queryResult); // The key should have been saved @@ -382,15 +394,16 @@ public async Task When_DevAddr_Is_In_Cache_With_Key_Should_Not_Query_Iot_Hub_For var lockToTake = new string[2] { FullUpdateKey, GlobalDevAddrUpdateKey }; await LockDevAddrHelper.PrepareLocksForTests(this.cache, lockToTake); - var deviceGetter = new DeviceGetter(registryManagerMock.Object, this.cache); + var deviceGetter = SetupDeviceGetter(registryManagerMock.Object); items = await deviceGetter.GetDeviceList(null, gatewayId, new DevNonce(0xABCD), devAddrJoining); Assert.Single(items); // Iot hub should never have been called. - registryManagerMock.Verify(x => x.CreateQuery(It.IsAny()), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); - registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny()), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); + registryManagerMock.Verify(x => x.GetLastUpdatedLoRaDevices(It.IsAny()), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); + registryManagerMock.Verify(x => x.GetAllLoRaDevices(), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); + registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny(), It.IsAny()), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); // Should not query for the key as key is there - registryManagerMock.Verify(x => x.GetDeviceAsync(It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetDevicePrimaryKeyAsync(It.IsAny()), Times.Never); } [Fact] @@ -423,7 +436,7 @@ public async Task When_Device_Is_Not_Ours_Save_In_Cache_And_Dont_Query_Hub_Again InitCache(this.cache, managerInput); var registryManagerMock = InitRegistryManager(managerInput); - var deviceGetter = new DeviceGetter(registryManagerMock.Object, this.cache); + var deviceGetter = new DeviceGetter(registryManagerMock.Object, this.cache, NullLogger.Instance); items = await deviceGetter.GetDeviceList(null, gatewayId, new DevNonce(0xABCD), devAddrJoining); Assert.Empty(items); @@ -437,10 +450,11 @@ public async Task When_Device_Is_Not_Ours_Save_In_Cache_And_Dont_Query_Hub_Again Assert.Single(query2Result); // Iot hub should never have been called. - registryManagerMock.Verify(x => x.CreateQuery(It.IsAny()), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); - registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny()), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); + registryManagerMock.Verify(x => x.GetLastUpdatedLoRaDevices(It.IsAny()), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); + registryManagerMock.Verify(x => x.GetAllLoRaDevices(), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); + registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny(), It.IsAny()), Times.Never, "IoT Hub should not have been called, as the device was present in the cache."); // Should not query for the key as key is there - registryManagerMock.Verify(x => x.GetDeviceAsync(It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetDevicePrimaryKeyAsync(It.IsAny()), Times.Never); } [Fact] @@ -470,15 +484,15 @@ public async Task When_FullUpdateKey_Is_Not_there_Should_Perform_Full_Reload() var items = new List(); - var deviceGetter = new DeviceGetter(registryManagerMock.Object, this.cache); + var deviceGetter = SetupDeviceGetter(registryManagerMock.Object); items = await deviceGetter.GetDeviceList(null, gatewayId, new DevNonce(0xABCD), devAddrJoining); Assert.Single(items); - // Iot hub should never have been called. - registryManagerMock.Verify(x => x.CreateQuery(It.IsAny()), Times.Once); - registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetAllLoRaDevices(), Times.Once); + registryManagerMock.Verify(x => x.GetLastUpdatedLoRaDevices(It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny(), It.IsAny()), Times.Never); // We expect to query for the key once (the device with an active connection) - registryManagerMock.Verify(x => x.GetDeviceAsync(It.IsAny()), Times.Once); + registryManagerMock.Verify(x => x.GetDevicePrimaryKeyAsync(It.IsAny()), Times.Once); // we expect the devices are saved for (var i = 1; i < 5; i++) @@ -506,7 +520,7 @@ public async Task Delta_Update_Perform_Correctly_On_Empty_Cache() DevEUI = TestEui.GenerateDevEui(), DevAddr = CreateDevAddr(), GatewayId = gatewayId, - LastUpdatedTwins = dateTime + LastUpdatedTwins = dateTime.AddMinutes((float)-i * 40) // on empty cache, only updates from last hour are processed, therefore out of 5 device only 2 will be added with this computation }); } @@ -544,11 +558,12 @@ public async Task Delta_Update_Perform_Correctly_On_Empty_Cache() // Only two items should be updated by the delta updates Assert.Equal(2, foundItem); + registryManagerMock.Verify(x => x.GetAllLoRaDevices(), Times.Never); + registryManagerMock.Verify(x => x.GetLastUpdatedLoRaDevices(It.IsAny()), Times.Once); // Iot hub should never have been called. - registryManagerMock.Verify(x => x.CreateQuery(It.IsAny()), Times.Once); - registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny(), It.IsAny()), Times.Never); // We expect to query for the key once (the device with an active connection) - registryManagerMock.Verify(x => x.GetDeviceAsync(It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetDevicePrimaryKeyAsync(It.IsAny()), Times.Never); } [Fact] @@ -562,7 +577,6 @@ public async Task Delta_Update_Perform_Correctly_On_Non_Empty_Cache_And_Keep_Old var oldGatewayId = NewUniqueEUI64(); var newGatewayId = NewUniqueEUI64(); var dateTime = DateTime.UtcNow; - var primaryKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(PrimaryKey)); var managerInput = new List(); @@ -588,7 +602,7 @@ public async Task Delta_Update_Perform_Correctly_On_Non_Empty_Cache_And_Keep_Old var devAddrJoining = managerInput[0].DevAddr; // The cache start as empty - var registryManagerMock = InitRegistryManager(managerInput, managerInput.Count); + var registryManagerMock = InitRegistryManager(managerInput); // Set up the cache with expectation. var cacheInput = new List(); @@ -656,12 +670,13 @@ public async Task Delta_Update_Perform_Correctly_On_Non_Empty_Cache_And_Keep_Old } } + registryManagerMock.Verify(x => x.GetAllLoRaDevices(), Times.Never); + registryManagerMock.Verify(x => x.GetLastUpdatedLoRaDevices(It.IsAny()), Times.Once); + // Iot hub should never have been called. - registryManagerMock.Verify(x => x.CreateQuery(It.IsAny()), Times.Once); - registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny(), It.IsAny()), Times.Never); // We expect to query for the key once (the device with an active connection) - registryManagerMock.Verify(x => x.GetDeviceAsync(It.IsAny()), Times.Never); - registryManagerMock.Verify(x => x.CreateQuery(It.IsAny(), It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetDevicePrimaryKeyAsync(It.IsAny()), Times.Never); } [Fact] @@ -674,8 +689,8 @@ public async Task Delta_Update_Perform_Correctly_On_Non_Empty_Cache_And_Keep_Old { var oldGatewayId = NewUniqueEUI64(); var newGatewayId = NewUniqueEUI64(); - var dateTime = DateTime.UtcNow; - var updateDateTime = DateTime.UtcNow.AddMinutes(10); + var dateTime = DateTime.UtcNow.AddMinutes(-10); + var updateDateTime = DateTime.UtcNow; var primaryKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(PrimaryKey)); var managerInput = new List(); @@ -691,7 +706,7 @@ public async Task Delta_Update_Perform_Correctly_On_Non_Empty_Cache_And_Keep_Old }); } - var registryManagerMock = InitRegistryManager(managerInput, managerInput.Count); + var registryManagerMock = InitRegistryManager(managerInput); // Set up the cache with expectation. var cacheInput = new List(); @@ -702,7 +717,8 @@ public async Task Delta_Update_Perform_Correctly_On_Non_Empty_Cache_And_Keep_Old DevEUI = managerInput[i].DevEUI, DevAddr = managerInput[i].DevAddr, LastUpdatedTwins = dateTime, - PrimaryKey = primaryKey + PrimaryKey = primaryKey, + GatewayId = oldGatewayId }); } @@ -726,11 +742,13 @@ public async Task Delta_Update_Perform_Correctly_On_Non_Empty_Cache_And_Keep_Old Assert.Equal(string.Empty, resultObject.PrimaryKey); } + registryManagerMock.Verify(x => x.GetAllLoRaDevices(), Times.Never); + registryManagerMock.Verify(x => x.GetLastUpdatedLoRaDevices(It.IsAny()), Times.Once); + // Iot hub should never have been called. - registryManagerMock.Verify(x => x.CreateQuery(It.IsAny()), Times.Once); - registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny(), It.IsAny()), Times.Never); // We expect to query for the key once (the device with an active connection) - registryManagerMock.Verify(x => x.GetDeviceAsync(It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetDevicePrimaryKeyAsync(It.IsAny()), Times.Never); } [Fact] @@ -771,7 +789,7 @@ public async Task Full_Update_Perform_Correctly_On_Non_Empty_Cache_And_Keep_Old_ var devAddrJoining = newValues[0].DevAddr; // The cache start as empty - var registryManagerMock = InitRegistryManager(newValues, newValues.Count); + var registryManagerMock = InitRegistryManager(newValues); // Set up the cache with expectation. var cacheInput = new List(); @@ -823,11 +841,13 @@ public async Task Full_Update_Perform_Correctly_On_Non_Empty_Cache_And_Keep_Old_ } } + registryManagerMock.Verify(x => x.GetLastUpdatedLoRaDevices(It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetAllLoRaDevices(), Times.Once); + // Iot hub should never have been called. - registryManagerMock.Verify(x => x.CreateQuery(It.IsAny()), Times.Once); - registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny(), It.IsAny()), Times.Never); // We expect to query for the key once (the device with an active connection) - registryManagerMock.Verify(x => x.GetDeviceAsync(It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetDevicePrimaryKeyAsync(It.IsAny()), Times.Never); } [Fact] @@ -859,7 +879,7 @@ public async Task Full_Update_Perform_Correctly_On_Non_Empty_Cache_And_Keep_Old_ } // The cache start as empty - var registryManagerMock = InitRegistryManager(newValues, newValues.Count); + var registryManagerMock = InitRegistryManager(newValues); // Set up the cache with expectation. var cacheInput = new List(); @@ -894,13 +914,18 @@ public async Task Full_Update_Perform_Correctly_On_Non_Empty_Cache_And_Keep_Old_ Assert.Equal(string.Empty, result2Object.PrimaryKey); } + registryManagerMock.Verify(x => x.GetLastUpdatedLoRaDevices(It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetAllLoRaDevices(), Times.Once); + // Iot hub should never have been called. - registryManagerMock.Verify(x => x.CreateQuery(It.IsAny()), Times.Once); - registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetTwinAsync(It.IsAny(), It.IsAny()), Times.Never); // We expect to query for the key once (the device with an active connection) - registryManagerMock.Verify(x => x.GetDeviceAsync(It.IsAny()), Times.Never); + registryManagerMock.Verify(x => x.GetDevicePrimaryKeyAsync(It.IsAny()), Times.Never); } private static DevAddr CreateDevAddr() => new DevAddr((uint)RandomNumberGenerator.GetInt32(int.MaxValue)); + + private DeviceGetter SetupDeviceGetter(IDeviceRegistryManager registryManager) => + new DeviceGetter(registryManager, this.cache, new TestOutputLogger(this.testOutputHelper)); } } diff --git a/Tests/Integration/DwellTimeIntegrationTests.cs b/Tests/Integration/DwellTimeIntegrationTests.cs index 9e15a96020..aa79b3666c 100644 --- a/Tests/Integration/DwellTimeIntegrationTests.cs +++ b/Tests/Integration/DwellTimeIntegrationTests.cs @@ -227,15 +227,15 @@ private WaitableLoRaRequest SetupRequest(Region region, DwellTimeSetting? report return result; } - protected override void Dispose(bool disposing) + protected override async ValueTask DisposeAsync(bool disposing) { if (disposing) { - this.loRaDevice.Dispose(); + await this.loRaDevice.DisposeAsync(); this.testOutputLoggerFactory.Dispose(); } - base.Dispose(disposing); + await base.DisposeAsync(disposing); } } } diff --git a/Tests/Integration/FunctionBundlerIntegrationTests.cs b/Tests/Integration/FunctionBundlerIntegrationTests.cs index d41902c1d7..60045d403e 100644 --- a/Tests/Integration/FunctionBundlerIntegrationTests.cs +++ b/Tests/Integration/FunctionBundlerIntegrationTests.cs @@ -50,10 +50,11 @@ public async Task Validate_Function_Bundler_Execution() .ReturnsAsync((Message)null); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loRaDevice); - using var loRaDeviceRegistry1 = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loRaDevice); + await using var loRaDeviceRegistry1 = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); - using var messageProcessor1 = new MessageDispatcher( + await using var messageProcessor1 = TestMessageDispatcher.Create( + cache, ServerConfiguration, loRaDeviceRegistry1, FrameCounterUpdateStrategyProvider); diff --git a/Tests/Integration/JoinRequestMessageHandlerIntegrationTests.cs b/Tests/Integration/JoinRequestMessageHandlerIntegrationTests.cs new file mode 100644 index 0000000000..c00a06a749 --- /dev/null +++ b/Tests/Integration/JoinRequestMessageHandlerIntegrationTests.cs @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Integration +{ + using System.Threading; + using System.Threading.Tasks; + using Common; + using LoRaTools.CommonAPI; + using LoRaTools.Regions; + using LoRaWan.NetworkServer; + using Microsoft.Azure.Devices.Shared; + using Microsoft.Extensions.Caching.Memory; + using Microsoft.Extensions.Logging; + using Moq; + using Xunit; + using Xunit.Abstractions; + + public sealed class JoinRequestMessageHandlerIntegrationTests : MessageProcessorTestBase + { + private readonly MemoryCache cache; + private readonly Mock apiServiceMock; + private readonly JoinRequestMessageHandler joinRequestHandler; + private readonly SimulatedDevice simulatedDevice; + private readonly Mock deviceMock; + private readonly TestOutputLoggerFactory testOutputLoggerFactory; + private readonly Mock deviceRegistryMock; + private readonly Mock clientMock; + private bool disposedValue; + + public JoinRequestMessageHandlerIntegrationTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + this.simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateOTAADevice(0)); + this.deviceMock = new Mock(MockBehavior.Default, + this.simulatedDevice.DevAddr, + this.simulatedDevice.DevEUI, + ConnectionManager) + { + CallBase = true + }; + this.deviceMock.Object.AppKey = this.simulatedDevice.AppKey; + this.deviceMock.Object.AppEui = this.simulatedDevice.AppEui; + this.deviceMock.Object.IsOurDevice = true; + this.testOutputLoggerFactory = new TestOutputLoggerFactory(testOutputHelper); + + this.cache = new MemoryCache(new MemoryCacheOptions()); + var concentratorDeduplication = new ConcentratorDeduplication(this.cache, this.testOutputLoggerFactory.CreateLogger()); + this.deviceRegistryMock = new Mock(); + _ = this.deviceRegistryMock.Setup(x => x.GetDeviceForJoinRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(this.deviceMock.Object); + + this.clientMock = new Mock(); + _ = this.clientMock.Setup(x => x.UpdateReportedPropertiesAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); + ConnectionManager.Register(this.deviceMock.Object, this.clientMock.Object); + + this.apiServiceMock = new Mock(); + + this.joinRequestHandler = new JoinRequestMessageHandler( + ServerConfiguration, + concentratorDeduplication, + this.deviceRegistryMock.Object, + this.testOutputLoggerFactory.CreateLogger(), + this.apiServiceMock.Object, + null); + } + + [Fact] + public async Task When_Same_Join_Request_Received_Multiple_Times_Succeeds_Only_Once() + { + var joinRequest = this.simulatedDevice.CreateJoinRequest(); + var loraRequest = CreateWaitableRequest(joinRequest); + loraRequest.SetPayload(joinRequest); + loraRequest.SetRegion(new RegionEU868()); + + // first request + await this.joinRequestHandler.ProcessJoinRequestAsync(loraRequest); + + // repeat + await this.joinRequestHandler.ProcessJoinRequestAsync(loraRequest); + + // assert + this.deviceMock.Verify(x => x.UpdateAfterJoinAsync(It.IsAny(), It.IsAny()), Times.Once()); + this.deviceRegistryMock.Verify(x => x.UpdateDeviceAfterJoin(It.IsAny(), null), Times.Once()); + this.apiServiceMock.Verify(x => x.SendJoinNotificationAsync(It.IsAny(), It.IsAny()), Times.Once()); + + // do another request + joinRequest = this.simulatedDevice.CreateJoinRequest(); + loraRequest.SetPayload(joinRequest); + await this.joinRequestHandler.ProcessJoinRequestAsync(loraRequest); + this.deviceMock.Verify(x => x.UpdateAfterJoinAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + this.deviceRegistryMock.Verify(x => x.UpdateDeviceAfterJoin(It.IsAny(), It.IsNotNull()), Times.Once()); + this.apiServiceMock.Verify(x => x.SendJoinNotificationAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Ensures_Connection_Closed_After_Every_Join(bool joinHandledByAnotherGateway) + { + // arrange + var joinRequest = this.simulatedDevice.CreateJoinRequest(); + var loraRequest = CreateWaitableRequest(joinRequest); + loraRequest.SetPayload(joinRequest); + loraRequest.SetRegion(new RegionEU868()); + + if (joinHandledByAnotherGateway) + this.deviceMock.Object.DevNonce = joinRequest.DevNonce; + + // act + await this.joinRequestHandler.ProcessJoinRequestAsync(loraRequest); + + // assert + this.deviceMock.Verify(x => x.CloseConnectionAsync(CancellationToken.None, true), Times.Once); + if (!joinHandledByAnotherGateway) + { + this.deviceMock.Verify(x => x.UpdateAfterJoinAsync(It.IsAny(), It.IsAny()), Times.Once()); + this.deviceRegistryMock.Verify(x => x.UpdateDeviceAfterJoin(It.IsAny(), null), Times.Once()); + } + this.apiServiceMock.Verify(x => x.SendJoinNotificationAsync(It.IsAny(), It.IsAny()), joinHandledByAnotherGateway ? Times.Never() : Times.Once()); + + // act and assert again + joinRequest = this.simulatedDevice.CreateJoinRequest(); + loraRequest.SetPayload(joinRequest); + await this.joinRequestHandler.ProcessJoinRequestAsync(loraRequest); + this.deviceMock.Verify(x => x.CloseConnectionAsync(CancellationToken.None, true), Times.Exactly(2)); + this.apiServiceMock.Verify(x => x.SendJoinNotificationAsync(It.IsAny(), It.IsAny()), joinHandledByAnotherGateway ? Times.Once() : Times.Exactly(2)); + } + + [Fact] + public async Task Ensures_Registry_Update_Only_Invoked_When_Twin_Updated() + { + var joinRequest = this.simulatedDevice.CreateJoinRequest(); + var loraRequest = CreateWaitableRequest(joinRequest); + loraRequest.SetPayload(joinRequest); + loraRequest.SetRegion(new RegionEU868()); + + _ = this.clientMock.Setup(x => x.UpdateReportedPropertiesAsync(It.IsAny(), It.IsAny())).ReturnsAsync(false); + // first request will not be able to update twin + await this.joinRequestHandler.ProcessJoinRequestAsync(loraRequest); + + // assert + this.deviceMock.Verify(x => x.UpdateAfterJoinAsync(It.IsAny(), It.IsAny()), Times.Once()); + this.deviceRegistryMock.Verify(x => x.UpdateDeviceAfterJoin(It.IsAny(), It.IsAny()), Times.Never()); + this.apiServiceMock.Verify(x => x.SendJoinNotificationAsync(It.IsAny(), It.IsAny()), Times.Never()); + + // do another request, which will succeed and therefore deviceRegistry should be updated + _ = this.clientMock.Setup(x => x.UpdateReportedPropertiesAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); + joinRequest = this.simulatedDevice.CreateJoinRequest(); + loraRequest.SetPayload(joinRequest); + await this.joinRequestHandler.ProcessJoinRequestAsync(loraRequest); + this.deviceMock.Verify(x => x.UpdateAfterJoinAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + // asserting that a UpdateDeviceAfterJoin with a null "oldDevAddr" is received meaning that device was not in dev Addr cache + this.deviceRegistryMock.Verify(x => x.UpdateDeviceAfterJoin(It.IsAny(), null), Times.Once()); + this.apiServiceMock.Verify(x => x.SendJoinNotificationAsync(It.IsAny(), It.IsAny()), Times.Once()); + } + + protected override async ValueTask DisposeAsync(bool disposing) + { + if (!this.disposedValue) + { + if (disposing) + { + this.cache.Dispose(); + this.testOutputLoggerFactory.Dispose(); + } + + this.disposedValue = true; + } + + // Call base class implementation. + await base.DisposeAsync(disposing); + } + } +} diff --git a/Tests/Integration/JoinSlowGetTwinTests.cs b/Tests/Integration/JoinSlowGetTwinTests.cs index 4879d0caa2..e58c3ef3cc 100644 --- a/Tests/Integration/JoinSlowGetTwinTests.cs +++ b/Tests/Integration/JoinSlowGetTwinTests.cs @@ -32,12 +32,7 @@ public async Task When_Join_Fails_Due_To_Timeout_Second_Try_Should_Reuse_Cached_ var devEui = simulatedDevice.LoRaDevice.DevEui; // Device twin will be queried twice, 1st time will take 7 seconds, 2nd time 0.1 second - var twin = new Twin(); - twin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - twin.Properties.Desired[TwinProperty.AppEui] = simulatedDevice.LoRaDevice.AppEui?.ToString(); - twin.Properties.Desired[TwinProperty.AppKey] = simulatedDevice.LoRaDevice.AppKey?.ToString(); - if (deviceGatewayID != null) twin.Properties.Desired[TwinProperty.GatewayID] = deviceGatewayID; - twin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; + var twin = LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetOtaaDesiredTwinProperties()); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); @@ -59,9 +54,10 @@ public async Task When_Join_Fails_Due_To_Timeout_Second_Try_Should_Reuse_Cached_ .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEui, "aabb").AsList())); using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); diff --git a/Tests/Integration/JoinSlowTwinUpdateTests.cs b/Tests/Integration/JoinSlowTwinUpdateTests.cs index ce2102bd5e..a4350f2165 100644 --- a/Tests/Integration/JoinSlowTwinUpdateTests.cs +++ b/Tests/Integration/JoinSlowTwinUpdateTests.cs @@ -35,12 +35,7 @@ public async Task When_First_Join_Fails_Due_To_Slow_Twin_Update_Retry_Second_Att var devEui = simulatedDevice.LoRaDevice.DevEui; // Device twin will be queried - var twin = new Twin(); - twin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - twin.Properties.Desired[TwinProperty.AppEui] = simulatedDevice.LoRaDevice.AppEui?.ToString(); - twin.Properties.Desired[TwinProperty.AppKey] = simulatedDevice.LoRaDevice.AppKey?.ToString(); - twin.Properties.Desired[TwinProperty.GatewayID] = deviceGatewayID; - twin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; + var twin = LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetOtaaDesiredTwinProperties()); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); @@ -80,9 +75,10 @@ public async Task When_First_Join_Fails_Due_To_Slow_Twin_Update_Retry_Second_Att .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEui, "aabb").AsList())); using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); diff --git a/Tests/Integration/JoinTests.cs b/Tests/Integration/JoinTests.cs index c0025639b2..2a633d384e 100644 --- a/Tests/Integration/JoinTests.cs +++ b/Tests/Integration/JoinTests.cs @@ -64,14 +64,13 @@ private async Task Join_With_Subsequent_Unconfirmed_And_Confirmed_Messages(strin ServerConfiguration.NetId = new NetId(netId); // Device twin will be queried - var twin = new Twin(); - twin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - twin.Properties.Desired[TwinProperty.AppEui] = simulatedDevice.LoRaDevice.AppEui?.ToString(); - twin.Properties.Desired[TwinProperty.AppKey] = simulatedDevice.LoRaDevice.AppKey?.ToString(); - if (deviceGatewayID != null) twin.Properties.Desired[TwinProperty.GatewayID] = deviceGatewayID; - twin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; - twin.Properties.Reported[TwinProperty.FCntUp] = initialFcntUp; - twin.Properties.Reported[TwinProperty.FCntDown] = initialFcntDown; + var twin = LoRaDeviceTwin.Create( + simulatedDevice.LoRaDevice.GetOtaaDesiredTwinProperties(), + new LoRaReportedTwinProperties + { + FCntUp = initialFcntUp, + FCntDown = initialFcntDown + }); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(twin); // Device twin will be updated @@ -130,10 +129,11 @@ private async Task Join_With_Subsequent_Unconfirmed_And_Confirmed_Messages(strin // using factory to create mock of using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -186,7 +186,7 @@ private async Task Join_With_Subsequent_Unconfirmed_And_Confirmed_Messages(strin // fcnt up was updated Assert.Equal(startingPayloadFcnt, loRaDevice.FCntUp); - Assert.Equal(0U, loRaDevice.FCntDown); + Assert.Equal(string.IsNullOrEmpty(deviceGatewayID) ? 1U : 0U, loRaDevice.FCntDown); // If the starting payload was not 0, it is expected that it updates the framecounter char // The device will perform the frame counter update and at this point in time it will have the same frame counter as the desired @@ -259,12 +259,7 @@ public async Task When_Join_Fails_Due_To_GetTwin_Error_Second_Try_Should_Reload_ var devEui = simulatedDevice.LoRaDevice.DevEui; // Device twin will be queried - var twin = new Twin(); - twin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - twin.Properties.Desired[TwinProperty.AppEui] = simulatedDevice.LoRaDevice.AppEui?.ToString(); - twin.Properties.Desired[TwinProperty.AppKey] = simulatedDevice.LoRaDevice.AppKey?.ToString(); - if (deviceGatewayID != null) twin.Properties.Desired[TwinProperty.GatewayID] = deviceGatewayID; - twin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; + var twin = LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetOtaaDesiredTwinProperties()); LoRaDeviceClient.SetupSequence(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync((Twin)null) .ReturnsAsync(twin); @@ -288,9 +283,10 @@ public async Task When_Join_Fails_Due_To_GetTwin_Error_Second_Try_Should_Reload_ // using factory to create mock of using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -359,12 +355,10 @@ public async Task Join_Device_Has_Mismatching_AppEUI_Should_Return_Null(string d var devEui = simulatedDevice.LoRaDevice.DevEui; // Device twin will be queried - var twin = new Twin(); - twin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - twin.Properties.Desired[TwinProperty.AppEui] = "012345678901234567890123456789FF"; - twin.Properties.Desired[TwinProperty.AppKey] = simulatedDevice.LoRaDevice.AppKey; - twin.Properties.Desired[TwinProperty.GatewayID] = deviceGatewayID; - twin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; + var twin = LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetOtaaDesiredTwinProperties() with + { + JoinEui = new JoinEui(01213147654) + }); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(twin); // Lora device api will be search by devices with matching deveui, @@ -373,9 +367,10 @@ public async Task Join_Device_Has_Mismatching_AppEUI_Should_Return_Null(string d // using factory to create mock of using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -404,12 +399,10 @@ public async Task Join_Device_Has_Mismatching_AppKey_Should_Return_Null(string d var devEui = simulatedDevice.LoRaDevice.DevEui; // Device twin will be queried - var twin = new Twin(); - twin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - twin.Properties.Desired[TwinProperty.AppEui] = simulatedDevice.LoRaDevice.AppEui?.ToString(); - twin.Properties.Desired[TwinProperty.AppKey] = "012345678901234567890123456789FF"; - twin.Properties.Desired[TwinProperty.GatewayID] = deviceGatewayID; - twin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; + var twin = LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetOtaaDesiredTwinProperties() with + { + AppKey = TestKeys.CreateAppKey(121314765654), + }); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(twin); // Lora device api will be search by devices with matching deveui, @@ -418,9 +411,10 @@ public async Task Join_Device_Has_Mismatching_AppKey_Should_Return_Null(string d // using factory to create mock of using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -455,12 +449,7 @@ public async Task OTAA_Join_Should_Use_Rchf_0(string deviceGatewayID, uint rfch) var devEui = simulatedDevice.LoRaDevice.DevEui; // Device twin will be queried - var twin = new Twin(); - twin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - twin.Properties.Desired[TwinProperty.AppEui] = simulatedDevice.LoRaDevice.AppEui?.ToString(); - twin.Properties.Desired[TwinProperty.AppKey] = simulatedDevice.LoRaDevice.AppKey?.ToString(); - twin.Properties.Desired[TwinProperty.GatewayID] = deviceGatewayID; - twin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; + var twin = LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetOtaaDesiredTwinProperties()); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(twin); // Device twin will be updated @@ -474,10 +463,11 @@ public async Task OTAA_Join_Should_Use_Rchf_0(string deviceGatewayID, uint rfch) // using factory to create mock of using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -508,12 +498,7 @@ public async Task When_Multiple_Joins_Are_Received_Should_Get_Twins_Once(string var devEui = simulatedDevice.LoRaDevice.DevEui; // Device twin will be queried - var twin = new Twin(); - twin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - twin.Properties.Desired[TwinProperty.AppEui] = simulatedDevice.LoRaDevice.AppEui?.ToString(); - twin.Properties.Desired[TwinProperty.AppKey] = simulatedDevice.LoRaDevice.AppKey?.ToString(); - twin.Properties.Desired[TwinProperty.GatewayID] = deviceGatewayID; - twin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; + var twin = LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetOtaaDesiredTwinProperties()); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); @@ -527,9 +512,10 @@ public async Task When_Multiple_Joins_Are_Received_Should_Get_Twins_Once(string // using factory to create mock of using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); diff --git a/Tests/Integration/KeepAliveConnectionTests.cs b/Tests/Integration/KeepAliveConnectionTests.cs index 75b78799a3..1db068b244 100644 --- a/Tests/Integration/KeepAliveConnectionTests.cs +++ b/Tests/Integration/KeepAliveConnectionTests.cs @@ -81,18 +81,19 @@ public async Task After_ClassA_Sends_Data_Should_Disconnect() // will disconnected client using var disconnectedEvent = new SemaphoreSlim(0, 1); - LoRaDeviceClient.Setup(x => x.Disconnect()) + LoRaDeviceClient.Setup(x => x.DisconnectAsync(It.IsAny())) .Callback(() => disconnectedEvent.Release()) - .Returns(true); + .Returns(Task.CompletedTask); var cachedDevice = CreateLoRaDevice(simulatedDevice); cachedDevice.KeepAliveTimeout = 3; using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(cachedDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(cachedDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -136,22 +137,23 @@ public async Task After_ClassA_Sends_Multiple_Data_Should_Disconnect() // will disconnected client using var disconnectedEvent = new SemaphoreSlim(0, 1); - LoRaDeviceClient.Setup(x => x.Disconnect()) + LoRaDeviceClient.Setup(x => x.DisconnectAsync(It.IsAny())) .Callback(() => { disconnectedEvent.Release(); isDisconnected = true; }) - .Returns(true); + .Returns(Task.CompletedTask); var cachedDevice = CreateLoRaDevice(simulatedDevice); cachedDevice.KeepAliveTimeout = 3; using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(cachedDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(cachedDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -202,22 +204,23 @@ public async Task After_Disconnecting_Should_Reconnect() // will disconnected client using var disconnectedEvent = new SemaphoreSlim(0, 1); - LoRaDeviceClient.Setup(x => x.Disconnect()) + LoRaDeviceClient.Setup(x => x.DisconnectAsync(It.IsAny())) .Callback(() => { disconnectedEvent.Release(); isDisconnected = true; }) - .Returns(true); + .Returns(Task.CompletedTask); var cachedDevice = CreateLoRaDevice(simulatedDevice); cachedDevice.KeepAliveTimeout = 3; using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(cachedDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(cachedDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -229,6 +232,8 @@ public async Task After_Disconnecting_Should_Reconnect() Assert.True(request1.ProcessingSucceeded); await EnsureDisconnectedAsync(disconnectedEvent); + LoRaDeviceClient.Verify(x => x.DisconnectAsync(CancellationToken.None), Times.Exactly(1)); + LoRaDeviceClient.Verify(x => x.EnsureConnected(), Times.Exactly(2)); // sends unconfirmed message #2 using var request2 = CreateWaitableRequest(simulatedDevice.CreateUnconfirmedDataUpMessage("2")); @@ -238,9 +243,8 @@ public async Task After_Disconnecting_Should_Reconnect() await EnsureDisconnectedAsync(disconnectedEvent); - LoRaDeviceClient.Verify(x => x.Disconnect(), Times.Exactly(2)); - LoRaDeviceClient.Verify(x => x.EnsureConnected(), Times.Exactly(2)); - + LoRaDeviceClient.Verify(x => x.DisconnectAsync(CancellationToken.None), Times.Exactly(2)); + LoRaDeviceClient.Verify(x => x.EnsureConnected(), Times.Exactly(2 * /* send + receive */ 2)); LoRaDeviceClient.VerifyAll(); LoRaDeviceApi.VerifyAll(); } @@ -255,10 +259,8 @@ public async Task When_Device_Is_Loaded_Should_Disconnect_After_Sending_Data() .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(simulatedDevice.DevAddr, simulatedDevice.DevEUI, "ada").AsList())); // will read the device twins - var twin = simulatedDevice.CreateABPTwin(desiredProperties: new Dictionary - { - { TwinProperty.KeepAliveTimeout, 3 } - }); + var twin = LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetAbpDesiredTwinProperties() with { KeepAliveTimeout = TimeSpan.FromSeconds(3) }, + simulatedDevice.GetAbpReportedTwinProperties()); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); @@ -279,14 +281,15 @@ public async Task When_Device_Is_Loaded_Should_Disconnect_After_Sending_Data() // will disconnected client using var disconnectedEvent = new SemaphoreSlim(0, 1); - LoRaDeviceClient.Setup(x => x.Disconnect()) + LoRaDeviceClient.Setup(x => x.DisconnectAsync(It.IsAny())) .Callback(() => disconnectedEvent.Release()) - .Returns(true); + .Returns(Task.CompletedTask); using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -309,17 +312,17 @@ public async Task After_Sending_Class_C_Downstream_Should_Disconnect_Client() { var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, gatewayID: ServerConfiguration.GatewayID, - deviceClassType: 'c')); + deviceClassType: LoRaDeviceClassType.C)); var devEui = simulatedDevice.DevEUI; // will disconnected client using var disconnectedEvent = new SemaphoreSlim(0, 1); - LoRaDeviceClient.Setup(x => x.Disconnect()) + LoRaDeviceClient.Setup(x => x.DisconnectAsync(It.IsAny())) .Callback(() => { disconnectedEvent.Release(); }) - .Returns(true); + .Returns(Task.CompletedTask); // will check client connection LoRaDeviceClient.Setup(x => x.EnsureConnected()) @@ -345,8 +348,8 @@ public async Task After_Sending_Class_C_Downstream_Should_Disconnect_Client() cachedDevice.SetLastProcessingStationEui(new StationEui(ulong.MaxValue)); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(cachedDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(cachedDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); var target = new DefaultClassCDevicesMessageSender( ServerConfiguration, diff --git a/Tests/Integration/LnsDiscoveryIntegrationTests.cs b/Tests/Integration/LnsDiscoveryIntegrationTests.cs new file mode 100644 index 0000000000..378b5a5327 --- /dev/null +++ b/Tests/Integration/LnsDiscoveryIntegrationTests.cs @@ -0,0 +1,226 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Integration +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Net.WebSockets; + using System.Text; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + using LoRaTools; + using LoRaTools.IoTHubImpl; + using LoRaTools.NetworkServerDiscovery; + using LoRaWan.NetworkServerDiscovery; + using LoRaWan.Tests.Common; + using Microsoft.AspNetCore.Mvc.Testing; + using Microsoft.AspNetCore.TestHost; + using Microsoft.Azure.Devices; + using Microsoft.Azure.Devices.Shared; + using Microsoft.Extensions.Caching.Memory; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.DependencyInjection.Extensions; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + using Moq; + using Xunit; + + internal sealed class LnsDiscoveryApplication : WebApplicationFactory + { + public Mock? RegistryManagerMock { get; private set; } + + protected override IHost CreateHost(IHostBuilder builder) + { + builder.ConfigureServices(services => + { + RegistryManagerMock = new Mock(); + services.RemoveAll(); + services.AddSingleton(sp => new TagBasedLnsDiscovery(sp.GetRequiredService(), RegistryManagerMock.Object, sp.GetRequiredService>())); + }); + + builder.ConfigureLogging(hostBuilder => hostBuilder.ClearProviders()); + + return base.CreateHost(builder); + } + } + + public sealed class LnsDiscoveryIntegrationTests : IDisposable + { + private static readonly string[] HostAddresses = new[] { "ws://foo:5000", "wss://bar:5001" }; + private static readonly StationEui StationEui = new StationEui(1); + + private readonly LnsDiscoveryApplication subject; + + public LnsDiscoveryIntegrationTests() + { + this.subject = new LnsDiscoveryApplication(); + } + + [Fact] + public async Task RouterInfo_Returns_Lns() + { + // arrange + var cancellationToken = CancellationToken.None; + var client = this.subject.Server.CreateWebSocketClient(); + SetupIotHubResponse(StationEui, HostAddresses); + + // act + var result = await SendSingleMessageAsync(client, StationEui, cancellationToken); + + // assert + AssertContainsHostAddress(new Uri(HostAddresses[0]), StationEui, result); + } + + [Fact] + public async Task RouterInfo_Returns_Lns_Using_Round_Robin_On_Rerequest() + { + // arrange + var cancellationToken = CancellationToken.None; + var client = this.subject.Server.CreateWebSocketClient(); + SetupIotHubResponse(StationEui, HostAddresses); + + // act + assert + for (var i = 0; i < HostAddresses.Length; ++i) + { + var result = await SendSingleMessageAsync(client, StationEui, cancellationToken); + AssertContainsHostAddress(new Uri(HostAddresses[i % HostAddresses.Length]), StationEui, result); + } + } + + public static TheoryData Erroneous_Host_Address_TheoryData() => TheoryDataFactory.From(new[] + { + "", "http://mylns:5000", "htt://mylns:5000", "ws:/mylns:5000" + }); + + [Theory] + [MemberData(nameof(Erroneous_Host_Address_TheoryData))] + public async Task RouterInfo_Fails_If_All_Lns_Are_Misconfigured(string hostAddress) + { + // arrange + var cancellationToken = CancellationToken.None; + var client = this.subject.Server.CreateWebSocketClient(); + SetupIotHubResponse(StationEui, new[] { hostAddress }); + + // act + var result = await SendSingleMessageAsync(client, StationEui, cancellationToken); + + // assert + Assert.Contains("No LNS found in network", result, StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [MemberData(nameof(Erroneous_Host_Address_TheoryData))] + public async Task RouterInfo_Is_Resilient_Against_Misconfigured_Lns(string hostAddress) + { + // arrange + var cancellationToken = CancellationToken.None; + var client = this.subject.Server.CreateWebSocketClient(); + SetupIotHubResponse(StationEui, HostAddresses.Append(hostAddress).ToList()); + + // act + assert + for (var i = 0; i < HostAddresses.Length; ++i) + { + var result = await SendSingleMessageAsync(client, StationEui, cancellationToken); + AssertContainsHostAddress(new Uri(HostAddresses[i % HostAddresses.Length]), StationEui, result); + } + } + + [Fact] + public async Task RouterInfo_Returns_Same_Lns_For_Different_Stations() + { + // arrange + var cancellationToken = CancellationToken.None; + var firstStation = new StationEui(1); + var secondStation = new StationEui(2); + var client = this.subject.Server.CreateWebSocketClient(); + SetupIotHubResponse(firstStation, HostAddresses); + SetupIotHubResponse(secondStation, HostAddresses); + + // act + var firstResult = await SendSingleMessageAsync(client, firstStation, cancellationToken); + var secondResult = await SendSingleMessageAsync(client, secondStation, cancellationToken); + + // assert + AssertContainsHostAddress(new Uri(HostAddresses[0]), firstStation, firstResult); + AssertContainsHostAddress(new Uri(HostAddresses[0]), secondStation, secondResult); + } + + [Fact] + public async Task RouterInfo_Returns_400_If_Connection_Is_Not_WebSocket() + { + // arrange + var cancellationToken = CancellationToken.None; + var client = this.subject.CreateClient(); + + // act + var result = await client.GetAsync(new Uri("router-info", UriKind.Relative), cancellationToken); + + // assert + Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); + } + + private static void AssertContainsHostAddress(Uri hostAddress, StationEui stationEui, string actual) + { + Assert.Contains($"\"uri\":\"{hostAddress}router-data/{stationEui}\"", actual, StringComparison.OrdinalIgnoreCase); + } + + private async Task SendSingleMessageAsync(WebSocketClient client, StationEui stationEui, CancellationToken cancellationToken) + { + var webSocket = await client.ConnectAsync(new Uri(this.subject.Server.BaseAddress, "router-info"), cancellationToken); + await webSocket.SendAsync(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new { router = stationEui.AsUInt64 })), WebSocketMessageType.Text, endOfMessage: true, cancellationToken); + var e = webSocket.ReadTextMessages(cancellationToken); + var result = !await e.MoveNextAsync() ? throw new InvalidOperationException("No response received.") : e.Current; + + try + { + await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Normal closure", cancellationToken); + } + catch (IOException) + { + // Remote already closed the connection. + } + + return result; + } + + private void SetupIotHubResponse(StationEui stationEui, IList hostAddresses) + { + const string networkId = "foo"; + SetupLbsTwinResponse(stationEui, networkId); + SetupIotHubQueryResponse(networkId, hostAddresses); + } + + private void SetupLbsTwinResponse(StationEui stationEui, string networkId) + { + this.subject + .RegistryManagerMock? + .Setup(rm => rm.GetTwinAsync(stationEui.ToString(), It.IsAny())) + .ReturnsAsync(new IoTHubDeviceTwin(new Twin { Tags = new TwinCollection(@$"{{""network"":""{networkId}""}}") })); + } + + private void SetupIotHubQueryResponse(string networkId, IList hostAddresses) + { + var queryMock = new Mock>(); + var i = 0; + queryMock.Setup(q => q.HasMoreResults).Returns(() => i++ % 2 == 0); + queryMock.Setup(q => q.GetNextPageAsync()).ReturnsAsync(from ha in hostAddresses + select JsonSerializer.Serialize(new { hostAddress = ha, deviceId = Guid.NewGuid().ToString() })); + this.subject + .RegistryManagerMock? + .Setup(rm => rm.FindLnsByNetworkId(networkId)) + .Returns(queryMock.Object); + } + + public void Dispose() + { + this.subject.Dispose(); + } + } +} diff --git a/Tests/Integration/LoRaApiHttpClientTests.cs b/Tests/Integration/LoRaApiHttpClientTests.cs new file mode 100644 index 0000000000..3e77ee2d0e --- /dev/null +++ b/Tests/Integration/LoRaApiHttpClientTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Integration +{ + using System; + using System.Net; + using System.Net.Http; + using System.Threading.Tasks; + using LoRaWan.NetworkServer; + using LoRaWan.Tests.Common; + using Microsoft.Extensions.DependencyInjection; + using Xunit; + + public sealed class LoRaApiHttpClientTests + { + [Fact] + public async Task AddApiClient_HttpClient_Retries_Eight_Times() + { + // arrange + var count = 0; + using var handler = new HttpMessageHandlerMock(); + handler.SetupHandler(_ => + { + ++count; + return new HttpResponseMessage(HttpStatusCode.InternalServerError); + }); + var client = new ServiceCollection().AddApiClient(() => handler) + .BuildServiceProvider() + .GetRequiredService() + .CreateClient(LoRaApiHttpClient.Name); + using var request = new HttpRequestMessage(HttpMethod.Get, new Uri("https://inexistenturlfoobar.ms")); + + // act + assert + var response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Equal(9, count); + } + } +} diff --git a/Tests/Integration/LoRaDataRequestHandlerConnectionAffinityIntegrationTests.cs b/Tests/Integration/LoRaDataRequestHandlerConnectionAffinityIntegrationTests.cs new file mode 100644 index 0000000000..da303c085d --- /dev/null +++ b/Tests/Integration/LoRaDataRequestHandlerConnectionAffinityIntegrationTests.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Integration +{ + using System.Threading; + using System.Threading.Tasks; + using LoRaTools.ADR; + using LoRaWan.NetworkServer; + using LoRaWan.NetworkServer.ADR; + using LoRaWan.Tests.Common; + using Microsoft.Extensions.Caching.Memory; + using Microsoft.Extensions.Logging; + using Moq; + using Xunit; + using Xunit.Abstractions; + + public class LoRaDataRequestHandlerConnectionAffinityIntegrationTests : MessageProcessorTestBase + { + private readonly MemoryCache cache; + private readonly TestOutputLoggerFactory testOutputLoggerFactory; + private readonly Mock frameCounterStrategyMock; + private readonly Mock frameCounterProviderMock; + private readonly Mock dataRequestHandlerMock; + private readonly SimulatedDevice simulatedABPDevice; + private readonly Mock deviceMock; + private readonly WaitableLoRaRequest loraRequest; + + public LoRaDataRequestHandlerConnectionAffinityIntegrationTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + this.cache = new MemoryCache(new MemoryCacheOptions()); + this.testOutputLoggerFactory = new TestOutputLoggerFactory(testOutputHelper); + var concentratorDeduplication = new ConcentratorDeduplication(this.cache, this.testOutputLoggerFactory.CreateLogger()); + + this.frameCounterStrategyMock = new Mock(); + this.frameCounterProviderMock = new Mock(); + + this.dataRequestHandlerMock = new Mock(MockBehavior.Default, + ServerConfiguration, + this.frameCounterProviderMock.Object, + concentratorDeduplication, + PayloadDecoder, + new DeduplicationStrategyFactory(this.testOutputLoggerFactory, this.testOutputLoggerFactory.CreateLogger()), + new LoRaADRStrategyProvider(this.testOutputLoggerFactory), + new LoRAADRManagerFactory(LoRaDeviceApi.Object, this.testOutputLoggerFactory), + new FunctionBundlerProvider(LoRaDeviceApi.Object, this.testOutputLoggerFactory, this.testOutputLoggerFactory.CreateLogger()), + testOutputHelper) + { + CallBase = true + }; + + this.simulatedABPDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(0)); + this.deviceMock = new Mock( + MockBehavior.Default, + this.simulatedABPDevice.DevAddr, + this.simulatedABPDevice.DevEUI, + ConnectionManager); + + _ = this.frameCounterStrategyMock.Setup(x => x.NextFcntDown(this.deviceMock.Object, It.IsAny())).Returns(() => ValueTask.FromResult(1)); + _ = this.frameCounterProviderMock.Setup(x => x.GetStrategy(this.deviceMock.Object.GatewayID)).Returns(this.frameCounterStrategyMock.Object); + + var message = this.simulatedABPDevice.CreateUnconfirmedDataUpMessage("payload", fcnt: 1); + this.loraRequest = CreateWaitableRequest(message); + } + + [Theory] + [InlineData(true, true)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(null, true)] + public async Task When_Gateway_Does_Not_Own_Connection_It_Should_Delay_If_Delay_Enabled(bool? connectionOwner, bool processingDelayEnabled) + { + // arrange + this.deviceMock.Object.IsConnectionOwner = connectionOwner; + this.dataRequestHandlerMock.Setup(x => x.IsProcessingDelayEnabled()).Returns(processingDelayEnabled); + + // act + _ = await this.dataRequestHandlerMock.Object.ProcessRequestAsync(this.loraRequest, this.deviceMock.Object); + + // assert + var shouldDelay = connectionOwner is false && processingDelayEnabled; + this.dataRequestHandlerMock.Verify(x => x.DelayProcessingAssert(), shouldDelay ? Times.Once : Times.Never); + } + + [Theory] + [InlineData(false, true)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(true, false)] + public async Task Connection_Handling_Should_Depend_On_Deduplication_Result_And_Processing_Delay(bool canProcess, bool processingDelayEnabled) + { + // arrange + this.deviceMock.Object.IsConnectionOwner = true; + _ = this.dataRequestHandlerMock.Setup(x => x.TryUseBundlerAssert()).Returns(new FunctionBundlerResult + { + DeduplicationResult = new DeduplicationResult + { CanProcess = canProcess }, + NextFCntDown = null + }); + + this.dataRequestHandlerMock.Setup(x => x.IsProcessingDelayEnabled()).Returns(processingDelayEnabled); + + // act + _ = await this.dataRequestHandlerMock.Object.ProcessRequestAsync(this.loraRequest, this.deviceMock.Object); + + // assert + Assert.Equal(canProcess || !processingDelayEnabled, this.deviceMock.Object.IsConnectionOwner); + + this.deviceMock.Verify(x => x.BeginDeviceClientConnectionActivity(), canProcess ? Times.Once : Times.Never); + + if (canProcess || !processingDelayEnabled) + { + this.deviceMock.Verify(x => x.CloseConnectionAsync(CancellationToken.None, It.IsAny()), Times.Never); + this.dataRequestHandlerMock.Verify(x => x.SaveChangesToDeviceAsyncAssert(), Times.Once); + } + else + { + this.deviceMock.Verify(x => x.CloseConnectionAsync(CancellationToken.None, false), Times.Once); + this.dataRequestHandlerMock.Verify(x => x.SaveChangesToDeviceAsyncAssert(), Times.Never); + } + } + + protected override async ValueTask DisposeAsync(bool disposing) + { + if (disposing) + { + this.cache.Dispose(); + this.loraRequest.Dispose(); + this.testOutputLoggerFactory.Dispose(); + } + await base.DisposeAsync(disposing); + } + } +} diff --git a/Tests/Integration/MultiGatewayTests.cs b/Tests/Integration/MultiGatewayTests.cs index b8cd00084f..bdc8bbbac1 100644 --- a/Tests/Integration/MultiGatewayTests.cs +++ b/Tests/Integration/MultiGatewayTests.cs @@ -51,10 +51,11 @@ public async Task When_Fcnt_Down_Fails_Should_Stop_And_Not_Update_Device_Twin(ui var device = CreateLoRaDevice(simulatedDevice); DeviceCache.Register(device); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -108,20 +109,17 @@ public async Task When_Fcnt_Down_Fails_Should_Stop_And_Not_Update_Device_Twin(ui .ReturnsAsync((Message)null); // twin will be loaded - var initialTwin = new Twin(); - initialTwin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - initialTwin.Properties.Desired[TwinProperty.AppEui] = simulatedDevice.LoRaDevice.AppEui?.ToString(); - initialTwin.Properties.Desired[TwinProperty.AppKey] = simulatedDevice.LoRaDevice.AppKey?.ToString(); - initialTwin.Properties.Desired[TwinProperty.NwkSKey] = simulatedDevice.LoRaDevice.NwkSKey?.ToString(); - initialTwin.Properties.Desired[TwinProperty.AppSKey] = simulatedDevice.LoRaDevice.AppSKey?.ToString(); - initialTwin.Properties.Desired[TwinProperty.DevAddr] = devAddr.ToString(); - if (twinGatewayID != null) - initialTwin.Properties.Desired[TwinProperty.GatewayID] = twinGatewayID; - initialTwin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; - if (deviceTwinFcntDown.HasValue) - initialTwin.Properties.Reported[TwinProperty.FCntDown] = deviceTwinFcntDown.Value; - if (deviceTwinFcntUp.HasValue) - initialTwin.Properties.Reported[TwinProperty.FCntUp] = deviceTwinFcntUp.Value; + var initialTwin = LoRaDeviceTwin.Create( + simulatedDevice.LoRaDevice.GetAbpDesiredTwinProperties() with + { + DevEui = devEui, + GatewayId = twinGatewayID + }, + new LoRaReportedTwinProperties + { + FCntDown = deviceTwinFcntDown, + FCntUp = deviceTwinFcntUp, + }); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(initialTwin); @@ -141,6 +139,16 @@ public async Task When_Fcnt_Down_Fails_Should_Stop_And_Not_Update_Device_Twin(ui .ReturnsAsync(true); } + LoRaDeviceApi + .Setup(x => x.ExecuteFunctionBundlerAsync(devEui, It.IsAny())) + .ReturnsAsync(() => + new FunctionBundlerResult + { + DeduplicationResult = new DeduplicationResult { GatewayId = ServerGatewayID, CanProcess = true, IsDuplicate = false }, + AdrResult = null, + NextFCntDown = 0 + }); + LoRaDeviceApi.Setup(x => x.ABPFcntCacheResetAsync(devEui, It.IsAny(), It.IsNotNull())) .ReturnsAsync(true); @@ -149,10 +157,11 @@ public async Task When_Fcnt_Down_Fails_Should_Stop_And_Not_Update_Device_Twin(ui .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEui, "abc").AsList())); using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -228,11 +237,12 @@ public async Task When_Getting_C2D_Message_Fails_To_Resolve_Fcnt_Down_Should_Aba var cachedDevice = CreateLoRaDevice(simulatedDevice); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(cachedDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(cachedDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); diff --git a/Tests/Integration/ParallelProcessingTests.cs b/Tests/Integration/ParallelProcessingTests.cs index f4cda0af88..9c3ab75684 100644 --- a/Tests/Integration/ParallelProcessingTests.cs +++ b/Tests/Integration/ParallelProcessingTests.cs @@ -29,73 +29,58 @@ public ParallelProcessingTests(ITestOutputHelper testOutputHelper) : base(testOu this.downstreamMessageSender = new TestDownstreamMessageSender(); } - public static IEnumerable Multiple_ABP_Messages() + public static TheoryData Multiple_ABP_Messages() => TheoryDataFactory.From(new[] { - yield return new object[] + new ParallelTestConfiguration { - new ParallelTestConfiguration() - { - DeviceID = 1, - GatewayID = ServerGatewayID, - BetweenMessageDuration = 1000, - SearchByDevAddrDuration = 100, - SendEventDuration = 100, - ReceiveEventDuration = 100, - UpdateTwinDuration = 100, - LoadTwinDuration = 100, - } - }; - + DeviceID = 1, + GatewayID = ServerGatewayID, + BetweenMessageDuration = 1000, + SearchByDevAddrDuration = 100, + SendEventDuration = 100, + ReceiveEventDuration = 100, + UpdateTwinDuration = 100, + LoadTwinDuration = 100, + }, // Slow first calls - yield return new object[] + new ParallelTestConfiguration { - new ParallelTestConfiguration() - { - DeviceID = 2, - GatewayID = ServerGatewayID, - BetweenMessageDuration = 1000, - SearchByDevAddrDuration = new int[] { 1000, 100 }, - SendEventDuration = new int[] { 1000, 100 }, - ReceiveEventDuration = 400, - UpdateTwinDuration = new int[] { 1000, 100 }, - LoadTwinDuration = new int[] { 1000, 100 }, - } - }; - + DeviceID = 2, + GatewayID = ServerGatewayID, + BetweenMessageDuration = 1000, + SearchByDevAddrDuration = new int[] { 1000, 100 }, + SendEventDuration = new int[] { 1000, 100 }, + ReceiveEventDuration = 400, + UpdateTwinDuration = new int[] { 1000, 100 }, + LoadTwinDuration = new int[] { 1000, 100 }, + }, // Slow first calls with non-zero fcnt counts - yield return new object[] + new ParallelTestConfiguration { - new ParallelTestConfiguration() - { - DeviceID = 3, - GatewayID = ServerGatewayID, - BetweenMessageDuration = 1000, - SearchByDevAddrDuration = new int[] { 1000, 100 }, - SendEventDuration = new int[] { 1000, 100 }, - ReceiveEventDuration = 400, - UpdateTwinDuration = new int[] { 1000, 100 }, - LoadTwinDuration = new int[] { 1000, 100 }, - DeviceTwinFcntDown = 5, - DeviceTwinFcntUp = 11, - } - }; - + DeviceID = 3, + GatewayID = ServerGatewayID, + BetweenMessageDuration = 1000, + SearchByDevAddrDuration = new int[] { 1000, 100 }, + SendEventDuration = new int[] { 1000, 100 }, + ReceiveEventDuration = 400, + UpdateTwinDuration = new int[] { 1000, 100 }, + LoadTwinDuration = new int[] { 1000, 100 }, + DeviceTwinFcntDown = 5, + DeviceTwinFcntUp = 11, + }, // Very slow first calls - yield return new object[] + new ParallelTestConfiguration { - new ParallelTestConfiguration() - { - DeviceID = 4, - GatewayID = ServerGatewayID, - BetweenMessageDuration = 1000, - SearchByDevAddrDuration = new int[] { 5000, 100 }, - SendEventDuration = new int[] { 1000, 100 }, - ReceiveEventDuration = 400, - UpdateTwinDuration = new int[] { 5000, 100 }, - LoadTwinDuration = new int[] { 5000, 100 }, - } - }; - } + DeviceID = 4, + GatewayID = ServerGatewayID, + BetweenMessageDuration = 1000, + SearchByDevAddrDuration = new int[] { 5000, 100 }, + SendEventDuration = new int[] { 1000, 100 }, + ReceiveEventDuration = 400, + UpdateTwinDuration = new int[] { 5000, 100 }, + LoadTwinDuration = new int[] { 5000, 100 }, + } + }); [Theory] [MemberData(nameof(Multiple_ABP_Messages))] @@ -127,20 +112,17 @@ public async Task ABP_Load_And_Receiving_Multiple_Unconfirmed_Should_Send_All_To }); // twin will be loaded - var initialTwin = new Twin(); - initialTwin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - initialTwin.Properties.Desired[TwinProperty.AppEui] = simulatedDevice.LoRaDevice.AppEui?.ToString(); - initialTwin.Properties.Desired[TwinProperty.AppKey] = simulatedDevice.LoRaDevice.AppKey?.ToString(); - initialTwin.Properties.Desired[TwinProperty.NwkSKey] = simulatedDevice.LoRaDevice.NwkSKey?.ToString(); - initialTwin.Properties.Desired[TwinProperty.AppSKey] = simulatedDevice.LoRaDevice.AppSKey?.ToString(); - initialTwin.Properties.Desired[TwinProperty.DevAddr] = devAddr.ToString(); - if (parallelTestConfiguration.GatewayID != null) - initialTwin.Properties.Desired[TwinProperty.GatewayID] = parallelTestConfiguration.GatewayID; - initialTwin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; - if (parallelTestConfiguration.DeviceTwinFcntDown.HasValue) - initialTwin.Properties.Reported[TwinProperty.FCntDown] = parallelTestConfiguration.DeviceTwinFcntDown.Value; - if (parallelTestConfiguration.DeviceTwinFcntUp.HasValue) - initialTwin.Properties.Reported[TwinProperty.FCntUp] = parallelTestConfiguration.DeviceTwinFcntUp.Value; + var initialTwin = LoRaDeviceTwin.Create( + simulatedDevice.LoRaDevice.GetAbpDesiredTwinProperties() with + { + DevEui = devEui, + GatewayId = parallelTestConfiguration.GatewayID + }, + new LoRaReportedTwinProperties + { + FCntDown = parallelTestConfiguration.DeviceTwinFcntDown, + FCntUp = parallelTestConfiguration.DeviceTwinFcntUp, + }); looseDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .Returns(() => @@ -196,10 +178,11 @@ public async Task ABP_Load_And_Receiving_Multiple_Unconfirmed_Should_Send_All_To }); using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -298,17 +281,17 @@ private async Task> SendMessages(SimulatedDevice devic const int payloadInitialFcnt = 2; var device1 = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1)); - var device1Twin = TestUtils.CreateABPTwin(device1); + var device1Twin = device1.GetDefaultAbpTwin(); var device2 = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(2)) { DevAddr = device1.DevAddr }; - var device2Twin = TestUtils.CreateABPTwin(device2); + var device2Twin = device2.GetDefaultAbpTwin(); var device3 = new SimulatedDevice(TestDeviceInfo.CreateOTAADevice(3)); device3.SetupJoin(TestKeys.CreateAppSessionKey(0x88), TestKeys.CreateNetworkSessionKey(0x88), new DevAddr(0x02000088)); - var device3Twin = TestUtils.CreateOTAATwin(device3); + var device3Twin = LoRaDeviceTwin.Create(device3.LoRaDevice.GetOtaaDesiredTwinProperties(), device3.GetOtaaReportedTwinProperties()); var device4 = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(4)); - var device4Twin = TestUtils.CreateABPTwin(device4); + var device4Twin = device4.GetDefaultAbpTwin(); var device1And2Result = new IoTHubDeviceInfo[] { @@ -316,6 +299,16 @@ private async Task> SendMessages(SimulatedDevice devic new IoTHubDeviceInfo(device2.DevAddr, device2.DevEUI, "2"), }; + LoRaDeviceApi + .Setup(x => x.ExecuteFunctionBundlerAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(() => + new FunctionBundlerResult + { + DeduplicationResult = new DeduplicationResult { GatewayId = ServerGatewayID, CanProcess = true, IsDuplicate = false }, + AdrResult = null, + NextFCntDown = 0 + }); + LoRaDeviceApi.Setup(x => x.SearchByDevAddrAsync(device1.DevAddr.Value)) .ReturnsAsync(new SearchDevicesResult(device1And2Result), TimeSpan.FromMilliseconds(searchDelay)); @@ -375,9 +368,10 @@ private async Task> SendMessages(SimulatedDevice devic LoRaDeviceFactory.SetClient(device4.DevEUI, deviceClient4.Object); using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); diff --git a/Tests/Integration/PreferredGatewayTestWithRedis.cs b/Tests/Integration/PreferredGatewayTestWithRedis.cs index d570e0f7e3..99fa7fa0f7 100644 --- a/Tests/Integration/PreferredGatewayTestWithRedis.cs +++ b/Tests/Integration/PreferredGatewayTestWithRedis.cs @@ -15,7 +15,9 @@ namespace LoRaWan.Tests.Integration /// Tests to run against a real redis instance. /// [Collection(RedisFixture.CollectionName)] +#pragma warning disable xUnit1033 // False positive: Test classes decorated with 'Xunit.IClassFixture' or 'Xunit.ICollectionFixture' should add a constructor argument of type TFixture public class PreferredGatewayTestWithRedis : IClassFixture +#pragma warning restore xUnit1033 // False positive: Test classes decorated with 'Xunit.IClassFixture' or 'Xunit.ICollectionFixture' should add a constructor argument of type TFixture { private readonly ILoRaDeviceCacheStore cache; private readonly PreferredGatewayExecutionItem preferredGatewayExecutionItem; diff --git a/Tests/Integration/ProcessingTests.cs b/Tests/Integration/ProcessingTests.cs index ab83c2aed9..b4586c6914 100644 --- a/Tests/Integration/ProcessingTests.cs +++ b/Tests/Integration/ProcessingTests.cs @@ -18,6 +18,7 @@ namespace LoRaWan.Tests.Integration using Moq; using Xunit; using Xunit.Abstractions; + using IoTHubDeviceInfo = NetworkServer.IoTHubDeviceInfo; // End to end tests without external dependencies (IoT Hub, Service Facade Function) // General message processor tests (Join tests are handled in other class) @@ -87,17 +88,9 @@ public class ProcessingTests : MessageProcessorTestBase .ReturnsAsync(true); } - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var cachedDevice = CreateLoRaDevice(simulatedDevice); - - DeviceCache.Register(cachedDevice); - - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var cachedDevice = CreateLoRaDevice(simulatedDevice); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(cachedDevice); + var messageDispatcher = messageDispatcherDisposableValue.Value; // sends unconfirmed message var unconfirmedMessagePayload = simulatedDevice.CreateUnconfirmedDataUpMessage("hello", fcnt: payloadFcntUp); @@ -140,7 +133,7 @@ public class ProcessingTests : MessageProcessorTestBase LoRaDeviceClient.VerifyAll(); LoRaDeviceApi.VerifyAll(); - LoRaDeviceClient.Setup(ldc => ldc.Dispose()); + LoRaDeviceClient.Setup(ldc => ldc.DisposeAsync()); } [Theory] @@ -170,16 +163,9 @@ public async Task ABP_Unconfirmed_With_No_Decoder_Sends_Raw_Payload(string devic .ReturnsAsync(true); } - // add device to cache already - DeviceCache.Register(loRaDevice); - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - // Send to message processor - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(loRaDevice); + var messageDispatcher = messageDispatcherDisposableValue.Value; // sends unconfirmed message var unconfirmedMessagePayload = simulatedDevice.CreateUnconfirmedDataUpMessage(msgPayload, fcnt: 1); @@ -213,15 +199,9 @@ public async Task ABP_Unconfirmed_Sends_Valid_Mac_Commands_As_Part_Of_Payload_An LoRaDeviceClient.Setup(x => x.ReceiveAsync(It.IsNotNull())) .ReturnsAsync((Message)null); - using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loRaDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); - // Send to message processor - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(loRaDevice); + var messageDispatcher = messageDispatcherDisposableValue.Value; // sends unconfirmed mac LinkCheckCmd var msgPayload = "02"; @@ -265,32 +245,11 @@ public async Task ABP_Unconfirmed_Sends_Valid_Mac_Commands_In_Fopts_And_Reply_In }) .ReturnsAsync(true); - var c2d = new ReceivedLoRaCloudToDeviceMessage() - { - Payload = "Hello", - Fport = FramePorts.App1, - }; - - using var cloudToDeviceMessage = c2d.CreateMessage(); - - LoRaDeviceClient.SetupSequence(x => x.ReceiveAsync(It.IsAny())) - .ReturnsAsync(cloudToDeviceMessage) - .ReturnsAsync((Message)null); // 2nd cloud to device message does not return anything - - LoRaDeviceClient.Setup(x => x.CompleteAsync(cloudToDeviceMessage)) - .ReturnsAsync(true); - - // add device to cache already - DeviceCache.Register(loRaDevice); - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + using var cloudToDeviceMessageSetup = UsePendingCloudToDeviceMessage(); // Send to message processor - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(loRaDevice); + var messageDispatcher = messageDispatcherDisposableValue.Value; // sends unconfirmed mac LinkCheckCmd var msgPayload = "Hello World"; @@ -339,27 +298,16 @@ public async Task ABP_Unconfirmed_Sends_Invalid_Mac_Commands_In_Fopts(byte macCo }) .ReturnsAsync(true); - var c2dMessage = new ReceivedLoRaCloudToDeviceMessage() - { - Payload = "Hello", - Fport = FramePorts.App1, - }; - - using var cloudToDeviceMessage = c2dMessage.CreateMessage(); - - LoRaDeviceClient.SetupSequence(x => x.ReceiveAsync(It.IsAny())) - .ReturnsAsync(cloudToDeviceMessage) - .ReturnsAsync((Message)null); // 2nd cloud to device message does not return anything - - LoRaDeviceClient.Setup(x => x.CompleteAsync(cloudToDeviceMessage)) - .ReturnsAsync(true); + var cloudToDeviceMessagePayload = "C2DMessagePayload"; + using var cloudToDeviceMessageSetup = UsePendingCloudToDeviceMessage(cloudToDeviceMessagePayload); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loRaDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loRaDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -379,7 +327,7 @@ public async Task ABP_Unconfirmed_Sends_Invalid_Mac_Commands_In_Fopts(byte macCo // FOpts are not encrypted var payload = data.GetDecryptedPayload(simulatedDevice.AppSKey.Value); var c2dreceivedPayload = Encoding.UTF8.GetString(payload); - Assert.Equal(c2dMessage.Payload, c2dreceivedPayload); + Assert.Equal(cloudToDeviceMessagePayload, c2dreceivedPayload); // Nothing should be sent to IoT Hub Assert.NotNull(loRaDeviceTelemetry); @@ -387,6 +335,33 @@ public async Task ABP_Unconfirmed_Sends_Invalid_Mac_Commands_In_Fopts(byte macCo LoRaDeviceApi.VerifyAll(); } + [Fact] + // https://github.com/Azure/iotedge-lorawan-starterkit/issues/1540. + public async Task Secondary_Tasks_Do_Not_Impact_Downstream_Message_Delivery_And_Do_Not_Cause_Processing_Failure() + { + var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, gatewayID: ServerGatewayID)); + var loRaDevice = CreateLoRaDevice(simulatedDevice); + + LoRaDeviceClient.Setup(c => c.SendEventAsync(It.IsAny(), It.IsAny>())) + .ReturnsAsync(true); + + using var cloudToDeviceMessage = UsePendingCloudToDeviceMessage(completeOperationException: new OperationCanceledException("Operation timed out.")); + + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(loRaDevice); + var messageDispatcher = messageDispatcherDisposableValue.Value; + + var payload = simulatedDevice.CreateConfirmedDataUpMessage("foo", fcnt: 1); + using var request = CreateWaitableRequest(payload); + + // act + messageDispatcher.DispatchRequest(request); + await request.WaitCompleteAsync(); + + // assert + Assert.Single(DownstreamMessageSender.DownlinkMessages); + Assert.True(request.ProcessingSucceeded); + } + [Theory] [InlineData("00")] [InlineData("26")] @@ -404,17 +379,9 @@ public async Task ABP_Unconfirmed_Sends_Invalid_Mac_Commands_As_Part_Of_Payload( LoRaDeviceClient.Setup(x => x.ReceiveAsync(It.IsNotNull())) .ReturnsAsync((Message)null); - // add device to cache already - DeviceCache.Register(loRaDevice); - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - // Send to message processor - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(loRaDevice); + var messageDispatcher = messageDispatcherDisposableValue.Value; // sends unconfirmed mac LinkCheckCmd var msgPayload = macCommand; @@ -462,18 +429,11 @@ public async Task ABP_Device_NetId_Should_Match_Server(int deviceNetId, int serv .ReturnsAsync(true); } - // add device to cache already - DeviceCache.Register(loRaDevice); - - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); ServerConfiguration.NetId = new NetId(serverNetId); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(loRaDevice); + var messageDispatcher = messageDispatcherDisposableValue.Value; // sends unconfirmed message var unconfirmedMessagePayload = simulatedDevice.CreateUnconfirmedDataUpMessage(msgPayload, fcnt: 1); @@ -517,11 +477,6 @@ public async Task When_Ack_Message_Received_Should_Be_In_Msg_Properties(string d if (msgId != null) loRaDevice.LastConfirmedC2DMessageID = msgId; - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - DeviceCache.Register(loRaDevice); - - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - Dictionary actualProperties = null; LoRaDeviceClient.Setup(x => x.SendEventAsync(It.IsAny(), It.IsAny>())) .Callback>((t, d) => actualProperties = d) @@ -531,10 +486,8 @@ public async Task When_Ack_Message_Received_Should_Be_In_Msg_Properties(string d LoRaDeviceClient.Setup(x => x.ReceiveAsync(It.IsNotNull())) .ReturnsAsync((Message)null); - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(loRaDevice); + var messageDispatcher = messageDispatcherDisposableValue.Value; var ackMessage = simulatedDevice.CreateUnconfirmedDataUpMessage(data, fcnt: payloadFcnt, fctrlFlags: FrameControlFlags.Ack); using var ackRequest = CreateWaitableRequest(ackMessage); @@ -546,10 +499,10 @@ public async Task When_Ack_Message_Received_Should_Be_In_Msg_Properties(string d Assert.Equal(payloadFcnt, loRaDeviceInfo.FCntUp); Assert.NotNull(actualProperties); - Assert.True(actualProperties.ContainsKey(Constants.C2D_MSG_PROPERTY_VALUE_NAME)); + Assert.True(actualProperties.ContainsKey(NetworkServer.Constants.C2D_MSG_PROPERTY_VALUE_NAME)); if (msgId == null) - Assert.True(actualProperties.ContainsValue(Constants.C2D_MSG_ID_PLACEHOLDER)); + Assert.True(actualProperties.ContainsValue(NetworkServer.Constants.C2D_MSG_ID_PLACEHOLDER)); else Assert.True(actualProperties.ContainsValue(msgId)); } @@ -605,17 +558,9 @@ public async Task When_ConfirmedUp_Message_With_Same_Fcnt_Should_Send_To_Hub_And }); } - // add device to cache already - DeviceCache.Register(loRaDevice); - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - // Send to message processor - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(loRaDevice); + var messageDispatcher = messageDispatcherDisposableValue.Value; // sends confirmed message var confirmedMessagePayload = simulatedDevice.CreateConfirmedDataUpMessage("repeat", fcnt: 100); @@ -671,19 +616,14 @@ public async Task When_Second_Gateway_Does_Not_Find_Device_Should_Keep_Trying_On simulatedDevice.SetupJoin(appSKey, nwkSKey, devAddr); - var updatedTwin = TestUtils.CreateTwin( - desired: new Dictionary - { - { TwinProperty.AppEui, simulatedDevice.AppEui?.ToString() }, - { TwinProperty.AppKey, simulatedDevice.AppKey?.ToString() }, - { TwinProperty.SensorDecoder, nameof(LoRaPayloadDecoder.DecoderValueSensor) }, - }, - reported: new Dictionary + var updatedTwin = LoRaDeviceTwin.Create( + simulatedDevice.LoRaDevice.GetOtaaDesiredTwinProperties() with { SensorDecoder = nameof(LoRaPayloadDecoder.DecoderValueSensor) }, + new LoRaReportedTwinProperties { - { TwinProperty.AppSKey, appSKey.ToString() }, - { TwinProperty.NwkSKey, nwkSKey.ToString() }, - { TwinProperty.DevAddr, devAddr.ToString() }, - { TwinProperty.DevNonce, "ABCD" }, + AppSessionKey = appSKey, + NetworkSessionKey = nwkSKey, + DevAddr = devAddr, + DevNonce = new DevNonce(0xABCD) }); // Twin will be loaded once @@ -705,8 +645,19 @@ public async Task When_Second_Gateway_Does_Not_Find_Device_Should_Keep_Trying_On .ReturnsAsync(new SearchDevicesResult()) .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEUI, "abc").AsList())); + // Making the function bundler return a processable message only for multigw scenario + LoRaDeviceApi + .Setup(x => x.ExecuteFunctionBundlerAsync(devEUI, It.IsAny())) + .ReturnsAsync(() => + new FunctionBundlerResult + { + DeduplicationResult = new DeduplicationResult { GatewayId = ServerGatewayID, CanProcess = true, IsDuplicate = false }, + AdrResult = null, + NextFCntDown = 0 + }); + using var cache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry( + await using var deviceRegistry = new LoRaDeviceRegistry( ServerConfiguration, cache, LoRaDeviceApi.Object, @@ -716,7 +667,8 @@ public async Task When_Second_Gateway_Does_Not_Find_Device_Should_Keep_Trying_On // Making the reload interval zero deviceRegistry.DevAddrReloadInterval = TimeSpan.Zero; - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -728,8 +680,7 @@ public async Task When_Second_Gateway_Does_Not_Find_Device_Should_Keep_Trying_On Assert.True(await unconfirmedRequest1.WaitCompleteAsync()); Assert.Null(unconfirmedRequest1.ResponseDownlink); - // wait 10ms so that loader is removed - await Task.Delay(10); + await WaitForLoaderEvictionAsync(); // Unconfirmed message #2 should fail var payload2 = simulatedDevice.CreateUnconfirmedDataUpMessage("2", fcnt: 2); @@ -738,8 +689,7 @@ public async Task When_Second_Gateway_Does_Not_Find_Device_Should_Keep_Trying_On Assert.True(await unconfirmedRequest2.WaitCompleteAsync()); Assert.Null(unconfirmedRequest2.ResponseDownlink); - // wait 10ms so that loader is removed - await Task.Delay(10); + await WaitForLoaderEvictionAsync(); // Unconfirmed message #3 should succeed var payload3 = simulatedDevice.CreateUnconfirmedDataUpMessage("3", fcnt: 3); @@ -755,6 +705,8 @@ public async Task When_Second_Gateway_Does_Not_Find_Device_Should_Keep_Trying_On LoRaDeviceClient.VerifyAll(); LoRaDeviceApi.VerifyAll(); + + Task WaitForLoaderEvictionAsync() => cache.WaitForEvictionAsync(LoRaDeviceRegistry.GetDevLoaderCacheKey(devAddr), CancellationToken.None); } /// @@ -783,18 +735,10 @@ public async Task ABP_Confirmed_Message_Should_Use_Rchf_0(string deviceGatewayID LoRaDeviceClient.Setup(x => x.ReceiveAsync(It.IsNotNull())) .ReturnsAsync((Message)null); - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - var cachedDevice = CreateLoRaDevice(simulatedDevice); - - DeviceCache.Register(cachedDevice); - - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - // Send to message processor - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + var cachedDevice = CreateLoRaDevice(simulatedDevice); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(cachedDevice); + var messageDispatcher = messageDispatcherDisposableValue.Value; // sends unconfirmed message var messagePayload = simulatedDevice.CreateConfirmedDataUpMessage("1234"); @@ -837,18 +781,10 @@ public async Task ABP_Confirmed_Message_Should_Use_Rchf_0(string deviceGatewayID .ReturnsAsync(true); } - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - var cachedDevice = CreateLoRaDevice(simulatedDevice); - - DeviceCache.Register(cachedDevice); - - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - // Send to message processor - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + var cachedDevice = CreateLoRaDevice(simulatedDevice); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(cachedDevice); + var messageDispatcher = messageDispatcherDisposableValue.Value; // sends unconfirmed message var unconfirmedMessagePayload = simulatedDevice.CreateUnconfirmedDataUpMessage("hello"); @@ -879,7 +815,7 @@ public async Task ABP_When_Getting_Twin_Fails_Should_Work_On_Retry(string device var devAddr = simulatedDevice.DevAddr.Value; // Device twin will be queried - var twin = simulatedDevice.CreateABPTwin(); + var twin = simulatedDevice.GetDefaultAbpTwin(); LoRaDeviceClient.SetupSequence(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync((Twin)null) .ReturnsAsync(twin); @@ -895,19 +831,34 @@ public async Task ABP_When_Getting_Twin_Fails_Should_Work_On_Retry(string device .ReturnsAsync((Message)null); // first device client will be disposed - LoRaDeviceClient.Setup(x => x.Dispose()); + LoRaDeviceClient.Setup(x => x.DisposeAsync()); // Lora device api will be search by devices with matching deveui, LoRaDeviceApi.Setup(x => x.SearchByDevAddrAsync(devAddr)) .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEui, "aabb").AsList())); + // Making the function bundler return a processable message only for multigw scenario + if (deviceGatewayID is null) + { + LoRaDeviceApi + .Setup(x => x.ExecuteFunctionBundlerAsync(devEui, It.IsAny())) + .ReturnsAsync(() => + new FunctionBundlerResult + { + DeduplicationResult = new DeduplicationResult { GatewayId = ServerGatewayID, CanProcess = true, IsDuplicate = false }, + AdrResult = null, + NextFCntDown = 0 + }); + } + using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Setting the interval in which we search for devices with same devAddr on server deviceRegistry.DevAddrReloadInterval = TimeSpan.Zero; - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -922,8 +873,7 @@ public async Task ABP_When_Getting_Twin_Fails_Should_Work_On_Retry(string device Assert.False(DeviceCache.TryGetForPayload(request1.Payload, out _)); - // Wait 100ms so loader can be removed from cache - await Task.Delay(100); + await cache.WaitForEvictionAsync(LoRaDeviceRegistry.GetDevLoaderCacheKey(devAddr), CancellationToken.None); // sends 2nd unconfirmed message, now get twin will work var unconfirmedMessage2 = simulatedDevice.CreateUnconfirmedDataUpMessage("2", fcnt: 2); @@ -959,7 +909,7 @@ public async Task ABP_When_First_Message_Has_Invalid_Mic_Second_Should_Send_To_H { var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, gatewayID: ServerGatewayID)); - using var loRaDevice = CreateLoRaDevice(simulatedDevice, isAlreadyInDeviceRegistryCache); + await using var loRaDevice = CreateLoRaDevice(simulatedDevice, isAlreadyInDeviceRegistryCache); loRaDevice.SensorDecoder = "DecoderValueSensor"; // message will be sent @@ -971,7 +921,7 @@ public async Task ABP_When_First_Message_Has_Invalid_Mic_Second_Should_Send_To_H if (!isAlreadyInDeviceRegistryCache) { LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(simulatedDevice.CreateABPTwin()); + .ReturnsAsync(simulatedDevice.GetDefaultAbpTwin()); } // C2D message will be checked @@ -982,20 +932,9 @@ public async Task ABP_When_First_Message_Has_Invalid_Mic_Second_Should_Send_To_H LoRaDeviceApi.Setup(x => x.SearchByDevAddrAsync(loRaDevice.DevAddr.Value)) .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(loRaDevice.DevAddr, loRaDevice.DevEUI, "aaa").AsList())); - // add device to cache already - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - if (isAlreadyInDeviceRegistryCache) - { - DeviceCache.Register(loRaDevice); - } - - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - // Send to message processor - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(isAlreadyInDeviceRegistryCache ? loRaDevice : null); + var messageDispatcher = messageDispatcherDisposableValue.Value; // first message should fail const int firstMessageFcnt = 3; @@ -1027,7 +966,7 @@ public async Task ABP_When_First_Message_Has_Invalid_Mic_Second_Should_Send_To_H LoRaDeviceApi.VerifyAll(); LoRaDeviceClient.VerifyAll(); - LoRaDeviceClient.Setup(ldc => ldc.Dispose()); + LoRaDeviceClient.Setup(ldc => ldc.DisposeAsync()); } /// @@ -1041,29 +980,24 @@ public async Task ABP_When_AppSKey_Or_NwkSKey_Or_DevAddr_Is_Missing_Should_Not_S { var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, gatewayID: ServerGatewayID)); - using var loRaDevice = CreateLoRaDevice(simulatedDevice, false); + await using var loRaDevice = CreateLoRaDevice(simulatedDevice, false); loRaDevice.SensorDecoder = "DecoderValueSensor"; // will get the device twin without AppSKey - var twin = TestUtils.CreateABPTwin(simulatedDevice); + var twin = simulatedDevice.GetDefaultAbpTwin(); twin.Properties.Desired[missingProperty] = null; LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); - LoRaDeviceClient.Setup(x => x.Dispose()); + LoRaDeviceClient.Setup(x => x.DisposeAsync()); // Lora device api // will search for the device twice LoRaDeviceApi.Setup(x => x.SearchByDevAddrAsync(loRaDevice.DevAddr.Value)) .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(loRaDevice.DevAddr, loRaDevice.DevEUI, "aaa").AsList())); - using var cache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - // Send to message processor - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(); + var messageDispatcher = messageDispatcherDisposableValue.Value; // message should not be processed using var request = CreateWaitableRequest(simulatedDevice.CreateUnconfirmedDataUpMessage("1234")); @@ -1132,18 +1066,10 @@ public async Task When_ConfirmedUp_Message_Is_Resubmitted_Should_Ack_3_Times(str .ReturnsAsync((DevEui _, FunctionBundlerRequest _) => new FunctionBundlerResult { AdrResult = new LoRaTools.ADR.LoRaADRResult { CanConfirmToDevice = true, NbRepetition = 1, TxPower = 0, FCntDown = deviceInitialFcntDown + 8 }, NextFCntDown = deviceInitialFcntDown + 8 }); } - // add device to cache already - var loRaDevice = CreateLoRaDevice(simulatedDevice); - DeviceCache.Register(loRaDevice); - - using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - // Send to message processor - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + var loRaDevice = CreateLoRaDevice(simulatedDevice); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(loRaDevice); + var messageDispatcher = messageDispatcherDisposableValue.Value; // sends confirmed message var firstMessagePayload = simulatedDevice.CreateConfirmedDataUpMessage("repeat", fcnt: deviceInitialFcntUp + 1); @@ -1204,14 +1130,9 @@ public async Task ABP_Device_With_Invalid_NetId_Should_Not_Load_Devices() var msgPayload = "1234"; var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, netId: 0)); - using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - // Send to message processor - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(); + var messageDispatcher = messageDispatcherDisposableValue.Value; // sends unconfirmed message #1 var unconfirmedMessagePayload1 = simulatedDevice.CreateUnconfirmedDataUpMessage(msgPayload, fcnt: 1); @@ -1240,7 +1161,7 @@ public async Task ABP_Device_With_Invalid_NetId_In_Allowed_DevAdr_Should_Be_Acce { var msgPayload = "1234"; var deviceClient = new Mock(MockBehavior.Strict); - deviceClient.Setup(dc => dc.Dispose()); + deviceClient.Setup(dc => dc.DisposeAsync()).Returns(ValueTask.CompletedTask); var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, netId: 0, gatewayID: ServerGatewayID)); var devAddr = simulatedDevice.LoRaDevice.DevAddr.Value; @@ -1251,14 +1172,9 @@ public async Task ABP_Device_With_Invalid_NetId_In_Allowed_DevAdr_Should_Be_Acce simulatedDevice.DevAddr.Value }; - using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - // Send to message processor - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(); + var messageDispatcher = messageDispatcherDisposableValue.Value; // device api will be searched for payload var searchDevicesResult = new SearchDevicesResult(new[] @@ -1270,9 +1186,14 @@ public async Task ABP_Device_With_Invalid_NetId_In_Allowed_DevAdr_Should_Be_Acce LoRaDeviceApi.Setup(x => x.SearchByDevAddrAsync(devAddr)) .ReturnsAsync(searchDevicesResult); - deviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(simulatedDevice.CreateABPTwin()); + deviceClient.Setup(x => x.EnsureConnected()).Returns(true); + + deviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(simulatedDevice.GetDefaultAbpTwin()); + + deviceClient.Setup(x => x.DisconnectAsync(It.IsAny())) + .Returns(Task.CompletedTask); - deviceClient.Setup(x => x.Disconnect()) + deviceClient.Setup(x => x.EnsureConnected()) .Returns(true); deviceClient.Setup(x => x.ReceiveAsync(It.IsNotNull())) @@ -1341,7 +1262,7 @@ public async Task When_Loading_Multiple_Devices_With_Same_DevAddr_Should_Add_All deviceClient1.Setup(x => x.ReceiveAsync(It.IsNotNull())) .ReturnsAsync((Message)null); - deviceClient1.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(simulatedDevice1.CreateABPTwin()); + deviceClient1.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(simulatedDevice1.GetDefaultAbpTwin()); if (isResetingDevice) { @@ -1352,7 +1273,7 @@ public async Task When_Loading_Multiple_Devices_With_Same_DevAddr_Should_Add_All // Device client 2 // - Get Twin var deviceClient2 = new Mock(); - deviceClient2.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(simulatedDevice2.CreateABPTwin()); + deviceClient2.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(simulatedDevice2.GetDefaultAbpTwin()); // device api will be searched for payload var searchDevicesResult = new SearchDevicesResult(new[] @@ -1367,14 +1288,9 @@ public async Task When_Loading_Multiple_Devices_With_Same_DevAddr_Should_Add_All LoRaDeviceFactory.SetClient(simulatedDevice1.DevEUI, deviceClient1.Object); LoRaDeviceFactory.SetClient(simulatedDevice2.DevEUI, deviceClient2.Object); - using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - // Send to message processor - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(); + var messageDispatcher = messageDispatcherDisposableValue.Value; // sends unconfirmed message #1 var unconfirmedMessagePayload1 = simulatedDevice1.CreateUnconfirmedDataUpMessage("1", fcnt: payloadFcntUp); @@ -1406,14 +1322,14 @@ public async Task When_Loading_Multiple_Devices_With_Same_DevAddr_Should_Add_All } else { - Assert.Equal(simulatedDevice1.FrmCntDown + Constants.MaxFcntUnsavedDelta - 1U, loRaDevice1.FCntDown); + Assert.Equal(simulatedDevice1.FrmCntDown + NetworkServer.Constants.MaxFcntUnsavedDelta - 1U, loRaDevice1.FCntDown); } Assert.Equal(payloadFcntUp + 1, loRaDevice1.FCntUp); Assert.True(DeviceCache.TryGetByDevEui(simulatedDevice2.DevEUI, out var loRaDevice2)); Assert.Equal(simulatedDevice2.FrmCntUp, loRaDevice2.FCntUp); - Assert.Equal(simulatedDevice2.FrmCntDown + Constants.MaxFcntUnsavedDelta - 1U, loRaDevice2.FCntDown); + Assert.Equal(simulatedDevice2.FrmCntDown + NetworkServer.Constants.MaxFcntUnsavedDelta - 1U, loRaDevice2.FCntDown); deviceClient1.VerifyAll(); deviceClient2.VerifyAll(); @@ -1457,7 +1373,7 @@ public async Task When_Loading_Multiple_Devices_With_Same_DevAddr_One_Fails_Shou deviceClient1.Setup(x => x.ReceiveAsync(It.IsNotNull())) .ReturnsAsync((Message)null); - deviceClient1.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(simulatedDevice1.CreateABPTwin()); + deviceClient1.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(simulatedDevice1.GetDefaultAbpTwin()); // If the framecounter is higher than 10 it will trigger an update of the framcounter in the reported properties. if (payloadFcntUp > 10) @@ -1482,14 +1398,9 @@ public async Task When_Loading_Multiple_Devices_With_Same_DevAddr_One_Fails_Shou LoRaDeviceFactory.SetClient(simulatedDevice1.DevEUI, deviceClient1.Object); LoRaDeviceFactory.SetClient(simulatedDevice2.DevEUI, deviceClient2.Object); - using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - // Send to message processor - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(); + var messageDispatcher = messageDispatcherDisposableValue.Value; // sends unconfirmed message #1 var unconfirmedMessagePayload1 = simulatedDevice1.CreateUnconfirmedDataUpMessage("1", fcnt: payloadFcntUp); @@ -1519,7 +1430,7 @@ public async Task When_Loading_Multiple_Devices_With_Same_DevAddr_One_Fails_Shou } else { - Assert.Equal(Constants.MaxFcntUnsavedDelta - 1U, loRaDevice1.FCntDown); + Assert.Equal(NetworkServer.Constants.MaxFcntUnsavedDelta - 1U, loRaDevice1.FCntDown); } Assert.Equal(payloadFcntUp + 1, loRaDevice1.FCntUp); @@ -1559,15 +1470,9 @@ public async Task When_Upstream_Is_Empty_Should_Call_Decoder_And_Send_Event_To_I .Callback((DevEui _, byte[] data, FramePort fport, string decoder) => receivedDecodeCalls.Add((fport, data))); PayloadDecoder.SetDecoder(payloadDecoder.Object); - using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loRaDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); - // Send to message processor - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(loRaDevice); + var messageDispatcher = messageDispatcherDisposableValue.Value; // sends unconfirmed message var unconfirmedMessagePayload1 = simDevice.CreateUnconfirmedDataUpMessage(null, fcnt: 4); @@ -1607,13 +1512,11 @@ public async Task When_Upstream_Fcnt_Is_Lower_Or_Equal_To_Device_Should_Discard_ var cachedDevice = CreateLoRaDevice(simulatedDevice, false); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(cachedDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(cachedDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(cachedDevice); + var messageDispatcher = messageDispatcherDisposableValue.Value; // sends unconfirmed message var unconfirmedMessagePayload = simulatedDevice.CreateUnconfirmedDataUpMessage("hello", fcnt: payloadFcnt); @@ -1651,7 +1554,7 @@ public async Task When_Receiving_Device_Message_And_Loading_Device_Fails_Second_ LoRaDeviceClient.SetupSequence(x => x.GetTwinAsync(CancellationToken.None)) .ThrowsAsync(new TimeoutException()) - .ReturnsAsync(simDevice.CreateABPTwin()); + .ReturnsAsync(simDevice.GetDefaultAbpTwin()); LoRaDeviceClient.Setup(x => x.SendEventAsync(It.IsNotNull(), null)) .ReturnsAsync(true); @@ -1659,13 +1562,8 @@ public async Task When_Receiving_Device_Message_And_Loading_Device_Fails_Second_ LoRaDeviceClient.Setup(x => x.ReceiveAsync(It.IsNotNull())) .ReturnsAsync((Message)null); - using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); - - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(); + var messageDispatcher = messageDispatcherDisposableValue.Value; using var request1 = CreateWaitableRequest(simDevice.CreateUnconfirmedDataUpMessage("1")); messageDispatcher.DispatchRequest(request1); @@ -1704,7 +1602,7 @@ public async Task When_Receiving_Device_Message_And_Loading_Device_Fails_Second_ } LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(simDevice.CreateABPTwin()); + .ReturnsAsync(simDevice.GetDefaultAbpTwin()); LoRaDeviceClient.Setup(x => x.SendEventAsync(It.IsNotNull(), null)) .ReturnsAsync(true); @@ -1712,13 +1610,22 @@ public async Task When_Receiving_Device_Message_And_Loading_Device_Fails_Second_ LoRaDeviceClient.Setup(x => x.ReceiveAsync(It.IsNotNull())) .ReturnsAsync((Message)null); - using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + // Making the function bundler return a processable message only for multigw scenario + if (gatewayID is null) + { + LoRaDeviceApi + .Setup(x => x.ExecuteFunctionBundlerAsync(simDevice.DevEUI, It.IsAny())) + .ReturnsAsync(() => + new FunctionBundlerResult + { + DeduplicationResult = new DeduplicationResult { GatewayId = ServerGatewayID, CanProcess = true, IsDuplicate = false }, + AdrResult = null, + NextFCntDown = 0 + }); + } - using var messageDispatcher = new MessageDispatcher( - ServerConfiguration, - deviceRegistry, - FrameCounterUpdateStrategyProvider); + await using var messageDispatcherDisposableValue = SetupMessageDispatcherAsync(); + var messageDispatcher = messageDispatcherDisposableValue.Value; using var request1 = CreateWaitableRequest(simDevice.CreateUnconfirmedDataUpMessage("1", fcnt: payloadFcnt)); messageDispatcher.DispatchRequest(request1); @@ -1730,5 +1637,50 @@ public async Task When_Receiving_Device_Message_And_Loading_Device_Fails_Second_ LoRaDeviceClient.Verify(x => x.UpdateReportedPropertiesAsync(It.IsAny(), It.IsAny()), Times.Never); } + + private IDisposable UsePendingCloudToDeviceMessage(Exception completeOperationException = null) => UsePendingCloudToDeviceMessage(Guid.NewGuid().ToString(), completeOperationException); + + private IDisposable UsePendingCloudToDeviceMessage(string payload, Exception completeOperationException = null) + { + var cloudToDeviceMessage = new ReceivedLoRaCloudToDeviceMessage { Payload = payload, Fport = FramePorts.App1 }.CreateMessage(); + LoRaDeviceClient.SetupSequence(x => x.ReceiveAsync(It.IsAny())) + .ReturnsAsync(cloudToDeviceMessage) + .ReturnsAsync((Message)null); // 2nd cloud to device message does not return anything + + if (completeOperationException is { } someCompleteOperationException) + { + LoRaDeviceClient.Setup(x => x.CompleteAsync(cloudToDeviceMessage)) + .ThrowsAsync(someCompleteOperationException); + } + else + { + LoRaDeviceClient.Setup(x => x.CompleteAsync(cloudToDeviceMessage)) + .ReturnsAsync(true); + } + + return cloudToDeviceMessage; + } + + private AsyncDisposableValue SetupMessageDispatcherAsync() => SetupMessageDispatcherAsync(null); + + private AsyncDisposableValue SetupMessageDispatcherAsync(LoRaDevice loRaDevice) + { + var cache = EmptyMemoryCache(); + + if (loRaDevice is { } someLoRaDevice) + { + DeviceCache.Register(someLoRaDevice); + } +#pragma warning disable CA2000 // Dispose objects before losing scope (ownership transferred to caller) + var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + var messageDispatcher = TestMessageDispatcher.Create(cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); +#pragma warning restore CA2000 // Dispose objects before losing scope + return new AsyncDisposableValue(messageDispatcher, async () => + { + cache.Dispose(); + await deviceRegistry.DisposeAsync(); + await messageDispatcher.DisposeAsync(); + }); + } } } diff --git a/Tests/Integration/RedisChannelPublisherTests.cs b/Tests/Integration/RedisChannelPublisherTests.cs new file mode 100644 index 0000000000..3d2451b58c --- /dev/null +++ b/Tests/Integration/RedisChannelPublisherTests.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Integration +{ + using System; + using System.Threading.Tasks; + using LoRaWan.Tests.Common; + using Xunit; + using LoraKeysManagerFacade; + using Xunit.Abstractions; + using Microsoft.Extensions.Logging.Abstractions; + using StackExchange.Redis; + using Moq; + using LoRaTools; + using System.Text.Json; + + [Collection(RedisFixture.CollectionName)] +#pragma warning disable xUnit1033 // False positive: Test classes decorated with 'Xunit.IClassFixture' or 'Xunit.ICollectionFixture' should add a constructor argument of type TFixture + public class RedisChannelPublisherTests : IClassFixture +#pragma warning restore xUnit1033 // False positive: Test classes decorated with 'Xunit.IClassFixture' or 'Xunit.ICollectionFixture' should add a constructor argument of type TFixture + { + private readonly IChannelPublisher channelPublisher; + private readonly ITestOutputHelper testOutputHelper; + private readonly ConnectionMultiplexer redis; + + public RedisChannelPublisherTests(RedisFixture redis, ITestOutputHelper testOutputHelper) + { + if (redis is null) throw new ArgumentNullException(nameof(redis)); + this.channelPublisher = new RedisChannelPublisher(redis.Redis, NullLogger.Instance); + this.testOutputHelper = testOutputHelper; + this.redis = redis.Redis; + } + + [Fact] + public async Task Publish_Aysnc() + { + // arrange + var message = new LnsRemoteCall(RemoteCallKind.CloseConnection, "test message"); + var serializedMessage = JsonSerializer.Serialize(message); + var channel = "channel1"; + var assert = new Mock>(); + this.testOutputHelper.WriteLine("Publishing message..."); + (await this.redis.GetSubscriber().SubscribeAsync(channel)).OnMessage(assert.Object); + + // act + await this.channelPublisher.PublishAsync(channel, message); + + // assert + await assert.RetryVerifyAsync(a => a.Invoke(It.Is(actual => actual.Message == serializedMessage)), Times.Once); + } + } +} diff --git a/Tests/Integration/RedisFixture.cs b/Tests/Integration/RedisFixture.cs index 0ca5064243..10d8f7a3bd 100644 --- a/Tests/Integration/RedisFixture.cs +++ b/Tests/Integration/RedisFixture.cs @@ -27,11 +27,10 @@ public class RedisFixture : IAsyncLifetime private const int RedisPort = 6001; private static readonly string TestContainerName = ContainerName + RedisPort; - private ConnectionMultiplexer redis; - private string containerId; public IDatabase Database { get; set; } + public ConnectionMultiplexer Redis { get; set; } private async Task StartRedisContainer() { @@ -51,8 +50,15 @@ private async Task StartRedisContainer() containers = await client.Containers.ListContainersAsync(new ContainersListParameters() { All = true }); Console.WriteLine("listing container..."); - // Download image - await client.Images.CreateImageAsync(new ImagesCreateParameters() { FromImage = ImageName, Tag = ImageTag }, new AuthConfig(), new Progress()); + // Download image only if not found + try + { + _ = await client.Images.InspectImageAsync($"{ImageName}:{ImageTag}"); + } + catch (DockerImageNotFoundException) + { + await client.Images.CreateImageAsync(new ImagesCreateParameters() { FromImage = ImageName, Tag = ImageTag }, new AuthConfig(), new Progress()); + } // Create the container var config = new Config() @@ -120,14 +126,15 @@ public async Task InitializeAsync() var redisConnectionString = $"localhost:{RedisPort}"; try { - this.redis = await ConnectionMultiplexer.ConnectAsync(redisConnectionString); - Database = this.redis.GetDatabase(); + this.Redis = await ConnectionMultiplexer.ConnectAsync(redisConnectionString); + Database = this.Redis.GetDatabase(); } catch (Exception ex) { throw new InvalidOperationException($"Failed to connect to redis at '{redisConnectionString}'. If running locally with docker: run 'docker run -d -p 6379:6379 redis'. If running in Azure DevOps: run redis in docker.", ex); } } + public async Task DisposeAsync() { @@ -150,8 +157,8 @@ await client.Containers.RemoveContainerAsync(this.containerId, new ContainerRemo } } - this.redis?.Dispose(); - this.redis = null; + this.Redis?.Dispose(); + this.Redis = null; } } } diff --git a/Tests/Integration/RedisRemoteCallListenerTests.cs b/Tests/Integration/RedisRemoteCallListenerTests.cs new file mode 100644 index 0000000000..bb872046b8 --- /dev/null +++ b/Tests/Integration/RedisRemoteCallListenerTests.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Integration +{ + using System; + using System.Collections.Generic; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + using LoRaTools; + using LoRaWan.NetworkServer; + using LoRaWan.Tests.Common; + using Microsoft.Extensions.Logging; + using Moq; + using StackExchange.Redis; + using Xunit; + using Xunit.Sdk; + + [Collection(RedisFixture.CollectionName)] + public sealed class RedisRemoteCallListenerTests : IClassFixture + { + private readonly ConnectionMultiplexer redis; + private readonly Mock> logger; + private readonly RedisRemoteCallListener subject; + + public RedisRemoteCallListenerTests(RedisFixture redisFixture) + { + this.redis = redisFixture.Redis; + this.logger = new Mock>(); + this.subject = new RedisRemoteCallListener(this.redis, this.logger.Object, TestMeter.Instance); + } + + [Fact] + public async Task Subscribe_Receives_Message() + { + // arrange + var lnsName = "some-lns"; + var remoteCall = new LnsRemoteCall(RemoteCallKind.CloudToDeviceMessage, "somejsondata"); + var function = new Mock>(); + + // act + await this.subject.SubscribeAsync(lnsName, function.Object, CancellationToken.None); + await PublishAsync(lnsName, remoteCall); + + // assert + await function.RetryVerifyAsync(a => a.Invoke(remoteCall), Times.Once); + } + + [Fact] + public async Task Subscribe_On_Different_Channel_Does_Not_Receive_Message() + { + // arrange + var function = new Mock>(); + + // act + await this.subject.SubscribeAsync("lns-1", function.Object, CancellationToken.None); + await PublishAsync("lns-2", new LnsRemoteCall(RemoteCallKind.CloudToDeviceMessage, null)); + + // assert + await function.RetryVerifyAsync(a => a.Invoke(It.IsAny()), Times.Never); + } + + [Fact] + public async Task UnsubscribeAsync_Unsubscribes_Successfully() + { + // arrange + var lns = "lns-1"; + var function = new Mock>(); + await this.subject.SubscribeAsync(lns, function.Object, CancellationToken.None); + + // act + await this.subject.UnsubscribeAsync(lns, CancellationToken.None); + await PublishAsync(lns, new LnsRemoteCall(RemoteCallKind.CloudToDeviceMessage, null)); + + // assert + function.Verify(a => a.Invoke(It.IsAny()), Times.Never); + } + + [Fact] + public async Task SubscribeAsync_Exceptions_Are_Tracked() + { + // arrange + var lns = "lns-1"; + var function = new Mock>(); + + // act + await this.subject.SubscribeAsync(lns, function.Object, CancellationToken.None); + await this.redis.GetSubscriber().PublishAsync(lns, string.Empty); + + // assert + var invocation = await RetryAssertSingleAsync(this.logger.GetLogInvocations()); + _ = Assert.IsType(invocation.Exception); + } + + private async Task PublishAsync(string channel, LnsRemoteCall lnsRemoteCall) + { + await this.redis.GetSubscriber().PublishAsync(channel, JsonSerializer.Serialize(lnsRemoteCall)); + } + + private static async Task RetryAssertSingleAsync(IEnumerable sequence, + int numberOfRetries = 5, + TimeSpan? delay = null) + { + var retryDelay = delay ?? TimeSpan.FromMilliseconds(50); + for (var i = 0; i < numberOfRetries + 1; ++i) + { + try + { + var result = Assert.Single(sequence); + return result; + } + catch (SingleException) when (i < numberOfRetries) + { + // assertion does not yet pass, retry once more. + await Task.Delay(retryDelay); + continue; + } + } + + throw new InvalidOperationException("asdfasdf"); + } + } +} diff --git a/Tests/Integration/TestDefaultLoRaRequestHandler.cs b/Tests/Integration/TestDefaultLoRaRequestHandler.cs index 9e4b59e3bc..f2f56a1631 100644 --- a/Tests/Integration/TestDefaultLoRaRequestHandler.cs +++ b/Tests/Integration/TestDefaultLoRaRequestHandler.cs @@ -10,7 +10,9 @@ namespace LoRaWan.Tests.Integration using LoRaWan.NetworkServer; using LoRaWan.NetworkServer.ADR; using LoRaWan.Tests.Common; + using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; + using Moq; using Xunit.Abstractions; internal class TestDefaultLoRaRequestHandler : DefaultLoRaDataRequestHandler @@ -28,7 +30,7 @@ internal class TestDefaultLoRaRequestHandler : DefaultLoRaDataRequestHandler ILoRaADRStrategyProvider loRaADRStrategyProvider, ILoRAADRManagerFactory loRaADRManagerFactory, IFunctionBundlerProvider functionBundlerProvider, - ITestOutputHelper testOutputHelper) : base( + ITestOutputHelper testOutputHelper) : this( configuration, frameCounterUpdateStrategyProvider, concentratorDeduplication, @@ -37,25 +39,52 @@ internal class TestDefaultLoRaRequestHandler : DefaultLoRaDataRequestHandler loRaADRStrategyProvider, loRaADRManagerFactory, functionBundlerProvider, - new TestOutputLogger(testOutputHelper), + new TestOutputLogger(testOutputHelper)) + { } + + public TestDefaultLoRaRequestHandler( + NetworkServerConfiguration configuration, + ILoRaDeviceFrameCounterUpdateStrategyProvider frameCounterUpdateStrategyProvider, + IConcentratorDeduplication concentratorDeduplication, + ILoRaPayloadDecoder payloadDecoder, + IDeduplicationStrategyFactory deduplicationFactory, + ILoRaADRStrategyProvider loRaADRStrategyProvider, + ILoRAADRManagerFactory loRaADRManagerFactory, + IFunctionBundlerProvider functionBundlerProvider, + ILogger logger) : base( + configuration, + frameCounterUpdateStrategyProvider, + concentratorDeduplication, + payloadDecoder, + deduplicationFactory, + loRaADRStrategyProvider, + loRaADRManagerFactory, + functionBundlerProvider, + logger, TestMeter.Instance) { this.configuration = configuration; } - protected override Task TryUseBundler(LoRaRequest request, LoRaDevice loRaDevice, LoRaPayloadData loraPayload, bool useMultipleGateways) + protected override FunctionBundler CreateBundler(LoRaPayloadData loraPayload, LoRaDevice loRaDevice, LoRaRequest request) + => new Mock().Object; + + protected override Task DelayProcessing() + => DelayProcessingAssert(); + + protected override Task TryUseBundler(FunctionBundler bundler, LoRaDevice loRaDevice) => Task.FromResult(TryUseBundlerAssert()); protected override Task PerformADR(LoRaRequest request, LoRaDevice loRaDevice, LoRaPayloadData loraPayload, uint payloadFcnt, LoRaADRResult loRaADRResult, ILoRaDeviceFrameCounterUpdateStrategy frameCounterStrategy) => Task.FromResult(PerformADRAssert()); - protected override Task ReceiveCloudToDeviceAsync(LoRaDevice loRaDevice, TimeSpan timeAvailableToCheckCloudToDeviceMessages) + internal override Task ReceiveCloudToDeviceAsync(LoRaDevice loRaDevice, TimeSpan timeAvailableToCheckCloudToDeviceMessages) => Task.FromResult(null); - protected override Task SendDeviceEventAsync(LoRaRequest request, LoRaDevice loRaDevice, LoRaOperationTimeWatcher timeWatcher, object decodedValue, bool isDuplicate, byte[] decryptedPayloadData) + internal override Task SendDeviceEventAsync(LoRaRequest request, LoRaDevice loRaDevice, LoRaOperationTimeWatcher timeWatcher, object decodedValue, bool isDuplicate, byte[] decryptedPayloadData) => Task.FromResult(SendDeviceAsyncAssert()); - protected override DownlinkMessageBuilderResponse DownlinkMessageBuilderResponse(LoRaRequest request, + internal override DownlinkMessageBuilderResponse DownlinkMessageBuilderResponse(LoRaRequest request, LoRaDevice loRaDevice, LoRaOperationTimeWatcher timeWatcher, LoRaADRResult loRaADRResult, @@ -67,11 +96,13 @@ protected override Task SendDeviceEventAsync(LoRaRequest request, LoRaDevi protected override Task SendMessageDownstreamAsync(LoRaRequest request, DownlinkMessageBuilderResponse confirmDownlinkMessageBuilderResp) => Task.FromResult(SendMessageDownstreamAsyncAssert(confirmDownlinkMessageBuilderResp)); - protected override Task SaveChangesToDeviceAsync(LoRaDevice loRaDevice, bool stationEuiChanged) + internal override Task SaveChangesToDeviceAsync(LoRaDevice loRaDevice, bool stationEuiChanged) => Task.FromResult(SaveChangesToDeviceAsyncAssert()); public virtual LoRaADRResult PerformADRAssert() => null; + public virtual Task DelayProcessingAssert() => Task.CompletedTask; + public virtual FunctionBundlerResult TryUseBundlerAssert() => null; public virtual bool SendDeviceAsyncAssert() => true; diff --git a/Tests/Simulation/IntegrationTestFixtureSim.cs b/Tests/Simulation/IntegrationTestFixtureSim.cs index 60479331e4..8db6a84272 100644 --- a/Tests/Simulation/IntegrationTestFixtureSim.cs +++ b/Tests/Simulation/IntegrationTestFixtureSim.cs @@ -8,6 +8,7 @@ namespace LoRaWan.Tests.Simulation using System.Globalization; using System.IO; using System.Linq; + using System.Threading.Tasks; using LoRaWan.NetworkServer; using LoRaWan.Tests.Common; using Newtonsoft.Json.Linq; @@ -20,6 +21,12 @@ public class IntegrationTestFixtureSim : IntegrationTestFixtureBase // Device1002_Simulated_OTAA: used for simulator public TestDeviceInfo Device1002_Simulated_OTAA { get; private set; } + // Device1003_Simulated_ABP: used for ABP simulator + public TestDeviceInfo Device1003_Simulated_ABP { get; private set; } + + // Device1004_Simulated_ABP: used for ABP simulator + public TestDeviceInfo Device1004_Simulated_ABP { get; private set; } + private readonly List deviceRange1000_ABP = new List(); public IReadOnlyCollection DeviceRange1000_ABP => this.deviceRange1000_ABP; @@ -43,6 +50,12 @@ public class IntegrationTestFixtureSim : IntegrationTestFixtureBase public IReadOnlyCollection DeviceRange6000_OTAA_FullLoad { get; private set; } public IReadOnlyCollection DeviceRange9000_OTAA_FullLoad_DuplicationDrop { get; private set; } + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + LoRaAPIHelper.Initialize(Configuration.FunctionAppCode, Configuration.FunctionAppBaseUrl); + } + public override void SetupTestDevices() { var gatewayID = Environment.GetEnvironmentVariable("IOTEDGE_DEVICEID") ?? Configuration.LeafDeviceGatewayID; @@ -72,6 +85,31 @@ public override void SetupTestDevices() SensorDecoder = "DecoderValueSensor", }; + // Device1003_Simulated_ABP: used for simulator + Device1003_Simulated_ABP = new TestDeviceInfo() + { + DeviceID = "0000000000001003", + Deduplication = DeduplicationMode.Drop, + SensorDecoder = "DecoderValueSensor", + IsIoTHubDevice = true, + AppSKey = GetAppSessionKey(1003), + NwkSKey = GetNetworkSessionKey(1003), + DevAddr = new DevAddr(0x00001003), + }; + + // Device1004_Simulated_ABP: used for simulator + Device1004_Simulated_ABP = new TestDeviceInfo() + { + DeviceID = "0000000000001004", + Deduplication = DeduplicationMode.Drop, + SensorDecoder = "DecoderValueSensor", + IsIoTHubDevice = true, + AppSKey = GetAppSessionKey(1004), + NwkSKey = GetNetworkSessionKey(1004), + DevAddr = new DevAddr(0x00001004), + ClassType = LoRaDeviceClassType.C + }; + var fileName = "EU863.json"; var jsonString = File.ReadAllText(fileName); @@ -121,7 +159,7 @@ public override void SetupTestDevices() DevAddr = DevAddr.Parse(deviceId.ToString("00000000", CultureInfo.InvariantCulture)), }; - TestDeviceInfo CreateOtaaDevice(int deviceId, DeduplicationMode deduplicationMode = DeduplicationMode.None) => + TestDeviceInfo CreateOtaaDevice(int deviceId, DeduplicationMode deduplicationMode = DeduplicationMode.Drop) => new TestDeviceInfo { DeviceID = deviceId.ToString("0000000000000000", CultureInfo.InvariantCulture), diff --git a/Tests/Simulation/SimulatedCloudTests.cs b/Tests/Simulation/SimulatedCloudTests.cs new file mode 100644 index 0000000000..3c042257b3 --- /dev/null +++ b/Tests/Simulation/SimulatedCloudTests.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Simulation +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using LoRaTools.CommonAPI; + using LoRaWan.Tests.Common; + using Newtonsoft.Json; + using Xunit; + using Xunit.Abstractions; + using static MoreLinq.Extensions.RepeatExtension; + + [Trait("Category", "SkipWhenLiveUnitTesting")] +#pragma warning disable xUnit1033 // False positive: Test classes decorated with 'Xunit.IClassFixture' or 'Xunit.ICollectionFixture' should add a constructor argument of type TFixture + public sealed class SimulatedCloudTests : IntegrationTestBaseSim, IAsyncLifetime +#pragma warning restore xUnit1033 // False positive: Test classes decorated with 'Xunit.IClassFixture' or 'Xunit.ICollectionFixture' should add a constructor argument of type TFixture + { + private readonly List simulatedBasicsStations; + /// + /// A unique upstream message fragment is used for each uplink message to ensure + /// that there is no interference between test runs. + /// + private readonly string uniqueMessageFragment; + private readonly TestOutputLogger logger; + + public TestConfiguration Configuration { get; } = TestConfiguration.GetConfiguration(); + + public SimulatedCloudTests(IntegrationTestFixtureSim testFixture, ITestOutputHelper testOutputHelper) + : base(testFixture) + { + this.uniqueMessageFragment = Guid.NewGuid().ToString(); + this.logger = new TestOutputLogger(testOutputHelper); + this.simulatedBasicsStations = + testFixture.DeviceRange5000_BasicsStationSimulators + .Zip(Configuration.LnsEndpointsForSimulator.Repeat(), + (tdi, lnsNameToUrl) => new SimulatedBasicsStation(StationEui.Parse(tdi.DeviceID), lnsNameToUrl.Value)) + .ToList(); + + Assert.True(this.simulatedBasicsStations.Count % Configuration.LnsEndpointsForSimulator.Count == 0, "Since Basics Stations are round-robin distributed to LNS, we must have the same number of stations per LNS for well-defined test assertions."); + } + + [Fact] + public async Task Single_ABP_Simulated_Device_Sends_And_Receives_C2D() + { + var testDeviceInfo = TestFixtureSim.Device1004_Simulated_ABP; + LogTestStart(testDeviceInfo); + + const int messageCount = 5; + var device = new SimulatedDevice(testDeviceInfo, simulatedBasicsStation: this.simulatedBasicsStations, logger: this.logger); + + TestLogger.Log($"[INFO] Simulating send of {messageCount} messages from {device.LoRaDevice.DeviceID}"); + await SimulationUtils.SendConfirmedUpstreamMessages(device, messageCount, this.uniqueMessageFragment); + + // Now sending a c2d + var c2d = new LoRaCloudToDeviceMessage() + { + DevEUI = device.DevEUI, + MessageId = Guid.NewGuid().ToString(), + Fport = FramePorts.App23, + RawPayload = Convert.ToBase64String(new byte[] { 0xFF, 0x00 }), + }; + + TestLogger.Log($"[INFO] Using service API to send C2D message to device {device.LoRaDevice.DeviceID}"); + TestLogger.Log($"[INFO] {JsonConvert.SerializeObject(c2d, Formatting.None)}"); + + // send message using the SendCloudToDeviceMessage API endpoint + Assert.True(await LoRaAPIHelper.SendCloudToDeviceMessage(device.DevEUI, c2d)); + + var c2dLogMessage = $"{device.LoRaDevice.DeviceID}: received cloud to device message from direct method"; + TestLogger.Log($"[INFO] Searching for following log in LNS logs: '{c2dLogMessage}'"); + + var searchResults = await TestFixture.SearchNetworkServerModuleAsync( + messageBody => messageBody.StartsWith(c2dLogMessage, StringComparison.OrdinalIgnoreCase), + new SearchLogOptions(c2dLogMessage) + { + MaxAttempts = 1 + }); + + Assert.True(searchResults.Found, $"Did not find '{device.LoRaDevice.DeviceID}: C2D log: {c2dLogMessage}' in logs"); + + TestLogger.Log($"[INFO] Asserting all messages were received in IoT Hub for device {device.LoRaDevice.DeviceID}"); + await SimulationUtils.AssertIotHubMessageCountAsync(device, + messageCount, + this.uniqueMessageFragment, + this.logger, + this.simulatedBasicsStations.Count, + TestFixture.IoTHubMessages, + Configuration.LnsEndpointsForSimulator.Count); + } + + public async Task InitializeAsync() + { + await Task.WhenAll(from basicsStation in this.simulatedBasicsStations + select basicsStation.StartAsync()); + } + + public async Task DisposeAsync() + { + foreach (var basicsStation in this.simulatedBasicsStations) + { + try + { + await basicsStation.StopAndValidateAsync(); + basicsStation.Dispose(); + } + catch (Exception) + { + // Dispose all basics stations + } + } + } + } +} diff --git a/Tests/Simulation/SimulatedLoadTests.cs b/Tests/Simulation/SimulatedLoadTests.cs index 50c49995ac..1b544ba5cf 100644 --- a/Tests/Simulation/SimulatedLoadTests.cs +++ b/Tests/Simulation/SimulatedLoadTests.cs @@ -8,13 +8,9 @@ namespace LoRaWan.Tests.Simulation using System.Diagnostics; using System.Globalization; using System.Linq; - using System.Runtime.CompilerServices; using System.Security.Cryptography; - using System.Text; - using System.Text.Json; using System.Threading.Tasks; using LoRaWan.Tests.Common; - using Microsoft.Azure.EventHubs; using Microsoft.Extensions.Logging; using NetworkServer; using Xunit; @@ -24,9 +20,10 @@ namespace LoRaWan.Tests.Simulation using static MoreLinq.Extensions.TransposeExtension; [Trait("Category", "SkipWhenLiveUnitTesting")] +#pragma warning disable xUnit1033 // False positive: Test classes decorated with 'Xunit.IClassFixture' or 'Xunit.ICollectionFixture' should add a constructor argument of type TFixture public sealed class SimulatedLoadTests : IntegrationTestBaseSim, IAsyncLifetime +#pragma warning restore xUnit1033 // False positive: Test classes decorated with 'Xunit.IClassFixture' or 'Xunit.ICollectionFixture' should add a constructor argument of type TFixture { - private const double DownstreamDroppedMessagesTolerance = 0.02; private static readonly TimeSpan IntervalBetweenMessages = TimeSpan.FromSeconds(5); private readonly List simulatedBasicsStations; /// @@ -46,7 +43,7 @@ public SimulatedLoadTests(IntegrationTestFixtureSim testFixture, ITestOutputHelp this.simulatedBasicsStations = testFixture.DeviceRange5000_BasicsStationSimulators .Zip(Configuration.LnsEndpointsForSimulator.Repeat(), - (tdi, lnsUrl) => new SimulatedBasicsStation(StationEui.Parse(tdi.DeviceID), lnsUrl)) + (tdi, lnsNameToUrl) => new SimulatedBasicsStation(StationEui.Parse(tdi.DeviceID), lnsNameToUrl.Value)) .ToList(); Assert.True(this.simulatedBasicsStations.Count % Configuration.LnsEndpointsForSimulator.Count == 0, "Since Basics Stations are round-robin distributed to LNS, we must have the same number of stations per LNS for well-defined test assertions."); @@ -55,51 +52,97 @@ public SimulatedLoadTests(IntegrationTestFixtureSim testFixture, ITestOutputHelp [Fact] public async Task Five_Devices_Sending_Messages_At_Same_Time() { + var testDeviceInfo = TestFixtureSim.DeviceRange1000_ABP; + LogTestStart(testDeviceInfo); + // arrange const int messageCount = 2; - var simulatedDevices = InitializeSimulatedDevices(TestFixtureSim.DeviceRange1000_ABP); + var simulatedDevices = SimulationUtils.InitializeSimulatedDevices(testDeviceInfo, this.simulatedBasicsStations, logger); Assert.NotEmpty(simulatedDevices); // act await Task.WhenAll(from device in simulatedDevices - select SendConfirmedUpstreamMessages(device, messageCount)); + select SimulationUtils.SendConfirmedUpstreamMessages(device, messageCount, this.uniqueMessageFragment)); // assert - await AssertIotHubMessageCountsAsync(simulatedDevices, messageCount); - AssertMessageAcknowledgements(simulatedDevices, messageCount); + await SimulationUtils.AssertIotHubMessageCountsAsync(simulatedDevices, + messageCount, + this.uniqueMessageFragment, + this.logger, + this.simulatedBasicsStations.Count, + TestFixture.IoTHubMessages, + Configuration.LnsEndpointsForSimulator.Count); + SimulationUtils.AssertMessageAcknowledgements(simulatedDevices, messageCount); } [Fact] public async Task Single_ABP_Simulated_Device() { + var testDeviceInfo = TestFixtureSim.Device1001_Simulated_ABP; + LogTestStart(testDeviceInfo); + const int messageCount = 5; - var device = new SimulatedDevice(TestFixtureSim.Device1001_Simulated_ABP, simulatedBasicsStation: this.simulatedBasicsStations, logger: this.logger); + var device = new SimulatedDevice(testDeviceInfo, simulatedBasicsStation: this.simulatedBasicsStations, logger: this.logger); - await SendConfirmedUpstreamMessages(device, messageCount); + await SimulationUtils.SendConfirmedUpstreamMessages(device, messageCount, this.uniqueMessageFragment); - await AssertIotHubMessageCountAsync(device, messageCount); - AssertMessageAcknowledgement(device, messageCount); + await SimulationUtils.AssertIotHubMessageCountAsync(device, messageCount, this.uniqueMessageFragment, this.logger, this.simulatedBasicsStations.Count, TestFixture.IoTHubMessages, Configuration.LnsEndpointsForSimulator.Count); + SimulationUtils.AssertMessageAcknowledgement(device, messageCount); + } + + [Fact] + public async Task Ensures_Disconnect_Happens_For_Losing_Gateway_When_Connection_Switches() + { + // arrange + var testDeviceInfo = TestFixtureSim.Device1003_Simulated_ABP; + LogTestStart(testDeviceInfo); + + var messagesToSendEachLNS = 3; + var simulatedDevice = new SimulatedDevice(testDeviceInfo, simulatedBasicsStation: new[] { this.simulatedBasicsStations.First() }, logger: this.logger); + await SimulationUtils.SendConfirmedUpstreamMessages(simulatedDevice, messagesToSendEachLNS, this.uniqueMessageFragment); + + await Task.Delay(messagesToSendEachLNS * IntervalBetweenMessages); + _ = await TestFixture.AssertNetworkServerModuleLogExistsAsync( + x => !x.Contains(LnsRemoteCallHandler.ClosedConnectionLog, StringComparison.Ordinal), + new SearchLogOptions("No connection switch should be logged") { TreatAsError = true }); + + // act: change basics station that the device is listened from and therefore the gateway it uses as well + simulatedDevice.SimulatedBasicsStations = new[] { this.simulatedBasicsStations.Last() }; + await SimulationUtils.SendConfirmedUpstreamMessages(simulatedDevice, messagesToSendEachLNS, this.uniqueMessageFragment); + + // assert + var expectedLnsToDropConnection = Configuration.LnsEndpointsForSimulator.First().Key; + _ = await TestFixture.AssertNetworkServerModuleLogExistsAsync( + x => x.Contains(LnsRemoteCallHandler.ClosedConnectionLog, StringComparison.Ordinal) && x.Contains(expectedLnsToDropConnection, StringComparison.Ordinal), + new SearchLogOptions($"{LnsRemoteCallHandler.ClosedConnectionLog} and {expectedLnsToDropConnection}") { TreatAsError = true }); + await SimulationUtils.AssertIotHubMessageCountAsync(simulatedDevice, messagesToSendEachLNS * 2, this.uniqueMessageFragment, this.logger, this.simulatedBasicsStations.Count, TestFixture.IoTHubMessages, Configuration.LnsEndpointsForSimulator.Count); } [Fact] public async Task Single_OTAA_Simulated_Device() { + var testDeviceInfo = TestFixtureSim.Device1002_Simulated_OTAA; + LogTestStart(testDeviceInfo); + const int messageCount = 5; - var device = new SimulatedDevice(TestFixtureSim.Device1002_Simulated_OTAA, simulatedBasicsStation: this.simulatedBasicsStations, logger: this.logger); + var device = new SimulatedDevice(testDeviceInfo, simulatedBasicsStation: this.simulatedBasicsStations, logger: this.logger); Assert.True(await device.JoinAsync(), "OTAA join failed"); - await SendConfirmedUpstreamMessages(device, messageCount); + await SimulationUtils.SendConfirmedUpstreamMessages(device, messageCount, this.uniqueMessageFragment); - await AssertIotHubMessageCountAsync(device, messageCount); - AssertMessageAcknowledgement(device, messageCount + 1); + await SimulationUtils.AssertIotHubMessageCountAsync(device, messageCount, this.uniqueMessageFragment, this.logger, this.simulatedBasicsStations.Count, TestFixture.IoTHubMessages, Configuration.LnsEndpointsForSimulator.Count); + SimulationUtils.AssertMessageAcknowledgement(device, messageCount + 1); } [Fact] public async Task Lots_Of_Devices_OTAA_Simulated_Load_Test() { + var testDeviceInfo = TestFixtureSim.DeviceRange4000_OTAA_FullLoad; + LogTestStart(testDeviceInfo); + // arrange const int messageCounts = 10; - var simulatedDevices = InitializeSimulatedDevices(TestFixtureSim.DeviceRange4000_OTAA_FullLoad); + var simulatedDevices = SimulationUtils.InitializeSimulatedDevices(testDeviceInfo, this.simulatedBasicsStations, this.logger); Assert.NotEmpty(simulatedDevices); // act @@ -111,12 +154,12 @@ async Task ActAsync(SimulatedDevice device, TimeSpan startOffset) { await Task.Delay(startOffset); Assert.True(await device.JoinAsync(), "OTAA join failed"); - await SendConfirmedUpstreamMessages(device, messageCounts); + await SimulationUtils.SendConfirmedUpstreamMessages(device, messageCounts, this.uniqueMessageFragment); } // assert - await AssertIotHubMessageCountsAsync(simulatedDevices, messageCounts); - AssertMessageAcknowledgements(simulatedDevices, messageCounts + 1); + await SimulationUtils.AssertIotHubMessageCountsAsync(simulatedDevices, messageCounts, this.uniqueMessageFragment, this.logger, this.simulatedBasicsStations.Count, TestFixture.IoTHubMessages, Configuration.LnsEndpointsForSimulator.Count); + SimulationUtils.AssertMessageAcknowledgements(simulatedDevices, messageCounts + 1); } /// @@ -126,6 +169,9 @@ async Task ActAsync(SimulatedDevice device, TimeSpan startOffset) [Fact] public async Task Connected_Factory_Load_Test_Scenario() { + var testDeviceInfo = TestFixtureSim.DeviceRange9000_OTAA_FullLoad_DuplicationDrop; + LogTestStart(testDeviceInfo); + const int numberOfFactories = 2; const double joinsPerSecond = 1.5; var messagesPerSecond = MoreLinq.MoreEnumerable.Generate(1.5, old => old > 2 ? old : old + 2); @@ -142,13 +188,12 @@ public async Task Connected_Factory_Load_Test_Scenario() Assert.True(stationsPerFactory >= 1, "There needs to be at least one concentrator per factory."); Assert.True(stationsPerFactory % Configuration.LnsEndpointsForSimulator.Count == 0, "LNS must be distributed evenly across factories (identical amount of indirectly connected LNS to factories)."); - var testDeviceInfo = TestFixtureSim.DeviceRange9000_OTAA_FullLoad_DuplicationDrop; var devicesByFactory = this.simulatedBasicsStations.Chunk(stationsPerFactory) .Take(numberOfFactories) .Zip(testDeviceInfo.Chunk(testDeviceInfo.Count / numberOfFactories) .Take(numberOfFactories), - (ss, ds) => ds.Select(d => InitializeSimulatedDevice(d, ss)).ToList()); + (ss, ds) => ds.Select(d => SimulationUtils.InitializeSimulatedDevice(d, ss, this.logger)).ToList()); // Cache the devices in a flat list to make distributing requests easier. // Transposing the matrix makes sure that device requests are distributed evenly across factories, @@ -171,7 +216,7 @@ public async Task Connected_Factory_Load_Test_Scenario() await ScheduleForEachAsync(devices, Intervals(TimeSpan.FromSeconds(1) / messageRate), async d => { - using var request = CreateConfirmedUpstreamMessage(d); + using var request = SimulationUtils.CreateConfirmedUpstreamMessage(d, this.uniqueMessageFragment); await d.SendDataMessageAsync(request); }); } @@ -181,8 +226,8 @@ public async Task Connected_Factory_Load_Test_Scenario() // A correction needs to be applied since concentrators are distributed across LNS, even if they are in the same factory // (detailed description found at the beginning of this test). - await AssertIotHubMessageCountsAsync(devices, numberOfLoops, 1 / (double)numberOfFactories); - AssertMessageAcknowledgements(devices, numberOfLoops + 1); + await SimulationUtils.AssertIotHubMessageCountsAsync(devices, numberOfLoops, this.uniqueMessageFragment, this.logger, this.simulatedBasicsStations.Count, TestFixture.IoTHubMessages, Configuration.LnsEndpointsForSimulator.Count, 1 / (double)numberOfFactories); + SimulationUtils.AssertMessageAcknowledgements(devices, numberOfLoops + 1); static IEnumerable Intervals(TimeSpan step, TimeSpan? initial = null) => MoreLinq.MoreEnumerable.Generate(initial ?? TimeSpan.Zero, ts => ts + step); @@ -201,6 +246,9 @@ public async Task Connected_Factory_Load_Test_Scenario() [Fact(Skip = "Test is only used for manual load tests.")] public async Task Multiple_ABP_and_OTAA_Simulated_Devices_Confirmed() { + var testAbpDevicesInfo = TestFixtureSim.DeviceRange2000_ABP_FullLoad; + var testOtaaDevicesInfo = TestFixtureSim.DeviceRange3000_OTAA_FullLoad; + LogTestStart(testAbpDevicesInfo.Concat(testOtaaDevicesInfo)); const int messagesPerDeviceExcludingWarmup = 10; const int batchSizeDataMessages = 15; const int batchSizeWarmupMessages = 2; @@ -208,8 +256,8 @@ public async Task Multiple_ABP_and_OTAA_Simulated_Devices_Confirmed() const int messagesBeforeConfirmed = 5; var warmupDelay = TimeSpan.FromSeconds(5); - var simulatedAbpDevices = InitializeSimulatedDevices(TestFixtureSim.DeviceRange2000_ABP_FullLoad); - var simulatedOtaaDevices = InitializeSimulatedDevices(TestFixtureSim.DeviceRange3000_OTAA_FullLoad); + var simulatedAbpDevices = SimulationUtils.InitializeSimulatedDevices(testAbpDevicesInfo, this.simulatedBasicsStations, this.logger); + var simulatedOtaaDevices = SimulationUtils.InitializeSimulatedDevices(testOtaaDevicesInfo, this.simulatedBasicsStations, this.logger); Assert.Equal(simulatedAbpDevices.Count, simulatedOtaaDevices.Count); Assert.True(simulatedOtaaDevices.Count < 50, "Simulator does not work for more than 50 of each devices (due to IoT Edge connection mode). To go beyond 100 device clients, use edge hub environment variable 'MaxConnectedClients'."); Assert.True(messagesBeforeConfirmed <= messagesBeforeJoin, "OTAA devices should send all messages as confirmed messages."); @@ -270,94 +318,15 @@ static async Task JoinAsync(SimulatedDevice device) // 3. Check that the correct number of messages have arrived in IoT Hub per device // Warn only. - await AssertIotHubMessageCountsAsync(simulatedAbpDevices, messagesPerDeviceExcludingWarmup); - AssertMessageAcknowledgements(simulatedAbpDevices, messagesPerDeviceExcludingWarmup - messagesBeforeConfirmed); + await SimulationUtils.AssertIotHubMessageCountsAsync(simulatedAbpDevices, messagesPerDeviceExcludingWarmup, this.uniqueMessageFragment, this.logger, this.simulatedBasicsStations.Count, TestFixture.IoTHubMessages, Configuration.LnsEndpointsForSimulator.Count); + SimulationUtils.AssertMessageAcknowledgements(simulatedAbpDevices, messagesPerDeviceExcludingWarmup - messagesBeforeConfirmed); // number of total data messages is number of messages per device minus the join message minus the number of messages sent before the join happens. const int numberOfOtaaDataMessages = messagesPerDeviceExcludingWarmup - messagesBeforeJoin - 1; - await AssertIotHubMessageCountsAsync(simulatedOtaaDevices, numberOfOtaaDataMessages, disableWaitForIotHub: true); - AssertMessageAcknowledgements(simulatedOtaaDevices, numberOfOtaaDataMessages + 1); - } - - private static void AssertMessageAcknowledgement(SimulatedDevice device, int expectedCount) => - AssertMessageAcknowledgements(new[] { device }, expectedCount); - - private static void AssertMessageAcknowledgements(IEnumerable devices, int expectedCount) - { - if (expectedCount == 0) throw new ArgumentException(null, nameof(expectedCount)); - - foreach (var device in devices) - { - var minimumMessagesReceived = Math.Max((int)(expectedCount * (1 - DownstreamDroppedMessagesTolerance)), 1); - Assert.True(minimumMessagesReceived <= device.ReceivedMessages.Count, $"Too many downlink messages were dropped. Received {device.ReceivedMessages.Count} messages but expected at least {minimumMessagesReceived}."); - } - } - - private async Task SendConfirmedUpstreamMessages(SimulatedDevice device, int count) - { - for (var i = 0; i < count; ++i) - { - using var request = CreateConfirmedUpstreamMessage(device); - await device.SendDataMessageAsync(request); - await Task.Delay(IntervalBetweenMessages); - } + await SimulationUtils.AssertIotHubMessageCountsAsync(simulatedOtaaDevices, numberOfOtaaDataMessages, this.uniqueMessageFragment, this.logger, this.simulatedBasicsStations.Count, TestFixture.IoTHubMessages, Configuration.LnsEndpointsForSimulator.Count, disableWaitForIotHub: true); + SimulationUtils.AssertMessageAcknowledgements(simulatedOtaaDevices, numberOfOtaaDataMessages + 1); } - private WaitableLoRaRequest CreateConfirmedUpstreamMessage(SimulatedDevice simulatedDevice) => - WaitableLoRaRequest.CreateWaitableRequest(simulatedDevice.CreateConfirmedDataUpMessage(this.uniqueMessageFragment + Guid.NewGuid())); - - private Task AssertIotHubMessageCountAsync(SimulatedDevice device, int numberOfMessages) => - AssertIotHubMessageCountsAsync(new[] { device }, numberOfMessages); - - private async Task AssertIotHubMessageCountsAsync(IEnumerable devices, - int numberOfMessages, - double? correction = null, - bool disableWaitForIotHub = false) - { - // Wait for messages in IoT Hub. - if (!disableWaitForIotHub) - { - await Task.Delay(TimeSpan.FromSeconds(10)); - } - - var actualMessageCounts = new Dictionary(); - foreach (var device in devices) - { - actualMessageCounts.Add(device.DevEUI, TestFixture.IoTHubMessages.Events.Count(e => ContainsMessageFromDevice(e, device))); - } - - bool ContainsMessageFromDevice(EventData eventData, SimulatedDevice simulatedDevice) - { - if (eventData.Properties.ContainsKey("iothub-message-schema")) return false; - if (eventData.GetDeviceId() != simulatedDevice.LoRaDevice.DeviceID) return false; - return Encoding.UTF8.GetString(eventData.Body).Contains(this.uniqueMessageFragment, StringComparison.Ordinal); - } - - this.logger.LogInformation("Message counts by DevEui:"); - this.logger.LogInformation(JsonSerializer.Serialize(actualMessageCounts.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value))); - - foreach (var device in devices) - { - var expectedMessageCount = device.LoRaDevice.Deduplication switch - { - DeduplicationMode.None or DeduplicationMode.Mark => numberOfMessages * this.simulatedBasicsStations.Count, - DeduplicationMode.Drop => numberOfMessages, - var mode => throw new SwitchExpressionException(mode) - }; - - var applicableMessageCount = correction is { } someCorrection ? expectedMessageCount * someCorrection : expectedMessageCount; - var actualMessageCount = actualMessageCounts[device.DevEUI]; - // Takes into account at-least-once delivery guarantees. - Assert.True(applicableMessageCount <= actualMessageCount, $"Expected at least {applicableMessageCount} IoT Hub messages for device {device.DevEUI} but counted {actualMessageCount}."); - } - } - - private List InitializeSimulatedDevices(IReadOnlyCollection testDeviceInfos) => - testDeviceInfos.Select(d => InitializeSimulatedDevice(d, this.simulatedBasicsStations)).ToList(); - - private SimulatedDevice InitializeSimulatedDevice(TestDeviceInfo testDeviceInfo, IReadOnlyCollection simulatedBasicsStations) => - new SimulatedDevice(testDeviceInfo, simulatedBasicsStation: simulatedBasicsStations, logger: this.logger); - public async Task InitializeAsync() { await Task.WhenAll(from basicsStation in this.simulatedBasicsStations diff --git a/Tests/Simulation/SimulationUtils.cs b/Tests/Simulation/SimulationUtils.cs new file mode 100644 index 0000000000..35c79574be --- /dev/null +++ b/Tests/Simulation/SimulationUtils.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Simulation +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Runtime.CompilerServices; + using System.Text; + using System.Text.Json; + using System.Threading.Tasks; + using Azure.Messaging.EventHubs; + using LoRaWan.NetworkServer; + using LoRaWan.Tests.Common; + using Microsoft.Extensions.Logging; + using Xunit; + + internal class SimulationUtils + { + private const double DownstreamDroppedMessagesTolerance = 0.02; + + internal static void AssertMessageAcknowledgement(SimulatedDevice device, int expectedCount) => + AssertMessageAcknowledgements(new[] { device }, expectedCount); + + internal static void AssertMessageAcknowledgements(IEnumerable devices, int expectedCount) + { + if (expectedCount == 0) throw new ArgumentException(null, nameof(expectedCount)); + + foreach (var device in devices) + { + var minimumMessagesReceived = Math.Max((int)(expectedCount * (1 - DownstreamDroppedMessagesTolerance)), 1); + Assert.True(minimumMessagesReceived <= device.ReceivedMessages.Count, $"Too many downlink messages were dropped. Received {device.ReceivedMessages.Count} messages but expected at least {minimumMessagesReceived}."); + } + } + + internal static async Task SendConfirmedUpstreamMessages(SimulatedDevice device, int count, string uniqueMessageFragment, int intervalBetweenMessagesInSeconds = 5) + { + for (var i = 0; i < count; ++i) + { + using var request = CreateConfirmedUpstreamMessage(device, uniqueMessageFragment); + await device.SendDataMessageAsync(request); + await Task.Delay(TimeSpan.FromSeconds(intervalBetweenMessagesInSeconds)); + } + } + + internal static WaitableLoRaRequest CreateConfirmedUpstreamMessage(SimulatedDevice simulatedDevice, string uniqueMessageFragment) => + WaitableLoRaRequest.CreateWaitableRequest(simulatedDevice.CreateConfirmedDataUpMessage(uniqueMessageFragment + Guid.NewGuid())); + + internal static Task AssertIotHubMessageCountAsync(SimulatedDevice device, + int numberOfMessages, + string uniqueMessageFragment, + TestOutputLogger logger, + int basicStationCount, + EventHubDataCollector hubDataCollector, + int lnsEndpointsForSimulatorCount) => + AssertIotHubMessageCountsAsync(new[] { device }, + numberOfMessages, + uniqueMessageFragment, + logger, + basicStationCount, + hubDataCollector, + lnsEndpointsForSimulatorCount); + + internal static async Task AssertIotHubMessageCountsAsync(IEnumerable devices, + int numberOfMessages, + string uniqueMessageFragment, + TestOutputLogger logger, + int basicStationCount, + EventHubDataCollector hubDataCollector, + int lnsEndpointsForSimulatorCount, + double? correction = null, + bool disableWaitForIotHub = false) + { + // Wait for messages in IoT Hub. + if (!disableWaitForIotHub) + { + await Task.Delay(TimeSpan.FromSeconds(100)); + } + + var actualMessageCounts = new Dictionary(); + foreach (var device in devices) + { + actualMessageCounts.Add(device.DevEUI, hubDataCollector.Events.Count(e => ContainsMessageFromDevice(e, device))); + } + + bool ContainsMessageFromDevice(EventData eventData, SimulatedDevice simulatedDevice) + { + if (eventData.Properties.ContainsKey("iothub-message-schema")) return false; + if (eventData.GetDeviceId() != simulatedDevice.LoRaDevice.DeviceID) return false; + return Encoding.UTF8.GetString(eventData.EventBody).Contains(uniqueMessageFragment, StringComparison.Ordinal); + } + + logger.Log(LogLevel.Information, "Message counts by DevEui:"); + logger.Log(LogLevel.Information, JsonSerializer.Serialize(actualMessageCounts.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value))); + + foreach (var device in devices) + { + var expectedMessageCount = device.LoRaDevice.Deduplication switch + { + DeduplicationMode.None or DeduplicationMode.Mark => numberOfMessages * basicStationCount, + DeduplicationMode.Drop => numberOfMessages, + var mode => throw new SwitchExpressionException(mode) + }; + + if (!string.IsNullOrEmpty(device.LoRaDevice.GatewayID)) + { + expectedMessageCount /= lnsEndpointsForSimulatorCount; + } + + var applicableMessageCount = correction is { } someCorrection ? expectedMessageCount * someCorrection : expectedMessageCount; + var actualMessageCount = actualMessageCounts[device.DevEUI]; + // Takes into account at-least-once delivery guarantees. + Assert.True(applicableMessageCount <= actualMessageCount, $"Expected at least {applicableMessageCount} IoT Hub messages for device {device.DevEUI} but counted {actualMessageCount}."); + } + } + + internal static List InitializeSimulatedDevices(IReadOnlyCollection testDeviceInfos, + IReadOnlyCollection simulatedBasicsStations, + TestOutputLogger logger) => + testDeviceInfos.Select(d => InitializeSimulatedDevice(d, simulatedBasicsStations, logger)).ToList(); + + internal static SimulatedDevice InitializeSimulatedDevice(TestDeviceInfo testDeviceInfo, + IReadOnlyCollection simulatedBasicsStations, + TestOutputLogger logger) => + new SimulatedDevice(testDeviceInfo, simulatedBasicsStation: simulatedBasicsStations, logger: logger); + } +} diff --git a/Tests/Simulation/appsettings.json b/Tests/Simulation/appsettings.json index a2f427c82f..1e4afd5391 100644 --- a/Tests/Simulation/appsettings.json +++ b/Tests/Simulation/appsettings.json @@ -5,12 +5,15 @@ "IoTHubConnectionString": "#{INTEGRATIONTEST_IoTHubConnectionString}#", "NetworkServerModuleLogAssertLevel": "Error", "IoTHubAssertLevel": "Warning", - "TcpLog": false, + "TcpLog": true, + "TcpLogPort": "#{INTEGRATIONTEST_TcpLogPort}#", "CreateDevices": true, "LeafDeviceGatewayID": "#{INTEGRATIONTEST_LeafDeviceGatewayID}#", "DevicePrefix": "#{INTEGRATIONTEST_DevicePrefix}#", - "LoadTestLnsEndpointsString": "#{INTEGRATIONTEST_LoadTestLnsEndpoints}#", + "LoadTestLnsEndpoints": "#{INTEGRATIONTEST_LoadTestLnsEndpoints}#", "NumberOfLoadTestDevices": "#{INTEGRATIONTEST_NumberOfLoadTestDevices}#", - "NumberOfLoadTestConcentrators": "#{INTEGRATIONTEST_NumberOfLoadTestConcentrators}#" + "NumberOfLoadTestConcentrators": "#{INTEGRATIONTEST_NumberOfLoadTestConcentrators}#", + "FunctionAppCode": "#{INTEGRATIONTEST_FunctionAppCode}#", + "FunctionAppBaseUrl": "#{INTEGRATIONTEST_FunctionAppBaseUrl}#" } } diff --git a/Tests/Unit/IoTHubImpl/DeviceTwinExtensionsTests.cs b/Tests/Unit/IoTHubImpl/DeviceTwinExtensionsTests.cs new file mode 100644 index 0000000000..79200092d5 --- /dev/null +++ b/Tests/Unit/IoTHubImpl/DeviceTwinExtensionsTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Unit.IoTHubImpl +{ + using System; + using global::LoRaTools; + using global::LoRaTools.IoTHubImpl; + using Microsoft.Azure.Devices.Shared; + using Xunit; + + public class DeviceTwinExtensionsTests + { + [Fact] + public void ToIoTHubDeviceTwinShouldReturnTwinInstance() + { + // Arrange + var twin = new Twin(); + + var deviceTwin = new IoTHubDeviceTwin(twin); + + // Act + var result = deviceTwin.ToIoTHubDeviceTwin(); + + // Assert + Assert.Equal(twin, result); + } + + [Fact] + public void WhenDeviceTwinIsNullShouldThrowArgumentNullException() + { + // Arrange + IoTHubDeviceTwin deviceTwin = null; + + // Act + Assert.Throws(() => deviceTwin.ToIoTHubDeviceTwin()); + } + + [Fact] + public void WhenDeviceTwinIsNotIoTHubDeviceTwinShouldThrowArgumentException() + { + // Arrange + IDeviceTwin deviceTwin = new FakeIoTHubDeviceTwinTests(); + + // Act + Assert.Throws(() => deviceTwin.ToIoTHubDeviceTwin()); + } + } +} diff --git a/Tests/Unit/IoTHubImpl/FakeIoTHubDeviceTwinTests.cs b/Tests/Unit/IoTHubImpl/FakeIoTHubDeviceTwinTests.cs new file mode 100644 index 0000000000..764e8dd716 --- /dev/null +++ b/Tests/Unit/IoTHubImpl/FakeIoTHubDeviceTwinTests.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Unit.IoTHubImpl +{ + using System; + using global::LoRaTools; + using Microsoft.Azure.Devices.Shared; + + internal sealed class FakeIoTHubDeviceTwinTests : IDeviceTwin + { + public string ETag => throw new NotImplementedException(); + + public TwinProperties Properties => throw new NotImplementedException(); + + public TwinCollection Tags => throw new NotImplementedException(); + + public string DeviceId => throw new NotImplementedException(); + + public string GetGatewayID() + { + throw new NotImplementedException(); + } + + public string GetNwkSKey() + { + throw new NotImplementedException(); + } + } +} diff --git a/Tests/Unit/IoTHubImpl/IoTHubDeviceTwinPageResultTets.cs b/Tests/Unit/IoTHubImpl/IoTHubDeviceTwinPageResultTets.cs new file mode 100644 index 0000000000..a5b3b7f9df --- /dev/null +++ b/Tests/Unit/IoTHubImpl/IoTHubDeviceTwinPageResultTets.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Unit.IoTHubImpl +{ + using System.Linq; + using System.Threading.Tasks; + using global::LoRaTools.IoTHubImpl; + using Microsoft.Azure.Devices; + using Microsoft.Azure.Devices.Shared; + using Moq; + using Xunit; + + public class IoTHubDeviceTwinPageResultTets + { + [Theory] + [InlineData(false)] + [InlineData(true)] + public void HasMoreResults(bool expectedResult) + { + // Arrange + var mockQuery = new Mock(); + mockQuery.SetupGet(c => c.HasMoreResults) + .Returns(expectedResult); + + var instance = new IoTHubDeviceTwinPageResult(mockQuery.Object); + + // Assert + Assert.Equal(expectedResult, instance.HasMoreResults); + mockQuery.Verify(c => c.HasMoreResults, Times.Once); + } + + [Fact] + public async Task GetNextPageAsync() + { + // Arrange + var mockQuery = new Mock(); + mockQuery.Setup(c => c.GetNextAsTwinAsync()) + .ReturnsAsync(new[] + { + new Twin("11111"), + new Twin("22222") + }); + + var instance = new IoTHubDeviceTwinPageResult(mockQuery.Object); + + // Act + var result = await instance.GetNextPageAsync(); + + // Assert + Assert.Equal(2, result.Count()); + mockQuery.Verify(c => c.GetNextAsTwinAsync(), Times.Once); + } + } +} diff --git a/Tests/Unit/IoTHubImpl/IoTHubDeviceTwinTests.cs b/Tests/Unit/IoTHubImpl/IoTHubDeviceTwinTests.cs new file mode 100644 index 0000000000..45f868ee08 --- /dev/null +++ b/Tests/Unit/IoTHubImpl/IoTHubDeviceTwinTests.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Unit.IoTHubImpl +{ + using global::LoRaTools; + using global::LoRaTools.IoTHubImpl; + using Microsoft.Azure.Devices.Shared; + using Moq; + using Xunit; + + public class IoTHubDeviceTwinTests + { + private readonly MockRepository mockRepository; + + public IoTHubDeviceTwinTests() + { + this.mockRepository = new MockRepository(MockBehavior.Strict); + } + + [Fact] + public void GetGatewayID() + { + // Arrange + const string gatewayId = "mygatewayid"; + var twinCollection = new TwinCollection(); + twinCollection[TwinPropertiesConstants.GatewayID] = gatewayId; + + var twin = new Twin(); + twin.Properties.Desired = twinCollection; + + var ioTHubDeviceTwin = new IoTHubLoRaDeviceTwin(twin); + + // Act + var result = ioTHubDeviceTwin.GetGatewayID(); + + // Assert + Assert.Equal(gatewayId, result); + this.mockRepository.VerifyAll(); + } + + [Fact] + public void GetGatewayIDShouldReturnEmptyStringIfGatewayIdNotPresentInTwin() + { + // Arrange + var twinCollection = new TwinCollection(); + + var twin = new Twin(); + twin.Properties.Desired = twinCollection; + + var ioTHubDeviceTwin = new IoTHubLoRaDeviceTwin(twin); + + // Act + var result = ioTHubDeviceTwin.GetGatewayID(); + + // Assert + Assert.True(string.IsNullOrEmpty(result)); + this.mockRepository.VerifyAll(); + } + + [Fact] + public void EqualsShouldReturnFalseIfTwinInstancesAreNotEquals() + { + // Arrange + var twin1 = new Twin("device1"); + var device1 = new IoTHubDeviceTwin(twin1); + + var twin2 = new Twin("device2"); + var device2 = new IoTHubDeviceTwin(twin2); + + // Act + var result = device1.Equals(device2); + + // Assert + Assert.False(result); + this.mockRepository.VerifyAll(); + } + + [Fact] + public void EqualsShouldReturnTrueIfTwinInstancesAreEquals() + { + // Arrange + var twin = new Twin("device1"); + var device1 = new IoTHubDeviceTwin(twin); + var device2 = new IoTHubDeviceTwin(twin); + + // Act + var result = device1.Equals(device2); + + // Assert + Assert.True(result); + this.mockRepository.VerifyAll(); + } + + [Fact] + public void WhenSecondIsNotIoTHubDeviceTwinEqualsShouldReturnFalse() + { + // Arrange + var twin = new Twin("device1"); + var device1 = new IoTHubDeviceTwin(twin); + IDeviceTwin device2 = new FakeIoTHubDeviceTwinTests(); + + // Act + var result = device1.Equals(device2); + + // Assert + Assert.False(result); + this.mockRepository.VerifyAll(); + } + + [Fact] + public void GetHashCodeShouldRetuenTheTwinInstancesHashCode() + { + // Arrange + var twin = new Twin(); + var ioTHubDeviceTwin = new IoTHubDeviceTwin(twin); + + // Act + var result = ioTHubDeviceTwin.GetHashCode(); + + // Assert + Assert.Equal(twin.GetHashCode(), result); + this.mockRepository.VerifyAll(); + } + } +} diff --git a/Tests/Unit/IoTHubImpl/IoTHubLoRaDeviceTwinPageResultTests.cs b/Tests/Unit/IoTHubImpl/IoTHubLoRaDeviceTwinPageResultTests.cs new file mode 100644 index 0000000000..ffd680aeaf --- /dev/null +++ b/Tests/Unit/IoTHubImpl/IoTHubLoRaDeviceTwinPageResultTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Unit.IoTHubImpl +{ + using System.Linq; + using System.Threading.Tasks; + using global::LoRaTools.IoTHubImpl; + using Microsoft.Azure.Devices; + using Microsoft.Azure.Devices.Shared; + using Moq; + using Xunit; + + public class IoTHubLoRaDeviceTwinPageResultTests + { + [Theory] + [InlineData(false)] + [InlineData(true)] + public void HasMoreResults(bool expectedResult) + { + // Arrange + var mockQuery = new Mock(); + mockQuery.SetupGet(c => c.HasMoreResults) + .Returns(expectedResult); + + var instance = new IoTHubLoRaDeviceTwinPageResult(mockQuery.Object); + + // Assert + Assert.Equal(expectedResult, instance.HasMoreResults); + mockQuery.Verify(c => c.HasMoreResults, Times.Once); + } + + [Fact] + public async Task GetNextPageAsync() + { + // Arrange + var mockQuery = new Mock(); + mockQuery.Setup(c => c.GetNextAsTwinAsync()) + .ReturnsAsync(new[] + { + new Twin("11111"), + new Twin("22222") + }); + + var instance = new IoTHubLoRaDeviceTwinPageResult(mockQuery.Object); + + // Act + var result = await instance.GetNextPageAsync(); + + // Assert + Assert.Equal(2, result.Count()); + mockQuery.Verify(c => c.GetNextAsTwinAsync(), Times.Once); + } + } +} diff --git a/Tests/Unit/IoTHubImpl/IoTHubRegistryManagerTests.cs b/Tests/Unit/IoTHubImpl/IoTHubRegistryManagerTests.cs new file mode 100644 index 0000000000..f3758fe89e --- /dev/null +++ b/Tests/Unit/IoTHubImpl/IoTHubRegistryManagerTests.cs @@ -0,0 +1,805 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Unit.IoTHubImpl +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Net; + using System.Net.Http; + using System.Net.Mime; + using System.Runtime.CompilerServices; + using System.Security.Cryptography; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Azure; + using Bogus.DataSets; + using global::LoRaTools; + using global::LoRaTools.IoTHubImpl; + using LoRaWan.Tests.Common; + using Microsoft.Azure.Devices; + using Microsoft.Azure.Devices.Shared; + using Moq; + using Moq.Protected; + using Newtonsoft.Json; + using RichardSzalay.MockHttp; + using Xunit; + using Xunit.Abstractions; + + public class IoTHubRegistryManagerTests : IAsyncDisposable + { + private readonly MockRepository mockRepository; + private readonly Mock mockRegistryManager; + private readonly Mock mockHttpClientFactory; + +#pragma warning disable CA2213 // Disposable fields should be disposed (false positive) + private readonly TestOutputLoggerFactory testOutputLoggerFactory; + private readonly MockHttpMessageHandler mockHttpClientHandler; +#pragma warning restore CA2213 // Disposable fields should be disposed + private bool disposedValue; + + public IoTHubRegistryManagerTests(ITestOutputHelper testOutputHelper) + { + this.mockRepository = new MockRepository(MockBehavior.Strict); + this.mockRegistryManager = this.mockRepository.Create(); + this.mockHttpClientFactory = this.mockRepository.Create(); + + this.mockHttpClientHandler = new MockHttpMessageHandler(); + + this.testOutputLoggerFactory = new TestOutputLoggerFactory(testOutputHelper); + } + + private IoTHubRegistryManager CreateManager() + { + this.mockRegistryManager.Protected().Setup("Dispose", ItExpr.Is(_ => true)); + + return new IoTHubRegistryManager(() => this.mockRegistryManager.Object, mockHttpClientFactory.Object, testOutputLoggerFactory.CreateLogger(nameof(RegistryManager))); + } + + [Fact] + public async Task GetTwinAsync_With_CancellationToken() + { + // Arrange + using (var manager = CreateManager()) + { + var deviceId = "deviceid"; + var cancellationToken = CancellationToken.None; + var twin = new Twin(deviceId); + + this.mockRegistryManager.Setup(c => c.GetTwinAsync( + It.Is(x => x == deviceId), + It.Is(x => x == cancellationToken))) + .ReturnsAsync(twin); + + // Act + var result = await manager.GetTwinAsync(deviceId, cancellationToken); + + // Assert + Assert.Equal(twin, result.ToIoTHubDeviceTwin()); + } + + this.mockRepository.VerifyAll(); + } + + [Fact] + public async Task GetTwinAsync() + { + // Arrange + using (var manager = CreateManager()) + { + var deviceId = "deviceid"; + var twin = new Twin(deviceId); + + this.mockRegistryManager.Setup(c => c.GetTwinAsync( + It.Is(x => x == deviceId), + It.IsAny())) + .ReturnsAsync(twin); + + // Act + var result = await manager.GetTwinAsync(deviceId); + + // Assert + Assert.Equal(twin, result.ToIoTHubDeviceTwin()); + } + + this.mockRepository.VerifyAll(); + } + + [Fact] + public async Task GetLoRaDeviceTwinAsync_With_CancellationToken() + { + // Arrange + using (var manager = CreateManager()) + { + var deviceId = "deviceid"; + var cancellationToken = CancellationToken.None; + var twin = new Twin(deviceId); + + this.mockRegistryManager.Setup(c => c.GetTwinAsync( + It.Is(x => x == deviceId), + It.Is(x => x == cancellationToken))) + .ReturnsAsync(twin); + + // Act + var result = await manager.GetLoRaDeviceTwinAsync(deviceId, cancellationToken); + + // Assert + Assert.Equal(twin, result.ToIoTHubDeviceTwin()); + } + + this.mockRepository.VerifyAll(); + } + + [Fact] + public async Task GetLoRaDeviceTwinAsync() + { + // Arrange + using (var manager = CreateManager()) + { + var deviceId = "deviceid"; + var twin = new Twin(deviceId); + + this.mockRegistryManager.Setup(c => c.GetTwinAsync( + It.Is(x => x == deviceId), + It.IsAny())) + .ReturnsAsync(twin); + + // Act + var result = await manager.GetLoRaDeviceTwinAsync(deviceId); + + // Assert + Assert.Equal(twin, result.ToIoTHubDeviceTwin()); + } + + this.mockRepository.VerifyAll(); + } + + [Fact] + public async Task UpdateTwinAsync2() + { + // Arrange + using (var manager = CreateManager()) + { + var deviceId = "deviceid"; + var deviceTwin = new IoTHubDeviceTwin(new Twin()); + var eTag = "eTag"; + + this.mockRegistryManager.Setup(c => c.UpdateTwinAsync( + It.Is(x => x == deviceId), + It.Is(x => x == deviceTwin.TwinInstance), + It.Is(x => x == eTag))) + .ReturnsAsync(deviceTwin.TwinInstance); + + // Act + var result = await manager.UpdateTwinAsync(deviceId, deviceTwin, eTag); + + // Assert + Assert.Equal(deviceTwin, result); + } + + this.mockRepository.VerifyAll(); + } + + [Fact] + public async Task UpdateTwinAsync_With_Module_And_CancellationToken() + { + // Arrange + using (var manager = CreateManager()) + { + var deviceId = "deviceid"; + var moduleId = "moduleid"; + var deviceTwin = new IoTHubDeviceTwin(new Twin()); + var eTag = "eTag"; + var cancellationToken = CancellationToken.None; + + this.mockRegistryManager.Setup(c => c.UpdateTwinAsync( + It.Is(x => x == deviceId), + It.Is(x => x == moduleId), + It.Is(x => x == deviceTwin.TwinInstance), + It.Is(x => x == eTag), + It.Is(x => x == cancellationToken))) + .ReturnsAsync(deviceTwin.TwinInstance); + + // Act + var result = await manager.UpdateTwinAsync( + deviceId, + moduleId, + deviceTwin, + eTag, + cancellationToken); + + // Assert + Assert.Equal(deviceTwin, result); + } + + this.mockRepository.VerifyAll(); + } + + [Fact] + public async Task RemoveDeviceAsync() + { + // Arrange + using (var manager = CreateManager()) + { + var deviceId = "deviceid"; + + this.mockRegistryManager.Setup(c => c.RemoveDeviceAsync( + It.Is(x => x == deviceId))) + .Returns(Task.CompletedTask); + + // Act + await manager.RemoveDeviceAsync( + deviceId); + } + + // Assert + this.mockRepository.VerifyAll(); + } + + [Fact] + public void GetEdgeDevices() + { + // Arrange + using var manager = CreateManager(); + + var mockQuery = new Mock(); + var twins = new List() + { + new Twin("edgeDevice") { Capabilities = new DeviceCapabilities() { IotEdge = true }}, + }; + + mockQuery.Setup(x => x.GetNextAsTwinAsync()) + .ReturnsAsync(twins); + + mockRegistryManager + .Setup(x => x.CreateQuery(It.IsAny())) + .Returns(mockQuery.Object); + + // Act + var result = manager.GetEdgeDevices(); + + // Assert + Assert.NotNull(result); + } + + [Fact] + public void GetAllLoRaDevices() + { + // Arrange + using var manager = CreateManager(); + var mockQuery = new Mock(); + + mockRegistryManager.Setup(c => c.CreateQuery("SELECT * FROM devices WHERE is_defined(properties.desired.AppKey) OR is_defined(properties.desired.AppSKey) OR is_defined(properties.desired.NwkSKey)")) + .Returns(mockQuery.Object); + + // Act + var result = manager.GetAllLoRaDevices(); + + // Assert + Assert.NotNull(result); + } + + [Fact] + public void GetLastUpdatedLoRaDevices() + { + // Arrange + using var manager = CreateManager(); + var mockQuery = new Mock(); + + var lastUpdateDateTime = DateTime.UtcNow; + var formattedDateTime = lastUpdateDateTime.ToString(Constants.RoundTripDateTimeStringFormat, CultureInfo.InvariantCulture); + + mockRegistryManager.Setup(c => c.CreateQuery($"SELECT * FROM devices where properties.desired.$metadata.$lastUpdated >= '{formattedDateTime}' OR properties.reported.$metadata.DevAddr.$lastUpdated >= '{formattedDateTime}'")) + .Returns(mockQuery.Object); + + // Act + var result = manager.GetLastUpdatedLoRaDevices(lastUpdateDateTime); + + // Assert + Assert.NotNull(result); + } + + [Fact] + public void FindLoRaDeviceByDevAddr() + { + // Arrange + using var manager = CreateManager(); + var someDevAddr = new DevAddr(123456789); + var mockQuery = new Mock(); + + mockRegistryManager.Setup(c => c.CreateQuery($"SELECT * FROM devices WHERE properties.desired.DevAddr = '{someDevAddr}' OR properties.reported.DevAddr ='{someDevAddr}'", It.IsAny())) + .Returns(mockQuery.Object); + + // Act + var result = manager.FindLoRaDeviceByDevAddr(someDevAddr); + + // Assert + Assert.NotNull(result); + } + + [Fact] + public void FindLnsByNetworkId() + { + // Arrange + using var manager = CreateManager(); + var networkId = "aaa"; + var mockQuery = new Mock(); + + mockRegistryManager.Setup(c => c.CreateQuery($"SELECT properties.desired.hostAddress, deviceId FROM devices.modules WHERE tags.network = '{networkId}'")) + .Returns(mockQuery.Object); + + // Act + var result = manager.FindLnsByNetworkId(networkId); + + // Assert + Assert.NotNull(result); + } + + [Fact] + public void FindDeviceByDevEUI() + { + // Arrange + using var manager = CreateManager(); + var devEUI = new DevEui(123456789); + var mockQuery = new Mock(); + + mockRegistryManager.Setup(c => c.CreateQuery($"SELECT * FROM devices WHERE deviceId = '{devEUI}'", 1)) + .Returns(mockQuery.Object); + + // Act + var result = manager.FindDeviceByDevEUI(devEUI); + + // Assert + Assert.NotNull(result); + } + + [Fact] + public async Task GetDevicePrimaryKeyAsync() + { + // Arrange + using var manager = CreateManager(); + var devEUI = new DevEui(123456789); + + const string mockPrimaryKey = "YWFhYWFhYWFhYWFhYWFhYQ=="; + + mockRegistryManager.Setup(c => c.GetDeviceAsync(It.Is(devEUI.ToString(), StringComparer.OrdinalIgnoreCase))) + .ReturnsAsync((string deviceId) => new Device(deviceId) { Authentication = new AuthenticationMechanism() { SymmetricKey = new SymmetricKey() { PrimaryKey = mockPrimaryKey } } }); + + // Act + var result = await manager.GetDevicePrimaryKeyAsync(devEUI.ToString()); + + // Assert + Assert.Equal(mockPrimaryKey, result); + } + + [Fact] + public async Task AddDevice() + { + // Arrange + using var manager = CreateManager(); + var devEUI = new DevEui(123456789); + var mockTwin = new Twin(devEUI.ToString()); + + var mockDeviceTwin = new IoTHubDeviceTwin(mockTwin); + + mockRegistryManager.Setup(c => c.AddDeviceWithTwinAsync(It.Is(d => d.Id == devEUI.ToString()), It.Is(t => t == mockTwin))) + .ReturnsAsync(new BulkRegistryOperationResult + { + IsSuccessful = true + }); + + // Act + var result = await manager.AddDeviceAsync(mockDeviceTwin); + + // Assert + Assert.True(result); + } + + [Fact] + public async Task WhenBulkOperationFailed_AddDevice_Should_Return_False() + { + // Arrange + using var manager = CreateManager(); + var devEUI = new DevEui(123456789); + var mockTwin = new Twin(devEUI.ToString()); + + var mockDeviceTwin = new IoTHubDeviceTwin(mockTwin); + + mockRegistryManager.Setup(c => c.AddDeviceWithTwinAsync(It.Is(d => d.Id == devEUI.ToString()), It.Is(t => t == mockTwin))) + .ReturnsAsync(new BulkRegistryOperationResult + { + IsSuccessful = false + }); + + // Act + var result = await manager.AddDeviceAsync(mockDeviceTwin); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData("2", "1", "3", "publishUserName", "publishPassword")] + [InlineData("2", "1", "3", "fakeUser", "fakePassword", "fakeNetworkId")] + [InlineData("2", "1", "3", "fakeUser", "fakePassword", "fakeNetworkId", "ws://fakelns:5000")] + public async Task DeployEdgeDevice(string resetPin, + string spiSpeed, + string spiDev, + string publishingUserName, + string publishingPassword, + string networkId = Constants.NetworkId, + string lnsHostAddress = "ws://mylns:5000") + { + // Arrange + using var manager = CreateManager(); + ConfigurationContent configurationContent = null; + Twin networkServerModuleTwin = null; + + var deviceId = this.SetupForEdgeDeployment( + publishingUserName, + publishingPassword, + (string _, ConfigurationContent content) => configurationContent = content, + (string _, string _, Twin t, string _) => networkServerModuleTwin = t); + + // Act + await manager.DeployEdgeDeviceAsync(deviceId, resetPin, spiSpeed, spiDev, publishingUserName, publishingPassword, networkId, lnsHostAddress); + + // Assert + Assert.Equal($"{{\"modulesContent\":{{\"$edgeAgent\":{{\"properties.desired\":{{\"modules\":{{\"LoRaBasicsStationModule\":{{\"env\":{{\"RESET_PIN\":{{\"value\":\"{resetPin}\"}},\"TC_URI\":{{\"value\":\"ws://172.17.0.1:5000\"}},\"SPI_DEV\":{{\"value\":\"{spiDev}\"}},\"SPI_SPEED\":{{\"value\":\"2\"}}}}}}}}}}}}}},\"moduleContent\":{{}},\"deviceContent\":{{}}}}", JsonConvert.SerializeObject(configurationContent)); + Assert.Equal($"{{\"deviceId\":null,\"etag\":null,\"version\":null,\"tags\":{{\"network\":\"{networkId}\"}},\"properties\":{{\"desired\":{{\"FacadeServerUrl\":\"https://fake-facade.azurewebsites.net/api/\",\"FacadeAuthCode\":\"uzW4cD3VH88di5UB8kr7U8Ri\",\"hostAddress\":\"{lnsHostAddress}\"}},\"reported\":{{}}}}}}", JsonConvert.SerializeObject(networkServerModuleTwin)); + + this.mockRegistryManager.Verify(c => c.AddDeviceAsync(It.IsAny()), Times.Once); + this.mockRegistryManager.Verify(c => c.AddModuleAsync(It.IsAny()), Times.Once); + this.mockRegistryManager.Verify(c => c.ApplyConfigurationContentOnDeviceAsync(It.Is(deviceId, StringComparer.OrdinalIgnoreCase), It.IsAny()), Times.Once); + this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.Is(deviceId, StringComparer.OrdinalIgnoreCase)), Times.Once); + this.mockRegistryManager.Verify(c => c.UpdateTwinAsync( + It.Is(deviceId, StringComparer.OrdinalIgnoreCase), + It.Is("LoRaWanNetworkSrvModule", StringComparer.OrdinalIgnoreCase), + It.IsAny(), + It.IsAny()), Times.Once); + + this.mockHttpClientHandler.VerifyNoOutstandingRequest(); + this.mockHttpClientHandler.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task DeployEdgeDeviceWhenOmmitingSpiDevAndAndSpiSpeedSettingsAreNotSendToConfiguration() + { + var publishingUserName = Guid.NewGuid().ToString(); + var publishingPassword = Guid.NewGuid().ToString(); + + // Arrange + using var manager = CreateManager(); + ConfigurationContent configurationContent = null; + Twin networkServerModuleTwin = null; + + var deviceId = this.SetupForEdgeDeployment( + publishingUserName, + publishingPassword, + (string _, ConfigurationContent content) => configurationContent = content, + (string _, string _, Twin t, string _) => networkServerModuleTwin = t); + + // Act + await manager.DeployEdgeDeviceAsync(deviceId, "2", null, null, publishingUserName, publishingPassword, Constants.NetworkId, "ws://mylns:5000"); + + // Assert + Assert.Equal($"{{\"modulesContent\":{{\"$edgeAgent\":{{\"properties.desired\":{{\"modules\":{{\"LoRaBasicsStationModule\":{{\"env\":{{\"RESET_PIN\":{{\"value\":\"2\"}},\"TC_URI\":{{\"value\":\"ws://172.17.0.1:5000\"}}}}}}}}}}}}}},\"moduleContent\":{{}},\"deviceContent\":{{}}}}", JsonConvert.SerializeObject(configurationContent)); + + this.mockRegistryManager.Verify(c => c.AddDeviceAsync(It.IsAny()), Times.Once); + this.mockRegistryManager.Verify(c => c.AddModuleAsync(It.IsAny()), Times.Once); + this.mockRegistryManager.Verify(c => c.ApplyConfigurationContentOnDeviceAsync(It.Is(deviceId, StringComparer.OrdinalIgnoreCase), It.IsAny()), Times.Once); + this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.Is(deviceId, StringComparer.OrdinalIgnoreCase)), Times.Once); + this.mockRegistryManager.Verify(c => c.UpdateTwinAsync( + It.Is(deviceId, StringComparer.OrdinalIgnoreCase), + It.Is("LoRaWanNetworkSrvModule", StringComparer.OrdinalIgnoreCase), + It.IsAny(), + It.IsAny()), Times.Once); + + this.mockHttpClientHandler.VerifyNoOutstandingRequest(); + this.mockHttpClientHandler.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task DeployEdgeDeviceSettingLogAnalyticsWorkspaceShouldDeployIotHubMetricsCollectorModule() + { + var publishingUserName = Guid.NewGuid().ToString(); + var publishingPassword = Guid.NewGuid().ToString(); + + // Arrange + using var manager = CreateManager(); + ConfigurationContent configurationContent = null; + Configuration iotHubMetricsCollectorModuleConfiguration = null; + + Twin networkServerModuleTwin = null; + + Environment.SetEnvironmentVariable("LOG_ANALYTICS_WORKSPACE_ID", "fake-workspace-id"); + Environment.SetEnvironmentVariable("IOT_HUB_RESOURCE_ID", "fake-hub-id"); + Environment.SetEnvironmentVariable("LOG_ANALYTICS_WORKSPACE_KEY", "fake-workspace-key"); + Environment.SetEnvironmentVariable("OBSERVABILITY_CONFIG_LOCATION", "https://fake.local/observabilityConfig.json"); + + var deviceId = this.SetupForEdgeDeployment( + publishingUserName, + publishingPassword, + (string _, ConfigurationContent content) => configurationContent = content, + (string _, string _, Twin t, string _) => networkServerModuleTwin = t); + + this.mockRegistryManager.Setup(c => c.AddModuleAsync(It.Is(m => m.DeviceId == deviceId && m.Id == "IotHubMetricsCollectorModule"))) + .ReturnsAsync((Module m) => m); + + this.mockRegistryManager.Setup(c => c.AddConfigurationAsync(It.Is(conf => conf.TargetCondition == $"deviceId='{deviceId}'"))) + .ReturnsAsync((Configuration c) => c) + .Callback((Configuration c) => iotHubMetricsCollectorModuleConfiguration = c); + +#pragma warning disable JSON001 // Invalid JSON pattern + _ = this.mockHttpClientHandler.When(HttpMethod.Get, "/observabilityConfig.json") + .Respond(HttpStatusCode.OK, MediaTypeNames.Application.Json, /*lang=json,strict*/ "{\"modulesContent\":{\"$edgeAgent\":{\"properties.desired.modules.IotHubMetricsCollectorModule\":{\"settings\":{\"image\":\"mcr.microsoft.com/azureiotedge-metrics-collector:1.0\"},\"type\":\"docker\",\"env\":{\"ResourceId\":{\"value\":\"[$iot_hub_resource_id]\"},\"UploadTarget\":{\"value\":\"AzureMonitor\"},\"LogAnalyticsWorkspaceId\":{\"value\":\"[$log_analytics_workspace_id]\"},\"LogAnalyticsSharedKey\":{\"value\":\"[$log_analytics_shared_key]\"},\"MetricsEndpointsCSV\":{\"value\":\"http://edgeHub:9600/metrics,http://edgeAgent:9600/metrics\"}},\"status\":\"running\",\"restartPolicy\":\"always\",\"version\":\"1.0\"}}}}"); +#pragma warning restore JSON001 // Invalid JSON pattern + + // Act + await manager.DeployEdgeDeviceAsync(deviceId, "2", null, null, publishingUserName, publishingPassword, Constants.NetworkId, "ws://mylns:5000"); + + // Assert + Assert.Equal(/*lang=json,strict*/ "{\"modulesContent\":{\"$edgeAgent\":{\"properties.desired\":{\"modules\":{\"LoRaBasicsStationModule\":{\"env\":{\"RESET_PIN\":{\"value\":\"2\"},\"TC_URI\":{\"value\":\"ws://172.17.0.1:5000\"}}}}}}},\"moduleContent\":{},\"deviceContent\":{}}", JsonConvert.SerializeObject(configurationContent)); + Assert.Equal(/*lang=json,strict*/ "{\"modulesContent\":{\"$edgeAgent\":{\"properties.desired.modules.IotHubMetricsCollectorModule\":{\"settings\":{\"image\":\"mcr.microsoft.com/azureiotedge-metrics-collector:1.0\"},\"type\":\"docker\",\"env\":{\"ResourceId\":{\"value\":\"fake-hub-id\"},\"UploadTarget\":{\"value\":\"AzureMonitor\"},\"LogAnalyticsWorkspaceId\":{\"value\":\"fake-workspace-id\"},\"LogAnalyticsSharedKey\":{\"value\":\"fake-workspace-key\"},\"MetricsEndpointsCSV\":{\"value\":\"http://edgeHub:9600/metrics,http://edgeAgent:9600/metrics\"}},\"status\":\"running\",\"restartPolicy\":\"always\",\"version\":\"1.0\"}}},\"moduleContent\":{},\"deviceContent\":{}}", JsonConvert.SerializeObject(iotHubMetricsCollectorModuleConfiguration.Content)); + + this.mockRegistryManager.Verify(c => c.AddDeviceAsync(It.IsAny()), Times.Once); + this.mockRegistryManager.Verify(c => c.AddModuleAsync(It.IsAny()), Times.Exactly(2)); + this.mockRegistryManager.Verify(c => c.AddModuleAsync(It.Is(m => m.DeviceId == deviceId && m.Id == "IotHubMetricsCollectorModule")), Times.Once); + this.mockRegistryManager.Verify(c => c.AddConfigurationAsync(It.Is(conf => conf.TargetCondition == $"deviceId='{deviceId}'")), Times.Once); + + this.mockRegistryManager.Verify(c => c.ApplyConfigurationContentOnDeviceAsync(It.Is(deviceId, StringComparer.OrdinalIgnoreCase), It.IsAny()), Times.Once); + this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.Is(deviceId, StringComparer.OrdinalIgnoreCase)), Times.Once); + this.mockRegistryManager.Verify(c => c.UpdateTwinAsync( + It.Is(deviceId, StringComparer.OrdinalIgnoreCase), + It.Is("LoRaWanNetworkSrvModule", StringComparer.OrdinalIgnoreCase), + It.IsAny(), + It.IsAny()), Times.Once); + + this.mockHttpClientHandler.VerifyNoOutstandingRequest(); + this.mockHttpClientHandler.VerifyNoOutstandingExpectation(); + } + + [Theory] + [InlineData("EU")] + [InlineData("US")] + [InlineData("EU", "fakeNetwork")] + [InlineData("US", "fakeNetwork")] + public async Task DeployConcentrator(string region, string networkId = Constants.NetworkId) + { + // Arrange + using var manager = CreateManager(); + Environment.SetEnvironmentVariable("EU863_CONFIG_LOCATION", "https://fake.local/eu863.config.json"); + Environment.SetEnvironmentVariable("US902_CONFIG_LOCATION", "https://fake.local/us902.config.json"); + const string stationEui = "123456789"; + var eTag = $"{DateTime.Now.Ticks}"; + + this.mockHttpClientFactory.Setup(c => c.CreateClient(It.IsAny())) + .Returns(() => mockHttpClientHandler.ToHttpClient()); + + _ = region switch + { + "EU" => this.mockHttpClientHandler.When(HttpMethod.Get, "/eu863.config.json") + .Respond(HttpStatusCode.OK, MediaTypeNames.Application.Json, /*lang=json,strict*/ "{\"config\":\"EU\"}"), + "US" => this.mockHttpClientHandler.When(HttpMethod.Get, "/us902.config.json") + .Respond(HttpStatusCode.OK, MediaTypeNames.Application.Json, /*lang=json,strict*/ "{\"config\":\"US\"}"), + _ => throw new ArgumentException($"{region} is not supported."), + }; + + this.mockRegistryManager.Setup(c => c.AddDeviceAsync(It.Is(d => d.Id == stationEui))) + .ReturnsAsync((Device d) => d); + + _ = this.mockRegistryManager.Setup(c => c.GetTwinAsync(It.Is(stationEui, StringComparer.OrdinalIgnoreCase))) + .ReturnsAsync(new Twin(stationEui) + { + ETag = eTag + }); + + _ = this.mockRegistryManager.Setup(c => c.UpdateTwinAsync( + It.Is(stationEui, StringComparer.OrdinalIgnoreCase), + It.IsAny(), + It.Is(eTag, StringComparer.OrdinalIgnoreCase))) + .ReturnsAsync((string _, Twin t, string _) => t) + .Callback((string _, Twin t, string _) => + { + Assert.Equal($"{{\"config\":\"{region}\"}}", JsonConvert.SerializeObject(t.Properties.Desired["routerConfig"])); + Assert.Equal($"\"{networkId}\"", JsonConvert.SerializeObject(t.Tags[Constants.NetworkTagName])); + }); + + // Act + await manager.DeployConcentratorAsync(stationEui, region, networkId); + + // Assert + this.mockRegistryManager.Verify(c => c.AddDeviceAsync(It.IsAny()), Times.Once); + this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.Is(stationEui, StringComparer.OrdinalIgnoreCase)), Times.Once); + this.mockRegistryManager.Verify(c => c.UpdateTwinAsync( + It.Is(stationEui, StringComparer.OrdinalIgnoreCase), + It.IsAny(), + It.Is(eTag, StringComparer.OrdinalIgnoreCase)), Times.Once); + + this.mockHttpClientHandler.VerifyNoOutstandingRequest(); + this.mockHttpClientHandler.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task DeployConcentratorWithNotImplementedRegionShouldThrowSwitchExpressionException() + { + // Arrange + using var manager = CreateManager(); + + this.mockHttpClientFactory.Setup(c => c.CreateClient(It.IsAny())) + .Returns(() => mockHttpClientHandler.ToHttpClient()); + + // Act + _ = await Assert.ThrowsAsync(() => manager.DeployConcentratorAsync("123456789", "FAKE")); + } + + [Fact] + public async Task DeployEndDevicesShouldCreateEndDevices() + { + // Arrange + using var manager = CreateManager(); + + Dictionary deviceTwins = new(); + var eTag = $"{DateTime.Now.Ticks}"; + + this.mockRegistryManager.Setup(c => c.GetDeviceAsync(It.IsAny())) + .ReturnsAsync((string _) => null); + + this.mockRegistryManager.Setup(c => c.AddDeviceAsync(It.IsAny())) + .ReturnsAsync((Device d) => d); + + this.mockRegistryManager.Setup(c => c.GetTwinAsync(It.IsAny())) + .ReturnsAsync((string id) => new Twin(id) + { + ETag = eTag + }); + + this.mockRegistryManager.Setup(c => c.UpdateTwinAsync(It.IsAny(), It.IsAny(), It.Is(eTag, StringComparer.OrdinalIgnoreCase))) + .ReturnsAsync((string _, Twin t, string _) => t) + .Callback((string id, Twin t, string _) => deviceTwins.Add(id, t)); + + // Act + var result = await manager.DeployEndDevicesAsync(); + + // Assert + Assert.True(result); + var abpDevice = deviceTwins[Constants.AbpDeviceId]; + var otaaDevice = deviceTwins[Constants.OtaaDeviceId]; + + Assert.Equal(/*lang=json*/ "{\"AppEUI\":\"BE7A0000000014E2\",\"AppKey\":\"8AFE71A145B253E49C3031AD068277A1\",\"GatewayID\":\"\",\"SensorDecoder\":\"DecoderValueSensor\"}", JsonConvert.SerializeObject(otaaDevice.Properties.Desired)); + Assert.Equal(/*lang=json*/ "{\"AppSKey\":\"2B7E151628AED2A6ABF7158809CF4F3C\",\"NwkSKey\":\"3B7E151628AED2A6ABF7158809CF4F3C\",\"GatewayID\":\"\",\"DevAddr\":\"0228B1B1\",\"SensorDecoder\":\"DecoderValueSensor\"}", JsonConvert.SerializeObject(abpDevice.Properties.Desired)); + + this.mockRegistryManager.Verify(c => c.GetDeviceAsync(It.IsAny()), Times.Exactly(2)); + this.mockRegistryManager.Verify(c => c.AddDeviceAsync(It.IsAny()), Times.Exactly(2)); + this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.IsAny()), Times.Exactly(2)); + this.mockRegistryManager.Verify(c => c.UpdateTwinAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + } + + [Fact] + public async Task DeployEndDevicesShouldBeIdempotent() + { + // Arrange + using var manager = CreateManager(); + + Dictionary deviceTwins = new(); + var eTag = $"{DateTime.Now.Ticks}"; + + this.mockRegistryManager.Setup(c => c.GetDeviceAsync(It.IsAny())) + .ReturnsAsync((string id) => new Device(id)); + + this.mockRegistryManager.Setup(c => c.GetTwinAsync(It.IsAny())) + .ReturnsAsync((string id) => new Twin(id) + { + ETag = eTag + }); + + this.mockRegistryManager.Setup(c => c.UpdateTwinAsync(It.IsAny(), It.IsAny(), It.Is(eTag, StringComparer.OrdinalIgnoreCase))) + .ReturnsAsync((string _, Twin t, string _) => t) + .Callback((string id, Twin t, string _) => deviceTwins.Add(id, t)); + + // Act + var result = await manager.DeployEndDevicesAsync(); + + // Assert + Assert.True(result); + var abpDevice = deviceTwins[Constants.AbpDeviceId]; + var otaaDevice = deviceTwins[Constants.OtaaDeviceId]; + + Assert.Equal(/*lang=json*/ "{\"AppEUI\":\"BE7A0000000014E2\",\"AppKey\":\"8AFE71A145B253E49C3031AD068277A1\",\"GatewayID\":\"\",\"SensorDecoder\":\"DecoderValueSensor\"}", JsonConvert.SerializeObject(otaaDevice.Properties.Desired)); + Assert.Equal(/*lang=json*/ "{\"AppSKey\":\"2B7E151628AED2A6ABF7158809CF4F3C\",\"NwkSKey\":\"3B7E151628AED2A6ABF7158809CF4F3C\",\"GatewayID\":\"\",\"DevAddr\":\"0228B1B1\",\"SensorDecoder\":\"DecoderValueSensor\"}", JsonConvert.SerializeObject(abpDevice.Properties.Desired)); + + this.mockRegistryManager.Verify(c => c.GetDeviceAsync(It.IsAny()), Times.Exactly(2)); + this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.IsAny()), Times.Exactly(2)); + this.mockRegistryManager.Verify(c => c.UpdateTwinAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + } + + private string SetupForEdgeDeployment(string publishingUserName, string publishingPassword, + Action onApplyConfigurationContentOnDevice, + Func onUpdateLoRaWanNetworkServerModuleTwin) + { + this.mockHttpClientFactory.Setup(c => c.CreateClient(It.IsAny())) + .Returns(() => mockHttpClientHandler.ToHttpClient()); + + const string deviceId = "edgeTest"; + var eTag = $"{DateTime.Now.Ticks}"; + + Environment.SetEnvironmentVariable("FACADE_HOST_NAME", "fake-facade"); + Environment.SetEnvironmentVariable("WEBSITE_CONTENTSHARE", "fake.local"); + Environment.SetEnvironmentVariable("DEVICE_CONFIG_LOCATION", "https://fake.local/deviceConfiguration.json"); + + this.mockRegistryManager.Setup(c => c.AddDeviceAsync(It.Is(d => d.Id == deviceId && d.Capabilities.IotEdge))) + .ReturnsAsync((Device d) => d); + + this.mockRegistryManager.Setup(c => c.AddModuleAsync(It.Is(m => m.DeviceId == deviceId && m.Id == "LoRaWanNetworkSrvModule"))) + .ReturnsAsync((Module m) => m); + + _ = this.mockHttpClientHandler.When(HttpMethod.Get, "/api/functions/admin/token") + .With(c => + { + Assert.Equal("Basic", c.Headers.Authorization.Scheme); + Assert.Equal(Convert.ToBase64String(Encoding.Default.GetBytes($"{publishingUserName}:{publishingPassword}")), c.Headers.Authorization.Parameter); + + return true; + }) + .Respond(HttpStatusCode.OK, MediaTypeNames.Text.Plain, "JWT-BEARER-TOKEN"); + + _ = this.mockHttpClientHandler.When(HttpMethod.Get, "/admin/host/keys") + .With(c => + { + Assert.Equal("Bearer", c.Headers.Authorization.Scheme); + Assert.Equal("JWT-BEARER-TOKEN", c.Headers.Authorization.Parameter); + return true; + }) + .Respond(HttpStatusCode.OK, MediaTypeNames.Application.Json, /*lang=json,strict*/ "{\"keys\":[{\"name\":\"default\",\"value\":\"uzW4cD3VH88di5UB8kr7U8Ri\"},{\"name\":\"master\",\"value\":\"4bF86stCFr7ga8A7j59XEYnX\"}]}"); + +#pragma warning disable JSON001 // Invalid JSON pattern + _ = this.mockHttpClientHandler.When(HttpMethod.Get, "/deviceConfiguration.json") + .Respond(HttpStatusCode.OK, MediaTypeNames.Application.Json, /*lang=json,strict*/ "{\"modulesContent\":{\"$edgeAgent\":{\"properties.desired\":{\"modules\":{\"LoRaBasicsStationModule\":{\"env\":{\"RESET_PIN\":{\"value\":\"[$reset_pin]\"},\"TC_URI\":{\"value\":\"ws://172.17.0.1:5000\"}[$spi_dev][$spi_speed]}}}}}}}"); +#pragma warning restore JSON001 // Invalid JSON pattern + + _ = this.mockRegistryManager.Setup(c => c.ApplyConfigurationContentOnDeviceAsync(It.Is(deviceId, StringComparer.OrdinalIgnoreCase), It.IsAny())) + .Callback(onApplyConfigurationContentOnDevice) + .Returns(Task.CompletedTask); + + _ = this.mockRegistryManager.Setup(c => c.GetTwinAsync(It.Is(deviceId, StringComparer.OrdinalIgnoreCase))) + .ReturnsAsync(new Twin(deviceId) + { + ETag = eTag + }); + + _ = this.mockRegistryManager.Setup(c => c.UpdateTwinAsync( + It.Is(deviceId, StringComparer.OrdinalIgnoreCase), + It.Is("LoRaWanNetworkSrvModule", StringComparer.OrdinalIgnoreCase), + It.IsAny(), + It.Is(eTag, StringComparer.OrdinalIgnoreCase))) + .ReturnsAsync(onUpdateLoRaWanNetworkServerModuleTwin); + + return deviceId; + } + + protected virtual ValueTask DisposeAsync(bool disposing) + { + if (!this.disposedValue) + { + if (disposing) + { + this.testOutputLoggerFactory.Dispose(); + this.mockHttpClientHandler.Dispose(); + } + + this.disposedValue = true; + } + + return new ValueTask(); + } + + public async ValueTask DisposeAsync() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + await DisposeAsync(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Tests/Unit/IoTHubImpl/JsonPageResultTests.cs b/Tests/Unit/IoTHubImpl/JsonPageResultTests.cs new file mode 100644 index 0000000000..4b293835f8 --- /dev/null +++ b/Tests/Unit/IoTHubImpl/JsonPageResultTests.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Unit.IoTHubImpl +{ + using System.Linq; + using System.Threading.Tasks; + using global::LoRaTools.IoTHubImpl; + using Microsoft.Azure.Devices; + using Moq; + using Xunit; + + public class JsonPageResultTests + { + [Theory] + [InlineData(false)] + [InlineData(true)] + public void HasMoreResults(bool expectedResult) + { + // Arrange + var mockQuery = new Mock(); + mockQuery.SetupGet(c => c.HasMoreResults) + .Returns(expectedResult); + + var instance = new JsonPageResult(mockQuery.Object); + + // Assert + Assert.Equal(expectedResult, instance.HasMoreResults); + mockQuery.Verify(c => c.HasMoreResults, Times.Once); + } + + [Fact] + public async Task GetNextPageAsync() + { + // Arrange + var mockQuery = new Mock(); + mockQuery.Setup(c => c.GetNextAsJsonAsync()) + .ReturnsAsync(new[] + { + "aaa", + "bbb" + }); + + var instance = new JsonPageResult(mockQuery.Object); + + // Act + var result = await instance.GetNextPageAsync(); + + // Assert + Assert.Equal(2, result.Count()); + mockQuery.Verify(c => c.GetNextAsJsonAsync(), Times.Once); + } + } +} diff --git a/Tests/Unit/LoRaTools/ApiVersionTest.cs b/Tests/Unit/LoRaTools/ApiVersionTest.cs index f9eaecbd5a..63064ae2f4 100644 --- a/Tests/Unit/LoRaTools/ApiVersionTest.cs +++ b/Tests/Unit/LoRaTools/ApiVersionTest.cs @@ -29,10 +29,11 @@ public async Task LatestVersion_Returns_Bad_Request_If_InvalidVersion_Requested( var dummyExecContext = new ExecutionContext(); var apiCalls = new Func>[] { - (req) => new DeviceGetter(null, null).GetDevice(req, NullLogger.Instance), - (req) => Task.Run(() => new FCntCacheCheck(null).NextFCntDownInvoke(req, NullLogger.Instance)), - (req) => Task.Run(() => new FunctionBundlerFunction(Array.Empty()).FunctionBundler(req, NullLogger.Instance, string.Empty)), - (req) => new SendCloudToDeviceMessage(null, null, null, null).Run(req, string.Empty) + (req) => new DeviceGetter(null, null, NullLogger.Instance).GetDevice(req), + (req) => Task.Run(() => new FCntCacheCheck(null, NullLogger.Instance).NextFCntDownInvoke(req)), + (req) => Task.Run(() => new FunctionBundlerFunction(Array.Empty(), NullLogger.Instance).FunctionBundler(req, string.Empty)), + (req) => new SendCloudToDeviceMessage(null, null, null, null, null, null).Run(req, string.Empty, default), + (req) => new ClearLnsCache(null, null, null, NullLogger.Instance).ClearNetworkServerCache(req, default) }; foreach (var apiCall in apiCalls) diff --git a/Tests/Unit/NetworkServer/JsonHandlers/LnsDiscoveryTests.cs b/Tests/Unit/LoRaTools/DiscoveryServiceJsonTests.cs similarity index 80% rename from Tests/Unit/NetworkServer/JsonHandlers/LnsDiscoveryTests.cs rename to Tests/Unit/LoRaTools/DiscoveryServiceJsonTests.cs index 249d5572d5..644207527d 100644 --- a/Tests/Unit/NetworkServer/JsonHandlers/LnsDiscoveryTests.cs +++ b/Tests/Unit/LoRaTools/DiscoveryServiceJsonTests.cs @@ -1,16 +1,17 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace LoRaWan.Tests.Unit.NetworkServer.BasicsStation.JsonHandlers +namespace LoRaWan.Tests.Unit.LoRaTools { using System; using System.Text.Json; + using global::LoRaTools; + using global::LoRaTools.NetworkServerDiscovery; using LoRaWan; using LoRaWan.NetworkServer; - using LoRaWan.NetworkServer.BasicsStation.JsonHandlers; using Xunit; - public class LnsDiscoveryTests + public class DiscoveryServiceJsonTests { [Theory] [InlineData(@"{ ""router"": ""b827:ebff:fee1:e39a"" }")] @@ -20,7 +21,7 @@ public class LnsDiscoveryTests [InlineData(@"{ ""router"": 13269834311795860378, ""onePropAfter"": { ""value"": 123 } }")] public void ReadQuery(string json) { - var stationEui = LnsDiscovery.QueryReader.Read(json); + var stationEui = DiscoveryService.QueryReader.Read(json); Assert.Equal(new StationEui(0xb827_ebff_fee1_e39aUL), stationEui); } @@ -29,7 +30,7 @@ public void ReadQuery(string json) [InlineData(@"{}")] public void ReadQuery_Throws_OnMissingProperty(string json) { - Assert.Throws(() => _ = LnsDiscovery.QueryReader.Read(json)); + Assert.Throws(() => _ = DiscoveryService.QueryReader.Read(json)); } [Theory] @@ -38,7 +39,7 @@ public void ReadQuery_Throws_OnMissingProperty(string json) [InlineData(@"{ ""router"": true }")] public void ReadQuery_Throws_OnInvalidPropertyType(string json) { - Assert.Throws(() => _ = LnsDiscovery.QueryReader.Read(json)); + Assert.Throws(() => _ = DiscoveryService.QueryReader.Read(json)); } private const string ValidMuxs = "0000:00FF:FE00:0000"; @@ -58,7 +59,7 @@ public void Write_Succeeds(string stationId6, string muxs, string routerDataEndp : throw new JsonException(); var computed = Json.Stringify(w => - LnsDiscovery.WriteResponse(w, stationEui, muxs, new Uri(routerDataEndpoint))); + DiscoveryService.WriteResponse(w, stationEui, muxs, new Uri(routerDataEndpoint))); Assert.Equal(expected, computed); } @@ -71,7 +72,7 @@ public void Write_Fails_BecauseOfNonId6Muxs() var stationEui = new StationEui(stationId6); Assert.Throws(() => - _ = Json.Write(w => LnsDiscovery.WriteResponse(w, stationEui, muxs, new Uri(ValidUrlString)))); + _ = Json.Write(w => DiscoveryService.WriteResponse(w, stationEui, muxs, new Uri(ValidUrlString)))); } [Fact] @@ -81,7 +82,7 @@ public void Write_Fails_BecauseNullUri() var stationEui = new StationEui(stationId6); Assert.Throws(() => - Json.Write(w => LnsDiscovery.WriteResponse(w, stationEui, ValidMuxs, null))); + Json.Write(w => DiscoveryService.WriteResponse(w, stationEui, ValidMuxs, null))); } } } diff --git a/Tests/Unit/LoRaTools/DiscoveryServiceTests.cs b/Tests/Unit/LoRaTools/DiscoveryServiceTests.cs new file mode 100644 index 0000000000..1e7461a386 --- /dev/null +++ b/Tests/Unit/LoRaTools/DiscoveryServiceTests.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Unit.LoRaTools +{ + using System; + using System.Linq; + using System.Net; + using System.Net.NetworkInformation; + using System.Net.WebSockets; + using System.Text; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + using global::LoRaTools.NetworkServerDiscovery; + using LoRaWan.Tests.Common; + using Microsoft.AspNetCore.Http; + using Microsoft.Extensions.Logging.Abstractions; + using Moq; + using Xunit; + + public sealed class DiscoveryServiceTests + { + private readonly Mock lnsDiscoveryMock; + private readonly DiscoveryService subject; + + public DiscoveryServiceTests() + { + this.lnsDiscoveryMock = new Mock(); + this.subject = new DiscoveryService(this.lnsDiscoveryMock.Object, NullLogger.Instance); + } + + [Fact] + public async Task HandleDiscoveryRequestAsync_Should_Return_400_If_Request_Is_Not_WebSocket() + { + // arrange + var (httpContext, _) = SetupWebSocketConnection(notWebSocketRequest: true); + + // act + await this.subject.HandleDiscoveryRequestAsync(httpContext.Object, CancellationToken.None); + + // assert + Assert.Equal(400, httpContext.Object.Response.StatusCode); + } + + public static TheoryData HandleDiscoveryRequestAsync_Success_TheoryData() => TheoryDataFactory.From(new[] + { + (new Uri("wss://localhost:5000"), true, new Uri("wss://localhost:5000/router-data/")), + (new Uri("wss://localhost:5000"), false, new Uri("wss://localhost:5000/router-data/")), + (new Uri("ws://localhost:5000"), true, new Uri("ws://localhost:5000/router-data/")), + (new Uri("ws://localhost:5000"), false, new Uri("ws://localhost:5000/router-data/")), + (new Uri("ws://localhost:5000/"), false, new Uri("ws://localhost:5000/router-data/")), + (new Uri("ws://localhost:5000/some-path"), true, new Uri("ws://localhost:5000/some-path/router-data/")), + (new Uri("ws://localhost:5000/some-path/"), true, new Uri("ws://localhost:5000/some-path/router-data/")), + (new Uri("ws://localhost:5000/some-path/router-data"), true, new Uri("ws://localhost:5000/some-path/router-data/")), + (new Uri("ws://localhost:5000/some-path/router-data/"), true, new Uri("ws://localhost:5000/some-path/router-data/")), + (new Uri("ws://localhost:5000/some-path/ROUTER-data/"), true, new Uri("ws://localhost:5000/some-path/router-data/")), + (new Uri("ws://localhost:5000/router-data/some-path"), true, new Uri("ws://localhost:5000/router-data/some-path/router-data/")), + (new Uri("ws://localhost:5000/router-data/some-path/router-data"), true, new Uri("ws://localhost:5000/router-data/some-path/router-data/")), + }); + + [Theory] + [MemberData(nameof(HandleDiscoveryRequestAsync_Success_TheoryData))] + public async Task HandleDiscoveryRequestAsync_Success_Response(Uri lnsUri, bool isValidNic, Uri expectedLnsUri) + { + // arrange + using var cts = new CancellationTokenSource(); + var stationEui = new StationEui(ulong.MaxValue); + var (httpContextMock, webSocketMock) = SetupWebSocketConnection(); + + // setup discovery request + SetupDiscoveryRequest(webSocketMock, stationEui); + + // setup lns resolution + this.lnsDiscoveryMock.Setup(d => d.ResolveLnsAsync(It.IsAny(), cts.Token)) + .ReturnsAsync(lnsUri); + + // setup muxs info + var connectionInfoMock = new Mock(); + _ = httpContextMock.Setup(h => h.Connection).Returns(connectionInfoMock.Object); + var nic = isValidNic ? GetMostUsedNic() : null; + var ip = isValidNic ? nic?.GetIPProperties().UnicastAddresses.First().Address : new IPAddress(new byte[] { 192, 168, 1, 10 }); + _ = connectionInfoMock.SetupGet(ci => ci.LocalIpAddress).Returns(ip); + var muxs = Id6.Format(nic?.GetPhysicalAddress().Convert48To64() ?? 0, Id6.FormatOptions.FixedWidth); + + // capture sent message + var actualResponse = string.Empty; + webSocketMock.Setup(x => x.SendAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((ArraySegment message, WebSocketMessageType _, bool _, CancellationToken _) => + actualResponse = Encoding.UTF8.GetString(message)); + + // act + await this.subject.HandleDiscoveryRequestAsync(httpContextMock.Object, cts.Token); + + // assert + var expectedResponse = @$"{{""router"":""{stationEui:i}"",""muxs"":""{muxs}"",""uri"":""{new Uri(expectedLnsUri, stationEui.ToHex()).AbsoluteUri}""}}"; + Assert.Equal(expectedResponse, actualResponse); + webSocketMock.Verify(ws => ws.SendAsync(It.IsAny>(), + WebSocketMessageType.Text, + true, cts.Token), Times.Once); + } + + [Fact] + public async Task HandleDiscoveryRequestAsync_Responds_With_Error_Message() + { + // arrange + using var cts = new CancellationTokenSource(); + var (httpContextMock, webSocketMock) = SetupWebSocketConnection(); + const string errorMessage = "LBS is not registered in IoT Hub"; + + SetupDiscoveryRequest(webSocketMock, new StationEui(1)); + this.lnsDiscoveryMock.Setup(d => d.ResolveLnsAsync(It.IsAny(), cts.Token)) + .ThrowsAsync(new InvalidOperationException(errorMessage)); + + // capture sent message + var actualResponse = string.Empty; + webSocketMock.Setup(x => x.SendAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((ArraySegment message, WebSocketMessageType _, bool _, CancellationToken _) => + actualResponse = Encoding.UTF8.GetString(message)); + + // act + assert + _ = await Assert.ThrowsAsync(() => this.subject.HandleDiscoveryRequestAsync(httpContextMock.Object, cts.Token)); + Assert.Contains(errorMessage, actualResponse, StringComparison.Ordinal); + webSocketMock.Verify(ws => ws.SendAsync(It.IsAny>(), WebSocketMessageType.Text, + true, cts.Token), Times.Once); + } + + private static void SetupDiscoveryRequest(Mock webSocketMock, StationEui stationEui) + { + var discvoryMessage = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new { router = stationEui.AsUInt64 })); + webSocketMock.Setup(ws => ws.ReceiveAsync(It.IsAny>(), It.IsAny())) + .Callback((Memory destination, CancellationToken _) => discvoryMessage.CopyTo(destination)) + .ReturnsAsync(new ValueWebSocketReceiveResult(discvoryMessage.Length, WebSocketMessageType.Text, true)); + } + + /// + /// Selects the network interface with most bytes received/sent. + /// Should correspond to the real ethernet/wifi interface on the machine. + /// + internal static NetworkInterface? GetMostUsedNic() => + NetworkInterface.GetAllNetworkInterfaces() + .OrderByDescending(x => x.GetIPv4Statistics().BytesReceived + x.GetIPv4Statistics().BytesSent) + .FirstOrDefault(); + + + private static (Mock, Mock) SetupWebSocketConnection(bool notWebSocketRequest = false) + { + var socketMock = new Mock(); + + var webSocketsManager = new Mock(); + _ = webSocketsManager.SetupGet(wsm => wsm.IsWebSocketRequest).Returns(!notWebSocketRequest); + _ = webSocketsManager.Setup(wsm => wsm.AcceptWebSocketAsync()).ReturnsAsync(socketMock.Object); + + var httpContextMock = new Mock(); + _ = httpContextMock.SetupGet(h => h.WebSockets).Returns(webSocketsManager.Object); + _ = httpContextMock.SetupGet(h => h.Response).Returns(Mock.Of()); + _ = httpContextMock.SetupGet(h => h.Connection).Returns(Mock.Of()); + + return (httpContextMock, socketMock); + } + } +} diff --git a/Tests/Unit/NetworkServer/JsonReaderTest.cs b/Tests/Unit/LoRaTools/JsonReaderTest.cs similarity index 99% rename from Tests/Unit/NetworkServer/JsonReaderTest.cs rename to Tests/Unit/LoRaTools/JsonReaderTest.cs index e1f84084d4..8dda3a06a4 100644 --- a/Tests/Unit/NetworkServer/JsonReaderTest.cs +++ b/Tests/Unit/LoRaTools/JsonReaderTest.cs @@ -1,13 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace LoRaWan.Tests.Unit.NetworkServer +namespace LoRaWan.Tests.Unit.LoRaTools { using System; using System.Linq; using System.Text; using System.Text.Json; - using LoRaWan.NetworkServer; + using global::LoRaTools; using LoRaWan.Tests.Common; using Xunit; diff --git a/Tests/Unit/LoRaTools/LnsRemoteCallTests.cs b/Tests/Unit/LoRaTools/LnsRemoteCallTests.cs new file mode 100644 index 0000000000..a988b6c45e --- /dev/null +++ b/Tests/Unit/LoRaTools/LnsRemoteCallTests.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Unit.LoRaTools +{ + using System.Text.Json; + using global::LoRaTools; + using Xunit; + + public sealed class LnsRemoteCallTests + { + [Fact] + public void Serialization_And_Deserialization_Preserves_Information() + { + // arrange + var subject = new LnsRemoteCall(RemoteCallKind.CloudToDeviceMessage, "somepayload"); + + // act + var result = JsonSerializer.Deserialize(JsonSerializer.Serialize(subject)); + + // assert + Assert.Equal(subject, result); + } + } +} diff --git a/Tests/Unit/LoRaTools/LoggerExtensionsTests.cs b/Tests/Unit/LoRaTools/LoggerExtensionsTests.cs new file mode 100644 index 0000000000..1c147f6e79 --- /dev/null +++ b/Tests/Unit/LoRaTools/LoggerExtensionsTests.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Unit.LoRaTools +{ + using System; + using System.Collections.Generic; + using global::LoRaTools; + using LoRaWan.Tests.Common; + using Microsoft.Extensions.Logging; + using Moq; + using Xunit; + + public sealed class LoggerExtensionsTests + { + private readonly List> actualScopes = new(); + private readonly ILogger logger; + + public LoggerExtensionsTests() + { + var loggerMock = new Mock(); + this.logger = loggerMock.Object; + loggerMock.Setup(l => l.BeginScope(It.IsAny>())) + .Callback((Dictionary scope) => this.actualScopes.Add(scope)); + } + + public static TheoryData BeginDeviceAddressScope_TheoryData() => TheoryDataFactory.From(new (DevAddr?, string?)[] + { + (new DevAddr(1), "00000001"), + (null, null) + }); + + [Theory] + [MemberData(nameof(BeginDeviceAddressScope_TheoryData))] + public void BeginDeviceAddressScope_Success(DevAddr? devAddr, string? expectedScopeValue) => + AssertBeginScope(logger => logger.BeginDeviceAddressScope(devAddr), expectedScopeValue, "DevAddr"); + + public static TheoryData BeginDeviceScope_TheoryData() => TheoryDataFactory.From(new (DevEui?, string?)[] + { + (new DevEui(1), "0000000000000001"), + (null, null) + }); + + [Theory] + [MemberData(nameof(BeginDeviceScope_TheoryData))] + public void BeginDeviceScope_Success(DevEui? devEui, string? expectedScopeValue) => + AssertBeginScope(logger => logger.BeginDeviceScope(devEui), expectedScopeValue, "DevEUI"); + + private void AssertBeginScope(Func act, string? expectedScopeValue, string expectedKey) + { + using (var scope = act(this.logger)) { /* noop */ } + + if (expectedScopeValue is object someExpectedScopeValue) + { + var actualScopeDictionary = Assert.Single(this.actualScopes); + var actualScope = Assert.Single(actualScopeDictionary); + Assert.Equal(KeyValuePair.Create(expectedKey, someExpectedScopeValue), actualScope); + } + else + { + Assert.Empty(this.actualScopes); + } + } + + [Fact] + public void BeginEuiScope_Success() => + AssertBeginScope(logger => logger.BeginEuiScope(new StationEui(1)), "0000000000000001", "StationEUI"); + } +} diff --git a/Tests/Unit/LoRaTools/MacCommandTests.cs b/Tests/Unit/LoRaTools/MacCommandTests.cs new file mode 100644 index 0000000000..6cee36f7db --- /dev/null +++ b/Tests/Unit/LoRaTools/MacCommandTests.cs @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Unit.LoRaTools +{ + using System; + using System.Collections.Generic; + using System.Linq; + using global::LoRaTools; + using global::LoRaTools.Mac; + using global::LoRaTools.Regions; + using LoRaWan.Tests.Common; + using Newtonsoft.Json; + using Xunit; + + public abstract class MacCommandTests where T : MacCommand + { + protected MacCommandTests(Cid cid, T subject, IReadOnlyList dataBytes) + { + Cid = cid; + Subject = subject; + DataBytes = dataBytes; + } + + /// + /// Gets the CID of the MAC command. + /// + protected Cid Cid { get; } + + /// + /// Gets a default MAC command test subject. + /// + protected T Subject { get; } + + /// + /// Gets the list of data bytes (excluding the CID). + /// + protected IReadOnlyList DataBytes { get; } + + /// + /// Gets the expected length of the MAC command. + /// + protected int Length => DataBytes.Count + 1; + + [Fact] + public void ToBytes_Success() => ToBytes_Internal(Subject, DataBytes); + + protected void ToBytes_Internal(MacCommand macCommand, IReadOnlyList expectedBytes) => + Assert.Equal(GetFullBytes(expectedBytes), macCommand.ToBytes()); + + [Fact] + public void Length_Success() => Assert.Equal(Length, Subject.Length); + + [Fact] + public void Cid_Success() => Assert.Equal(Cid, Subject.Cid); + + protected void DeserializationTest(Func transform, string json) => + Assert.Equal(transform(Subject), transform(JsonConvert.DeserializeObject(JsonUtil.Strictify(json)) ?? throw new InvalidOperationException("JSON was deserialized to null."))); + + protected void FromBytesTest(Func transform, Func subject) => + Assert.Equal(transform(Subject), transform(subject(GetFullBytes(DataBytes)))); + + private byte[] GetFullBytes(IReadOnlyList dataBytes) => dataBytes.Prepend((byte)Cid).ToArray(); + } + + public sealed class TxParamSetupRequestTests : MacCommandTests + { + public TxParamSetupRequestTests() : + base(Cid.TxParamSetupCmd, + new TxParamSetupRequest(new DwellTimeSetting(false, false, 0)), + new byte[] { 0b0000_0000 }) + { } + + [Fact] + public void Init_Throws_When_Dwell_Time_Settings_Are_Empty() => + Assert.Throws(() => new TxParamSetupRequest(null!)); + + [Fact] + public void Init_Throws_When_Eirp_Is_Invalid() => + Assert.Throws(() => new TxParamSetupRequest(new DwellTimeSetting(false, false, 16))); + + public static TheoryData ToBytes_Theory_Data() => TheoryDataFactory.From(new (DwellTimeSetting, byte)[] + { + (new DwellTimeSetting(false, false, 0), 0b0000_0000), + (new DwellTimeSetting(true, false, 0), 0b0010_0000), + (new DwellTimeSetting(false, true, 0), 0b0001_0000), + (new DwellTimeSetting(false, false, 13), 0b0000_1101), + (new DwellTimeSetting(true, true, 15), 0b0011_1111), + }); + + [Theory] + [MemberData(nameof(ToBytes_Theory_Data))] + public void ToByte_Success_Cases(DwellTimeSetting dwellTimeSetting, byte expected) => + ToBytes_Internal(new TxParamSetupRequest(dwellTimeSetting), new[] { expected }); + } + + public sealed class TxParamSetupAnswerTests : MacCommandTests + { + public TxParamSetupAnswerTests() : + base(Cid.TxParamSetupCmd, + new TxParamSetupAnswer(), + Array.Empty()) + { } + } + + public sealed class DevStatusAnswerTests : MacCommandTests + { + public DevStatusAnswerTests() : + base(Cid.DevStatusCmd, + new DevStatusAnswer(1, 2), + new byte[] { 1, 2 }) + { } + + [Fact] + public void FromBytes_Success() => FromBytesTest(command => new { command.Cid, command.Battery, command.Margin }, + bytes => new DevStatusAnswer(bytes.AsSpan())); + } + + public sealed class DevStatusRequestTests : MacCommandTests + { + public DevStatusRequestTests() : + base(Cid.DevStatusCmd, + new DevStatusRequest(), + Array.Empty()) + { } + + [Fact] + public void Deserializes_Correctly() => + DeserializationTest(command => new { command.Cid }, "{cid:6}"); + } + + public sealed class DutyCycleAnswerTests : MacCommandTests + { + public DutyCycleAnswerTests() : + base(Cid.DutyCycleCmd, + new DutyCycleAnswer(), + Array.Empty()) + { } + } + + public sealed class DutyCycleRequestTests : MacCommandTests + { + public DutyCycleRequestTests() : + base(Cid.DutyCycleCmd, + new DutyCycleRequest(3), + new byte[] { 3 }) + { } + + [Fact] + public void Deserializes_Correctly() => + DeserializationTest(command => new { command.Cid, command.DutyCyclePL }, "{cid:4,dutyCyclePL:3}"); + } + + public sealed class LinkAdrAnswerTests : MacCommandTests + { + public LinkAdrAnswerTests() : + base(Cid.LinkADRCmd, + new LinkADRAnswer(1, true, false), + new byte[] { 0b110 }) + { } + + [Fact] + public void FromBytes_Success() => FromBytesTest(command => new { command.Cid, command.PowerAck, command.DRAck, command.CHMaskAck }, + bytes => new LinkADRAnswer(bytes.AsSpan())); + } + + public sealed class LinkAdrRequestTests : MacCommandTests + { + private static object Transform(LinkADRRequest command) => new + { + command.Cid, + command.DataRate, + command.TxPower, + command.ChMask, + command.ChMaskCntl, + command.NbRep + }; + + public LinkAdrRequestTests() : + base(Cid.LinkADRCmd, + new LinkADRRequest(1, 2, 3, 4, 5), + new byte[] { 0b10010, 0b11, 0, 0b1000101 }) + { } + + [Fact] + public void Deserializes_Correctly() => + DeserializationTest(Transform, "{cid:3,dataRate:1,txPower:2,chMask:3,chMaskCntl:4,nbRep:5}"); + + [Fact] + public void FromBytes_Success() => FromBytesTest(Transform, bytes => new LinkADRRequest(bytes)); + } + + public sealed class LinkCheckAnswerTests : MacCommandTests + { + public LinkCheckAnswerTests() : + base(Cid.LinkCheckCmd, + new LinkCheckAnswer(1, 2), + new byte[] { 1, 2 }) + { } + + [Fact] + public void FromBytes_Success() => FromBytesTest(command => new { command.Cid, command.Margin, command.GwCnt }, + bytes => new LinkCheckAnswer(bytes.AsSpan())); + } + + public sealed class LinkCheckRequestTests : MacCommandTests + { + public LinkCheckRequestTests() : + base(Cid.LinkCheckCmd, + new LinkCheckRequest(), + Array.Empty()) + { } + } + + public sealed class NewChannelAnswerTests : MacCommandTests + { + public NewChannelAnswerTests() : + base(Cid.NewChannelCmd, + new NewChannelAnswer(false, true), + new byte[] { 1 }) + { } + + [Fact] + public void FromBytes_Success() => FromBytesTest(command => new { command.Cid, command.DataRangeOk, command.ChannelFreqOk }, + bytes => new NewChannelAnswer(bytes.AsSpan())); + } + + public sealed class NewChannelRequestTests : MacCommandTests + { + public NewChannelRequestTests() : + base(Cid.NewChannelCmd, + new NewChannelRequest(1, 2, 3, 4), + new byte[] { 1, 2, 0, 0, 0b110100 }) + { } + + [Fact] + public void Deserializes_Correctly() => + DeserializationTest(command => new { command.Cid, command.ChIndex, command.Freq, command.MaxDR, command.MinDR }, + "{cid:7,chIndex:1,freq:2,drRange:52}"); + } + + public sealed class RxParamSetupAnswerTests : MacCommandTests + { + public RxParamSetupAnswerTests() : + base(Cid.RXParamCmd, + new RXParamSetupAnswer(true, false, true), + new byte[] { 0b101 }) + { } + + [Fact] + public void FromBytes_Success() => FromBytesTest(command => new { command.Cid, command.Rx1DROffsetAck, command.Rx2DROffsetAck, command.ChannelAck }, + bytes => new RXParamSetupAnswer(bytes.AsSpan())); + } + + public sealed class RxParamSetupRequestTests : MacCommandTests + { + public RxParamSetupRequestTests() : + base(Cid.RXParamCmd, + new RXParamSetupRequest(1, 2, 3), + new byte[] { 0b10010, 3, 0, 0 }) + { } + + [Fact] + public void Deserializes_Correctly() => + DeserializationTest(command => new { command.Cid, command.Frequency, command.RX1DROffset, command.RX2DataRate }, + "{cid:5,frequency:3,dlSettings:18}"); + } + + public sealed class RxTimingSetupAnswerTests : MacCommandTests + { + public RxTimingSetupAnswerTests() : + base(Cid.RXTimingCmd, + new RXTimingSetupAnswer(), + Array.Empty()) + { } + } + + public sealed class RxTimingSetupRequestTests : MacCommandTests + { + public RxTimingSetupRequestTests() : + base(Cid.RXTimingCmd, + new RXTimingSetupRequest(1), + new byte[] { 1 }) + { } + + [Fact] + public void Deserializes_Correctly() => + DeserializationTest(command => new { command.Cid, command.Settings }, + "{cid:8,settings:1}"); + } +} diff --git a/Tests/Unit/NetworkServer/PhysicalAddressExtensionsTests.cs b/Tests/Unit/LoRaTools/PhysicalAddressExtensionsTests.cs similarity index 92% rename from Tests/Unit/NetworkServer/PhysicalAddressExtensionsTests.cs rename to Tests/Unit/LoRaTools/PhysicalAddressExtensionsTests.cs index 4043f997cd..52b599b9cd 100644 --- a/Tests/Unit/NetworkServer/PhysicalAddressExtensionsTests.cs +++ b/Tests/Unit/LoRaTools/PhysicalAddressExtensionsTests.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace LoRaWan.Tests.Unit.NetworkServer +namespace LoRaWan.Tests.Unit.LoRaTools { using System; using System.Net.NetworkInformation; - using LoRaWan.NetworkServer; + using global::LoRaTools.NetworkServerDiscovery; using Xunit; public class PhysicalAddressExtensionsTests diff --git a/Tests/Unit/LoRaTools/RegionAS923TestData.cs b/Tests/Unit/LoRaTools/RegionAS923TestData.cs index 892f261b90..f664ce2013 100644 --- a/Tests/Unit/LoRaTools/RegionAS923TestData.cs +++ b/Tests/Unit/LoRaTools/RegionAS923TestData.cs @@ -6,160 +6,153 @@ namespace LoRaWan.Tests.Unit.LoRaTools.Regions using System.Collections.Generic; using System.Linq; using global::LoRaTools.Regions; + using LoRaWan.Tests.Common; + using Xunit; using static LoRaWan.DataRateIndex; using static LoRaWan.Metric; public static class RegionAS923TestData { - private static readonly List dataRates = new() { DR0, DR1, DR2, DR3, DR4, DR5, DR6, DR7 }; + private static readonly List DataRates = new() { DR0, DR1, DR2, DR3, DR4, DR5, DR6, DR7 }; - private static readonly List frequencies = + private static readonly List Frequencies = new List { 923_200_000, 923_400_000, 921_400_000, 916_600_000, 917_500_000 } .Select(fr => new Hertz(fr)).ToList(); - private static readonly DwellTimeLimitedRegion region; - private static readonly DwellTimeLimitedRegion regionWithDwellTime; + private static readonly DwellTimeLimitedRegion Region; + private static readonly DwellTimeLimitedRegion RegionWithDwellTime; #pragma warning disable CA1810 // Initialize reference type static fields inline (test code is not performance-sensitive) static RegionAS923TestData() #pragma warning restore CA1810 // Initialize reference type static fields inline { - region = new RegionAS923().WithFrequencyOffset(frequencies[0], frequencies[1]); - region.UseDwellTimeSetting(new DwellTimeSetting(false, false, 0)); - regionWithDwellTime = new RegionAS923().WithFrequencyOffset(frequencies[0], frequencies[1]); - regionWithDwellTime.UseDwellTimeSetting(new DwellTimeSetting(true, true, 0)); + Region = new RegionAS923().WithFrequencyOffset(Frequencies[0], Frequencies[1]); + Region.UseDwellTimeSetting(new DwellTimeSetting(false, false, 0)); + RegionWithDwellTime = new RegionAS923().WithFrequencyOffset(Frequencies[0], Frequencies[1]); + RegionWithDwellTime.UseDwellTimeSetting(new DwellTimeSetting(true, true, 0)); } - public static IEnumerable TestRegionFrequencyData() - { - foreach (var dr in dataRates) - { - foreach (var freq in frequencies) - yield return new object[] { region, freq, dr, freq }; - } - } + public static TheoryData TestRegionFrequencyData() => + TheoryDataFactory.From(from dr in DataRates + from freq in Frequencies + select (Region, freq, dr, freq)); - public static IEnumerable TestRegionDataRateData() => - new List + public static TheoryData TestRegionDataRateData() => + TheoryDataFactory.From(new[] { // No DwellTime limit - new object[] { region, 0, 0, 0 }, - new object[] { region, 1, 1, 0 }, - new object[] { region, 6, 6, 0 }, - new object[] { region, 2, 1, 1 }, - new object[] { region, 3, 1, 2 }, - new object[] { region, 4, 2, 2 }, - new object[] { region, 5, 7, 7 }, - new object[] { region, 6, 7, 6 }, - new object[] { region, 3, 4, 6 }, + (Region, DR0, DR0, 0), + (Region, DR1, DR1, 0), + (Region, DR6, DR6, 0), + (Region, DR2, DR1, 1), + (Region, DR3, DR1, 2), + (Region, DR4, DR2, 2), + (Region, DR5, DR7, 7), + (Region, DR6, DR7, 6), + (Region, DR3, DR4, 6), // With DwellTime limit - new object[] { regionWithDwellTime, 0, 2, 0 }, - new object[] { regionWithDwellTime, 1, 2, 0 }, - new object[] { regionWithDwellTime, 6, 6, 0 }, - new object[] { regionWithDwellTime, 2, 2, 1 }, - new object[] { regionWithDwellTime, 3, 2, 2 }, - new object[] { regionWithDwellTime, 4, 2, 2 }, - new object[] { regionWithDwellTime, 5, 7, 7 }, - new object[] { regionWithDwellTime, 6, 7, 6 }, - new object[] { regionWithDwellTime, 3, 4, 6 }, - }; - - public static IEnumerable TestRegionDataRateData_InvalidOffset => - new List - { - new object[] { region, 1, 8 }, - new object[] { region, 1, 9 }, - new object[] { regionWithDwellTime, 1, 10 }, - }; + (RegionWithDwellTime, DR0, DR2, 0), + (RegionWithDwellTime, DR1, DR2, 0), + (RegionWithDwellTime, DR6, DR6, 0), + (RegionWithDwellTime, DR2, DR2, 1), + (RegionWithDwellTime, DR3, DR2, 2), + (RegionWithDwellTime, DR4, DR2, 2), + (RegionWithDwellTime, DR5, DR7, 7), + (RegionWithDwellTime, DR6, DR7, 6), + (RegionWithDwellTime, DR3, DR4, 6), + }); + + public static TheoryData TestRegionDataRateData_InvalidOffset => + TheoryDataFactory.From(new[] + { + (Region, DR1, 8), + (Region, DR1, 9), + (RegionWithDwellTime, DR1, 10), + }); - public static IEnumerable TestRegionLimitData => - from x in new (Hertz Frequency, ushort DataRate)[] + public static TheoryData TestRegionLimitData => + TheoryDataFactory.From(new (Region, Hertz, DataRateIndex)[] { - (Mega(900.0), 8), - (Mega(914.5), 9), - (Mega(930.0), 10), - (Mega(928.4), 18), - (Mega(928.5), 90), - (Mega(928.2), 100), - } - select new object[] { region, x.Frequency, x.DataRate }; - - public static IEnumerable TestRegionMaxPayloadLengthData => - new List + (Region, Mega(900.0), DR8), + (Region, Mega(914.5), DR9), + (Region, Mega(930.0), DR10), + (Region, Mega(928.4), (DataRateIndex)18), + (Region, Mega(928.5), (DataRateIndex)90), + (Region, Mega(928.2), (DataRateIndex)100), + }); + + public static TheoryData TestRegionMaxPayloadLengthData => + TheoryDataFactory.From(new (Region, DataRateIndex, uint)[] { - new object[] { region, 0, 59 }, - new object[] { region, 1, 59 }, - new object[] { region, 2, 123 }, - new object[] { region, 3, 123 }, - new object[] { region, 4, 230 }, - new object[] { region, 5, 230 }, - new object[] { region, 6, 230 }, - new object[] { region, 7, 230 }, - new object[] { regionWithDwellTime, 2, 19 }, - new object[] { regionWithDwellTime, 3, 61 }, - new object[] { regionWithDwellTime, 4, 133 }, - new object[] { regionWithDwellTime, 5, 230 }, - new object[] { regionWithDwellTime, 6, 230 }, - new object[] { regionWithDwellTime, 7, 230 }, - }; - - public static IEnumerable TestDownstreamRX2FrequencyData => - from x in new (Hertz? NwkSrvRx2Freq, Hertz ExpectedFreq)[] + (Region, DR0, 59), + (Region, DR1, 59), + (Region, DR2, 123), + (Region, DR3, 123), + (Region, DR4, 230), + (Region, DR5, 230), + (Region, DR6, 230), + (Region, DR7, 230), + (RegionWithDwellTime, DR2, 19), + (RegionWithDwellTime, DR3, 61), + (RegionWithDwellTime, DR4, 133), + (RegionWithDwellTime, DR5, 230), + (RegionWithDwellTime, DR6, 230), + (RegionWithDwellTime, DR7, 230), + }); + + public static TheoryData TestDownstreamRX2FrequencyData => + TheoryDataFactory.From(new (Region, Hertz?, Hertz)[] { - (null , Mega(923.2)), - (Mega(923.4), Mega(923.4)), - (Mega(925.0), Mega(925.0)), - } - select new object[] { region, x.NwkSrvRx2Freq, x.ExpectedFreq }; - - public static IEnumerable TestDownstreamRX2DataRateData => - new List - { - new object[] { region, null, null, 2 }, - new object[] { region, null, DR2, 2 }, - new object[] { region, null, DR5, 5 }, - new object[] { region, DR3, null, 3 }, - new object[] { region, DR3, DR4, 4 }, - new object[] { region, DR2, DR3, 3 }, - new object[] { region, null, DR9, 2 }, - }; - - public static IEnumerable TestTranslateToRegionData => - new List - { - new object[] { region, LoRaRegionType.AS923 }, - }; + (Region, null , Mega(923.2)), + (Region, Mega(923.4), Mega(923.4)), + (Region, Mega(925.0), Mega(925.0)), + }); - public static IEnumerable TestTryGetJoinChannelIndexData => - from freq in new Hertz[] { Mega(923.4), Mega(928.0) } - select new object[] { region, freq, /* expected index */ -1 }; - - public static IEnumerable TestIsValidRX1DROffsetData => - new List + public static TheoryData TestDownstreamRX2DataRateData => + TheoryDataFactory.From(new (Region, DataRateIndex?, DataRateIndex?, DataRateIndex)[] + { + (Region, null, null, DR2), + (Region, null, DR2, DR2), + (Region, null, DR5, DR5), + (Region, DR3, null, DR3), + (Region, DR3, DR4, DR4), + (Region, DR2, DR3, DR3), + (Region, null, DR9, DR2), + }); + + public static TheoryData TestTranslateToRegionData => + TheoryDataFactory.From(new[] { (Region, LoRaRegionType.AS923) }); + + public static TheoryData TestTryGetJoinChannelIndexData => + TheoryDataFactory.From(from freq in new Hertz[] { Mega(923.4), Mega(928.0) } + select (Region, freq, /* expected index */ -1)); + + public static TheoryData TestIsValidRX1DROffsetData => + TheoryDataFactory.From(new[] { - new object[] { region, 0, true }, - new object[] { region, 7, true }, - new object[] { region, 8, false }, - new object[] { region, 10, false }, - }; - - public static IEnumerable TestIsDRIndexWithinAcceptableValuesData => - new List + (Region, 0, true), + (Region, 7, true), + (Region, 8, false), + (Region, 10, false), + }); + + public static TheoryData TestIsDRIndexWithinAcceptableValuesData => + TheoryDataFactory.From(new[] { - new object[] { region, DR0, true, true }, - new object[] { region, DR2, true, true }, - new object[] { region, DR7, true, true }, - new object[] { region, DR0, false, true }, - new object[] { region, DR2, false, true }, - new object[] { region, DR7, false, true }, - new object[] { region, DR9, true, false }, - new object[] { region, DR10, false, false }, - new object[] { regionWithDwellTime, DR0, false, false }, - new object[] { regionWithDwellTime, DR0, true, true }, - new object[] { regionWithDwellTime, DR1, false, false }, - new object[] { regionWithDwellTime, DR1, true, true }, - new object[] { regionWithDwellTime, DR2, false, true }, - new object[] { regionWithDwellTime, DR2, true, true } - }; + (Region, DR0, true, true), + (Region, DR2, true, true), + (Region, DR7, true, true), + (Region, DR0, false, true), + (Region, DR2, false, true), + (Region, DR7, false, true), + (Region, DR9, true, false), + (Region, DR10, false, false), + (RegionWithDwellTime, DR0, false, false), + (RegionWithDwellTime, DR0, true, true), + (RegionWithDwellTime, DR1, false, false), + (RegionWithDwellTime, DR1, true, true), + (RegionWithDwellTime, DR2, false, true), + (RegionWithDwellTime, DR2, true, true) + }); } } diff --git a/Tests/Unit/LoRaTools/RegionCN470RP1TestData.cs b/Tests/Unit/LoRaTools/RegionCN470RP1TestData.cs index 1e2b0a9d92..ef16b3ac0f 100644 --- a/Tests/Unit/LoRaTools/RegionCN470RP1TestData.cs +++ b/Tests/Unit/LoRaTools/RegionCN470RP1TestData.cs @@ -3,127 +3,122 @@ namespace LoRaWan.Tests.Unit.LoRaTools.Regions { - using System.Collections.Generic; using System.Linq; using global::LoRaTools.Regions; + using LoRaWan.Tests.Common; + using Xunit; using static LoRaWan.DataRateIndex; using static LoRaWan.Metric; public static class RegionCN470RP1TestData { - private static readonly Region region = RegionManager.CN470RP1; + private static readonly Region Region = RegionManager.CN470RP1; - public static IEnumerable TestRegionFrequencyData => - from f in new (Hertz Input, Hertz Output)[] + public static TheoryData TestRegionFrequencyData => + TheoryDataFactory.From(new (Region, Hertz, DataRateIndex, Hertz)[] { - (Mega(470.3), Mega(500.3)), - (Mega(471.5), Mega(501.5)), - (Mega(473.3), Mega(503.3)), - (Mega(475.9), Mega(505.9)), - (Mega(477.7), Mega(507.7)), - (Mega(478.1), Mega(508.1)), - (Mega(479.7), Mega(509.7)), - (Mega(479.9), Mega(500.3)), - (Mega(480.1), Mega(500.5)), - (Mega(484.1), Mega(504.5)), - (Mega(489.3), Mega(509.7)), - } - select new object[] { region, f.Input, /* data rate */ 0, f.Output }; + (Region, Mega(470.3), DR0, Mega(500.3)), + (Region, Mega(471.5), DR0, Mega(501.5)), + (Region, Mega(473.3), DR0, Mega(503.3)), + (Region, Mega(475.9), DR0, Mega(505.9)), + (Region, Mega(477.7), DR0, Mega(507.7)), + (Region, Mega(478.1), DR0, Mega(508.1)), + (Region, Mega(479.7), DR0, Mega(509.7)), + (Region, Mega(479.9), DR0, Mega(500.3)), + (Region, Mega(480.1), DR0, Mega(500.5)), + (Region, Mega(484.1), DR0, Mega(504.5)), + (Region, Mega(489.3), DR0, Mega(509.7)), + }); - public static IEnumerable TestRegionDataRateData => - new List + public static TheoryData TestRegionDataRateData => + TheoryDataFactory.From(new[] { - new object[] { region, 0, 0, 0 }, - new object[] { region, 1, 1, 0 }, - new object[] { region, 2, 2, 0 }, - new object[] { region, 5, 5, 0 }, - new object[] { region, 1, 0, 5 }, - new object[] { region, 2, 1, 1 }, - new object[] { region, 3, 1, 2 }, - new object[] { region, 3, 0, 3 }, - new object[] { region, 4, 2, 2 }, - new object[] { region, 5, 2, 3 }, - }; + (Region, DR0, DR0, 0), + (Region, DR1, DR1, 0), + (Region, DR2, DR2, 0), + (Region, DR5, DR5, 0), + (Region, DR1, DR0, 5), + (Region, DR2, DR1, 1), + (Region, DR3, DR1, 2), + (Region, DR3, DR0, 3), + (Region, DR4, DR2, 2), + (Region, DR5, DR2, 3), + }); - public static IEnumerable TestRegionDataRateData_InvalidOffset => - new List + public static TheoryData TestRegionDataRateData_InvalidOffset => + TheoryDataFactory.From(new[] { - new object[] { region, 0, 6 }, - new object[] { region, 2, 10 }, - }; + (Region, DR0, 6), + (Region, DR2, 10), + }); - public static IEnumerable TestRegionLimitData => - from x in new (Hertz Frequency, ushort DataRate)[] + public static TheoryData TestRegionLimitData => + TheoryDataFactory.From(new (Region, Hertz, DataRateIndex)[] { - (Mega(467.0), 6), - (Mega(469.9), 7), - (Mega(510.8), 20), - (Mega(512.3), 110), - } - select new object[] { region, x.Frequency, x.DataRate }; + (Region, Mega(467.0), DR6), + (Region, Mega(469.9), DR7), + (Region, Mega(510.8), (DataRateIndex)20), + (Region, Mega(512.3), (DataRateIndex)110), + }); - public static IEnumerable TestRegionMaxPayloadLengthData => - new List + public static TheoryData TestRegionMaxPayloadLengthData => + TheoryDataFactory.From(new (Region, DataRateIndex, uint)[] { - new object[] { region, 0, 59 }, - new object[] { region, 1, 59 }, - new object[] { region, 2, 59 }, - new object[] { region, 3, 123 }, - new object[] { region, 4, 230 }, - new object[] { region, 5, 230 }, - }; + (Region, DR0, 59), + (Region, DR1, 59), + (Region, DR2, 59), + (Region, DR3, 123), + (Region, DR4, 230), + (Region, DR5, 230), + }); - public static IEnumerable TestDownstreamRX2FrequencyData => - from x in new (Hertz? NwkSrvRx2Freq, Hertz ExpectedFreq)[] + public static TheoryData TestDownstreamRX2FrequencyData => + TheoryDataFactory.From(new (Region, Hertz?, Hertz)[] { - (null , Mega(505.3)), - (Mega(505.3), Mega(505.3)), - (Mega(500.3), Mega(500.3)), - (Mega(509.7), Mega(509.7)), - } - select new object[] { region, x.NwkSrvRx2Freq, x.ExpectedFreq }; + (Region, null , Mega(505.3)), + (Region, Mega(505.3), Mega(505.3)), + (Region, Mega(500.3), Mega(500.3)), + (Region, Mega(509.7), Mega(509.7)), + }); - public static IEnumerable TestDownstreamRX2DataRateData => - new List + public static TheoryData TestDownstreamRX2DataRateData => + TheoryDataFactory.From(new (Region, DataRateIndex?, DataRateIndex?, DataRateIndex)[] { - new object[] { region, null, null, DR0 }, - new object[] { region, null, DR2, DR2 }, - new object[] { region, null, DR5, DR5 }, - new object[] { region, null, DR6, DR0 }, - new object[] { region, DR4, null, DR4 }, - new object[] { region, DR4, DR5, DR5 }, - }; + (Region, null, null, DR0), + (Region, null, DR2, DR2), + (Region, null, DR5, DR5), + (Region, null, DR6, DR0), + (Region, DR4, null, DR4), + (Region, DR4, DR5, DR5), + }); - public static IEnumerable TestTranslateToRegionData => - new List - { - new object[] { region, LoRaRegionType.CN470RP1 }, - }; + public static TheoryData TestTranslateToRegionData => + TheoryDataFactory.From(new[] { (Region, LoRaRegionType.CN470RP1) }); - public static IEnumerable TestTryGetJoinChannelIndexData => - from freq in new Hertz[] { Mega(470.3), Mega(489.3), Mega(509.7) } - select new object[] { region, freq, /* expected index */ -1 }; + public static TheoryData TestTryGetJoinChannelIndexData => + TheoryDataFactory.From(from freq in new Hertz[] { Mega(470.3), Mega(489.3), Mega(509.7) } + select (Region, freq, /* expected index */ -1)); - public static IEnumerable TestIsValidRX1DROffsetData => - new List + public static TheoryData TestIsValidRX1DROffsetData => + TheoryDataFactory.From(new[] { - new object[] { region, 0, true }, - new object[] { region, 5, true }, - new object[] { region, 6, false }, - }; + (Region, 0, true), + (Region, 5, true), + (Region, 6, false), + }); - public static IEnumerable TestIsDRIndexWithinAcceptableValuesData => - new List + public static TheoryData TestIsDRIndexWithinAcceptableValuesData => + TheoryDataFactory.From(new[] { - new object[] { region, DR0, true, true }, - new object[] { region, DR2, true, true }, - new object[] { region, DR5, true, true }, - new object[] { region, DR6, true, false }, - new object[] { region, DR0, false, true }, - new object[] { region, DR2, false, true }, - new object[] { region, DR5, false, true }, - new object[] { region, DR6, false, false }, - new object[] { region, DR10, false, false }, - }; + (Region, DR0, true, true), + (Region, DR2, true, true), + (Region, DR5, true, true), + (Region, DR6, true, false), + (Region, DR0, false, true), + (Region, DR2, false, true), + (Region, DR5, false, true), + (Region, DR6, false, false), + (Region, DR10, false, false), + }); } } diff --git a/Tests/Unit/LoRaTools/RegionCN470RP2TestData.cs b/Tests/Unit/LoRaTools/RegionCN470RP2TestData.cs index 01811f0c7d..4fd8ad958f 100644 --- a/Tests/Unit/LoRaTools/RegionCN470RP2TestData.cs +++ b/Tests/Unit/LoRaTools/RegionCN470RP2TestData.cs @@ -3,186 +3,168 @@ namespace LoRaWan.Tests.Unit.LoRaTools.Regions { - using System.Collections.Generic; - using System.Linq; using global::LoRaTools.Regions; + using LoRaWan.Tests.Common; + using Xunit; using static LoRaWan.DataRateIndex; using static LoRaWan.Metric; public static class RegionCN470RP2TestData { - private static readonly Region region = RegionManager.CN470RP2; + private static readonly Region Region = RegionManager.CN470RP2; - public static IEnumerable TestRegionFrequencyData => - from p in new[] + public static TheoryData TestRegionFrequencyData => + TheoryDataFactory.From(new (Region, Hertz, DataRateIndex, Hertz, int)[] { // 20 MHz plan A - new { Frequency = new { Input = Mega(470.3), Output = Mega(483.9) }, JoinChannel = 0 }, - new { Frequency = new { Input = Mega(471.5), Output = Mega(485.1) }, JoinChannel = 1 }, - new { Frequency = new { Input = Mega(476.5), Output = Mega(490.1) }, JoinChannel = 2 }, - new { Frequency = new { Input = Mega(503.9), Output = Mega(490.7) }, JoinChannel = 3 }, - new { Frequency = new { Input = Mega(503.5), Output = Mega(490.3) }, JoinChannel = 4 }, - new { Frequency = new { Input = Mega(504.5), Output = Mega(491.3) }, JoinChannel = 5 }, - new { Frequency = new { Input = Mega(509.7), Output = Mega(496.5) }, JoinChannel = 7 }, + (Region, Mega(470.3), DR0, Mega(483.9), 0), + (Region, Mega(471.5), DR0, Mega(485.1), 1), + (Region, Mega(476.5), DR0, Mega(490.1), 2), + (Region, Mega(503.9), DR0, Mega(490.7), 3), + (Region, Mega(503.5), DR0, Mega(490.3), 4), + (Region, Mega(504.5), DR0, Mega(491.3), 5), + (Region, Mega(509.7), DR0, Mega(496.5), 7), // 20 MHz plan B - new { Frequency = new { Input = Mega(476.9), Output = Mega(476.9) }, JoinChannel = 8 }, - new { Frequency = new { Input = Mega(479.9), Output = Mega(479.9) }, JoinChannel = 8 }, - new { Frequency = new { Input = Mega(503.1), Output = Mega(503.1) }, JoinChannel = 9 }, + (Region, Mega(476.9), DR0, Mega(476.9), 8), + (Region, Mega(479.9), DR0, Mega(479.9), 8), + (Region, Mega(503.1), DR0, Mega(503.1), 9), // 26 MHz plan A - new { Frequency = new { Input = Mega(470.3), Output = Mega(490.1) }, JoinChannel = 10 }, - new { Frequency = new { Input = Mega(473.3), Output = Mega(493.1) }, JoinChannel = 11 }, - new { Frequency = new { Input = Mega(475.1), Output = Mega(490.1) }, JoinChannel = 12 }, - new { Frequency = new { Input = Mega(471.1), Output = Mega(490.9) }, JoinChannel = 14 }, + (Region, Mega(470.3), DR0, Mega(490.1), 10), + (Region, Mega(473.3), DR0, Mega(493.1), 11), + (Region, Mega(475.1), DR0, Mega(490.1), 12), + (Region, Mega(471.1), DR0, Mega(490.9), 14), // 26 MHz plan B - new { Frequency = new { Input = Mega(480.3), Output = Mega(500.1) }, JoinChannel = 15 }, - new { Frequency = new { Input = Mega(485.1), Output = Mega(500.1) }, JoinChannel = 16 }, - new { Frequency = new { Input = Mega(485.3), Output = Mega(500.3) }, JoinChannel = 17 }, - new { Frequency = new { Input = Mega(489.7), Output = Mega(504.7) }, JoinChannel = 18 }, - new { Frequency = new { Input = Mega(488.9), Output = Mega(503.9) }, JoinChannel = 19 }, - } - select new object[] { region, p.Frequency.Input, /* data rate */ 0, p.Frequency.Output, p.JoinChannel }; + (Region, Mega(480.3), DR0, Mega(500.1), 15), + (Region, Mega(485.1), DR0, Mega(500.1), 16), + (Region, Mega(485.3), DR0, Mega(500.3), 17), + (Region, Mega(489.7), DR0, Mega(504.7), 18), + (Region, Mega(488.9), DR0, Mega(503.9), 19), + }); - public static IEnumerable TestRegionDataRateData => - new List + public static TheoryData TestRegionDataRateData => + TheoryDataFactory.From(new[] { - new object[] { region, 0, 0, 0 }, - new object[] { region, 1, 1, 0 }, - new object[] { region, 2, 2, 0 }, - new object[] { region, 6, 6, 0 }, - new object[] { region, 2, 1, 1 }, - new object[] { region, 3, 1, 2 }, - new object[] { region, 4, 2, 2 }, - new object[] { region, 6, 3, 3 }, - }; + (Region, DR0, DR0, 0), + (Region, DR1, DR1, 0), + (Region, DR2, DR2, 0), + (Region, DR6, DR6, 0), + (Region, DR2, DR1, 1), + (Region, DR3, DR1, 2), + (Region, DR4, DR2, 2), + (Region, DR6, DR3, 3), + }); - public static IEnumerable TestRegionDataRateData_InvalidOffset => - new List + public static TheoryData TestRegionDataRateData_InvalidOffset => + TheoryDataFactory.From(new[] { - new object[] { region, 0, 6 }, - new object[] { region, 2, 10 }, - }; + (Region, DR0, 6), + (Region, DR2, 10), + }); - public static IEnumerable TestRegionLimitData => - from x in new (Hertz Frequency, ushort DataRate)[] + public static TheoryData TestRegionLimitData => + TheoryDataFactory.From(new (Region, Hertz, DataRateIndex, int)[] { - (Mega(470.0), 8), - (Mega(510.0), 10), - (Mega(509.8), 100), - (Mega(469.9), 110), - } - select new object[] { region, x.Frequency, x.DataRate, 0 }; + (Region, Mega(470.0), DR8, 0), + (Region, Mega(510.0), DR10, 0), + (Region, Mega(509.8), (DataRateIndex)100, 0), + (Region, Mega(469.9), (DataRateIndex)110, 0), + }); - public static IEnumerable TestRegionMaxPayloadLengthData => - new List + public static TheoryData TestRegionMaxPayloadLengthData => + TheoryDataFactory.From(new (Region, DataRateIndex, uint)[] { - new object[] { region, 1, 31 }, - new object[] { region, 2, 94 }, - new object[] { region, 3, 192 }, - new object[] { region, 4, 250 }, - new object[] { region, 5, 250 }, - new object[] { region, 6, 250 }, - new object[] { region, 7, 250 }, - }; + (Region, DR1, 31), + (Region, DR2, 94), + (Region, DR3, 192), + (Region, DR4, 250), + (Region, DR5, 250), + (Region, DR6, 250), + (Region, DR7, 250), + }); - public static IEnumerable TestDownstreamRX2FrequencyData => - from x in new[] - { - // OTAA devices - new { NwkSrvRx2Freq = double.NaN, ExpectedFreq = 485.3, JoinChannel = new { Reported = (int?) 0, Desired = (int?)null } }, - new { NwkSrvRx2Freq = double.NaN, ExpectedFreq = 486.9, JoinChannel = new { Reported = (int?) 1, Desired = (int?)9 } }, - new { NwkSrvRx2Freq = double.NaN, ExpectedFreq = 496.5, JoinChannel = new { Reported = (int?) 7, Desired = (int?)null } }, - new { NwkSrvRx2Freq = double.NaN, ExpectedFreq = 498.3, JoinChannel = new { Reported = (int?) 9, Desired = (int?)8 } }, - new { NwkSrvRx2Freq = double.NaN, ExpectedFreq = 492.5, JoinChannel = new { Reported = (int?)10, Desired = (int?)null } }, - new { NwkSrvRx2Freq = double.NaN, ExpectedFreq = 492.5, JoinChannel = new { Reported = (int?)12, Desired = (int?)null } }, - new { NwkSrvRx2Freq = double.NaN, ExpectedFreq = 492.5, JoinChannel = new { Reported = (int?)14, Desired = (int?)14 } }, - new { NwkSrvRx2Freq = double.NaN, ExpectedFreq = 502.5, JoinChannel = new { Reported = (int?)17, Desired = (int?)null } }, - new { NwkSrvRx2Freq = double.NaN, ExpectedFreq = 502.5, JoinChannel = new { Reported = (int?)19, Desired = (int?)18 } }, - new { NwkSrvRx2Freq = 498.3 , ExpectedFreq = 498.3, JoinChannel = new { Reported = (int?) 7, Desired = (int?)null } }, - new { NwkSrvRx2Freq = 485.3 , ExpectedFreq = 485.3, JoinChannel = new { Reported = (int?)15, Desired = (int?)null } }, - new { NwkSrvRx2Freq = 492.5 , ExpectedFreq = 492.5, JoinChannel = new { Reported = (int?)15, Desired = (int?)15 } }, - // ABP devices - new { NwkSrvRx2Freq = double.NaN, ExpectedFreq = 486.9, JoinChannel = new { Reported = (int?)null, Desired = (int?)0 } }, - new { NwkSrvRx2Freq = double.NaN, ExpectedFreq = 486.9, JoinChannel = new { Reported = (int?)null, Desired = (int?)7 } }, - new { NwkSrvRx2Freq = double.NaN, ExpectedFreq = 498.3, JoinChannel = new { Reported = (int?)null, Desired = (int?)8 } }, - new { NwkSrvRx2Freq = double.NaN, ExpectedFreq = 498.3, JoinChannel = new { Reported = (int?)null, Desired = (int?)9 } }, - new { NwkSrvRx2Freq = double.NaN, ExpectedFreq = 492.5, JoinChannel = new { Reported = (int?)null, Desired = (int?)14 } }, - new { NwkSrvRx2Freq = double.NaN, ExpectedFreq = 502.5, JoinChannel = new { Reported = (int?)null, Desired = (int?)15 } }, - new { NwkSrvRx2Freq = double.NaN, ExpectedFreq = 502.5, JoinChannel = new { Reported = (int?)null, Desired = (int?)19 } }, - new { NwkSrvRx2Freq = 486.9 , ExpectedFreq = 486.9, JoinChannel = new { Reported = (int?)null, Desired = (int?)12 } }, - new { NwkSrvRx2Freq = 502.5 , ExpectedFreq = 502.5, JoinChannel = new { Reported = (int?)null, Desired = (int?)17 } }, - } - select new object[] - { - region, - !double.IsNaN(x.NwkSrvRx2Freq) ? Hertz.Mega(x.NwkSrvRx2Freq) : (Hertz?)null, - Hertz.Mega(x.ExpectedFreq), - x.JoinChannel.Reported, - x.JoinChannel.Desired, - }; + public static TheoryData TestDownstreamRX2FrequencyData => + TheoryDataFactory.From( + new (Region, Hertz?, Hertz, int?, int?)[] + { + // OTAA devices + (Region, null, Mega(485.3), 0, null ), + (Region, null, Mega(486.9), 1, 9 ), + (Region, null, Mega(496.5), 7, null ), + (Region, null, Mega(498.3), 9, 8 ), + (Region, null, Mega(492.5), 10, null ), + (Region, null, Mega(492.5), 12, null ), + (Region, null, Mega(492.5), 14, 14 ), + (Region, null, Mega(502.5), 17, null ), + (Region, null, Mega(502.5), 19, 18 ), + (Region, Mega(498.3), Mega(498.3), 7, null ), + (Region, Mega(485.3), Mega(485.3), 15, null ), + (Region, Mega(492.5), Mega(492.5), 15, 15 ), + // ABP devices + (Region, null, Mega(486.9), null, 0 ), + (Region, null, Mega(486.9), null, 7 ), + (Region, null, Mega(498.3), null, 8 ), + (Region, null, Mega(498.3), null, 9 ), + (Region, null, Mega(492.5), null, 14 ), + (Region, null, Mega(502.5), null, 15 ), + (Region, null, Mega(502.5), null, 19 ), + (Region, Mega(486.9), Mega(486.9), null, 12 ), + (Region, Mega(502.5), Mega(502.5), null, 17 ), + }); - public static IEnumerable TestDownstreamRX2DataRateData => - new List + public static TheoryData TestDownstreamRX2DataRateData => + TheoryDataFactory.From(new (Region, DataRateIndex?, DataRateIndex?, DataRateIndex, int?, int?)[] { - new object[] { region, null, null, DR1, 0, null }, - new object[] { region, null, null, DR1, 8, null }, - new object[] { region, null, null, DR1, 10, null }, - new object[] { region, null, null, DR1, 19, null }, - new object[] { region, null, null, DR1, null, 5 }, - new object[] { region, null, null, DR1, null, 12 }, - new object[] { region, null, null, DR1, 10, 14 }, - new object[] { region, null, DR2 , DR2, 0, null }, - new object[] { region, DR3 , null, DR3, 0, null }, - new object[] { region, DR3 , DR2 , DR2, 0, null }, - new object[] { region, DR4 , DR3 , DR3, 0, 8 }, - new object[] { region, null, DR9 , DR1, 11, null }, - }; + (Region, null, null, DR1, 0, null), + (Region, null, null, DR1, 8, null), + (Region, null, null, DR1, 10, null), + (Region, null, null, DR1, 19, null), + (Region, null, null, DR1, null, 5), + (Region, null, null, DR1, null, 12), + (Region, null, null, DR1, 10, 14), + (Region, null, DR2 , DR2, 0, null), + (Region, DR3 , null, DR3, 0, null), + (Region, DR3 , DR2 , DR2, 0, null), + (Region, DR4 , DR3 , DR3, 0, 8), + (Region, null, DR9 , DR1, 11, null), + }); - public static IEnumerable TestTranslateToRegionData => - new List - { - new object[] { region, LoRaRegionType.CN470RP2 }, - }; + public static TheoryData TestTranslateToRegionData => + TheoryDataFactory.From(new[] { (Region, LoRaRegionType.CN470RP2) }); - public static IEnumerable TestTryGetJoinChannelIndexData => - from x in new[] - { - new { Freq = 470.9, ExpectedIndex = 0 }, - new { Freq = 472.5, ExpectedIndex = 1 }, - new { Freq = 475.7, ExpectedIndex = 3 }, - new { Freq = 507.3, ExpectedIndex = 6 }, - new { Freq = 479.9, ExpectedIndex = 8 }, - new { Freq = 499.9, ExpectedIndex = 9 }, - new { Freq = 478.3, ExpectedIndex = 14 }, - new { Freq = 482.3, ExpectedIndex = 16 }, - new { Freq = 486.3, ExpectedIndex = 18 }, - new { Freq = 488.3, ExpectedIndex = 19 }, - } - select new object[] + public static TheoryData TestTryGetJoinChannelIndexData => + TheoryDataFactory.From(new (Region, Hertz, int)[] { - region, - Hertz.Mega(x.Freq), - x.ExpectedIndex, - }; + (Region, Mega(470.9), 0), + (Region, Mega(472.5), 1), + (Region, Mega(475.7), 3), + (Region, Mega(507.3), 6), + (Region, Mega(479.9), 8), + (Region, Mega(499.9), 9), + (Region, Mega(478.3), 14), + (Region, Mega(482.3), 16), + (Region, Mega(486.3), 18), + (Region, Mega(488.3), 19), + }); - public static IEnumerable TestIsValidRX1DROffsetData => - new List + public static TheoryData TestIsValidRX1DROffsetData => + TheoryDataFactory.From(new[] { - new object[] { region, 0, true }, - new object[] { region, 5, true }, - new object[] { region, 6, false }, - }; + (Region, 0, true), + (Region, 5, true), + (Region, 6, false), + }); - public static IEnumerable TestIsDRIndexWithinAcceptableValuesData => - new List + public static TheoryData TestIsDRIndexWithinAcceptableValuesData => + TheoryDataFactory.From(new[] { - new object[] { region, DR0, true, true }, - new object[] { region, DR2, true, true }, - new object[] { region, DR7, true, true }, - new object[] { region, DR0, false, true }, - new object[] { region, DR2, false, true }, - new object[] { region, DR7, false, true }, - new object[] { region, DR9, true, false }, - new object[] { region, DR10, false, false }, - }; + (Region, DR0, true, true), + (Region, DR2, true, true), + (Region, DR7, true, true), + (Region, DR0, false, true), + (Region, DR2, false, true), + (Region, DR7, false, true), + (Region, DR9, true, false), + (Region, DR10, false, false), + }); } } diff --git a/Tests/Unit/LoRaTools/RegionEU868TestData.cs b/Tests/Unit/LoRaTools/RegionEU868TestData.cs index aa5bad2728..7aac5bf9a7 100644 --- a/Tests/Unit/LoRaTools/RegionEU868TestData.cs +++ b/Tests/Unit/LoRaTools/RegionEU868TestData.cs @@ -3,109 +3,103 @@ namespace LoRaWan.Tests.Unit.LoRaTools.Regions { - using System.Collections.Generic; using System.Linq; using global::LoRaTools.Regions; + using LoRaWan.Tests.Common; + using Xunit; using static LoRaWan.DataRateIndex; using static LoRaWan.Metric; public static class RegionEU868TestData { - private static readonly Region region = RegionManager.EU868; - private static readonly ushort[] dataRates = { 0, 1, 2, 3, 4, 5, 6 }; - private static readonly Hertz[] frequencies = { Mega(868.1), Mega(868.3), Mega(868.5) }; + private static readonly Region Region = RegionManager.EU868; + private static readonly DataRateIndex[] DataRates = { DR0, DR1, DR2, DR3, DR4, DR5, DR6 }; + private static readonly Hertz[] Frequencies = { Mega(868.1), Mega(868.3), Mega(868.5) }; - public static IEnumerable TestRegionFrequencyData() - { - foreach (var dr in dataRates) - { - foreach (var freq in frequencies) - yield return new object[] { region, freq, dr, freq }; - } - } + public static TheoryData TestRegionFrequencyData() => + TheoryDataFactory.From(from dr in DataRates + from freq in Frequencies + select (Region, freq, dr, freq)); - public static IEnumerable TestRegionDataRateData() { - foreach (var dr in dataRates) - yield return new object[] { region, dr, dr }; - } + public static TheoryData TestRegionDataRateData() => + TheoryDataFactory.From(from dr in DataRates + select (Region, dr, dr)); - public static IEnumerable TestRegionDataRateData_InvalidOffset => - new List - { - new object[] { region, 0, 6 }, - new object[] { region, 1, 10 }, - }; + public static TheoryData TestRegionDataRateData_InvalidOffset => + TheoryDataFactory.From(new[] + { + (Region, DR0, 6), + (Region, DR1, 10) + }); - public static IEnumerable TestRegionLimitData => - from x in new(Hertz Frequency, ushort DataRate)[] + public static TheoryData TestRegionLimitData => + TheoryDataFactory.From(new (Region, Hertz, DataRateIndex)[] { - (Mega( 800 ), 8), - (Mega(1023 ), 10), - (Mega( 862.1), 90), - (Mega( 860.3), 100), - (Mega( 880 ), 100), - } - select new object[] { region, x.Frequency, x.DataRate }; + (Region, Mega( 800 ), DR8), + (Region, Mega(1023 ), DR10), + (Region, Mega( 862.1), (DataRateIndex)90), + (Region, Mega( 860.3), (DataRateIndex)100), + (Region, Mega( 880 ), (DataRateIndex)100), + }); - public static IEnumerable TestRegionMaxPayloadLengthData => - new List - { - new object[] { region, 0, 59 }, - new object[] { region, 1, 59 }, - new object[] { region, 2, 59 }, - new object[] { region, 3, 123 }, - new object[] { region, 4, 230 }, - new object[] { region, 5, 230 }, - new object[] { region, 6, 230 }, - new object[] { region, 7, 230 }, - }; + public static TheoryData TestRegionMaxPayloadLengthData => + TheoryDataFactory.From(new (Region, DataRateIndex, uint)[] + { + (Region, DR0, 59), + (Region, DR1, 59), + (Region, DR2, 59), + (Region, DR3, 123), + (Region, DR4, 230), + (Region, DR5, 230), + (Region, DR6, 230), + (Region, DR7, 230), + }); - public static IEnumerable TestDownstreamRX2FrequencyData => - from x in new (Hertz? NwkSrvRx2Freq, Hertz ExpectedFreq)[] - { - (null , Mega(869.525)), - (Mega(868.250), Mega(868.250)), - } - select new object[] { region, x.NwkSrvRx2Freq, x.ExpectedFreq }; + public static TheoryData TestDownstreamRX2FrequencyData => + TheoryDataFactory.From(new (Region, Hertz?, Hertz)[] + { + (Region, null , Mega(869.525)), + (Region, Mega(868.250), Mega(868.250)), + }); - public static IEnumerable TestDownstreamRX2DataRateData => - new List + public static TheoryData TestDownstreamRX2DataRateData => + TheoryDataFactory.From(new (Region, DataRateIndex?, DataRateIndex?, DataRateIndex)[] { - new object[] { region, null, null, DR0 }, // Standard EU. - new object[] { region, DR3, null, DR3 }, // nwksrvDR is correctly applied if no device twins. - new object[] { region, DR3, DR6, DR6 }, // device twins are applied in priority. - }; + (Region, null, null, DR0), // Standard EU. + (Region, DR3, null, DR3), // nwksrvDR is correctly applied if no device twins. + (Region, DR3, DR6, DR6), // device twins are applied in priority. + }); - public static IEnumerable TestTranslateToRegionData => - new List + public static TheoryData TestTranslateToRegionData => + TheoryDataFactory.From(new[] { - new object[] { region, LoRaRegionType.EU868 }, - new object[] { region, LoRaRegionType.EU863 }, - }; + (Region, LoRaRegionType.EU868), + (Region, LoRaRegionType.EU863), + }); - public static IEnumerable TestTryGetJoinChannelIndexData => - from freq in new Hertz[] { Mega(863), Mega(870) } - select new object[] { region, freq, /* expected index */ -1 }; + public static TheoryData TestTryGetJoinChannelIndexData => + TheoryDataFactory.From(from freq in new Hertz[] { Mega(863), Mega(870) } + select (Region, freq, /* expected index */ -1)); - public static IEnumerable TestIsValidRX1DROffsetData => - new List + public static TheoryData TestIsValidRX1DROffsetData => + TheoryDataFactory.From(new[] { - new object[] { region, 0, true }, - new object[] { region, 5, true }, - new object[] { region, 6, false }, - }; + (Region, 0, true), + (Region, 5, true), + (Region, 6, false), + }); - public static IEnumerable TestIsDRIndexWithinAcceptableValuesData => - new List + public static TheoryData TestIsDRIndexWithinAcceptableValuesData => + TheoryDataFactory.From(new[] { - new object[] { region, DR0, true, true }, - new object[] { region, DR1, true, true }, - new object[] { region, DR3, true, true }, - new object[] { region, DR5, false, true }, - new object[] { region, DR8, true, false }, - new object[] { region, DR8, false, false }, - new object[] { region, DR10, true, false }, - new object[] { region, DR10, false, false }, - }; + (Region, DR0, true, true), + (Region, DR1, true, true), + (Region, DR3, true, true), + (Region, DR5, false, true), + (Region, DR8, true, false), + (Region, DR8, false, false), + (Region, DR10, true, false), + (Region, DR10, false, false), + }); } } diff --git a/Tests/Unit/LoRaTools/RegionUS915TestData.cs b/Tests/Unit/LoRaTools/RegionUS915TestData.cs index f8bf47560d..bc9785df2a 100644 --- a/Tests/Unit/LoRaTools/RegionUS915TestData.cs +++ b/Tests/Unit/LoRaTools/RegionUS915TestData.cs @@ -3,151 +3,140 @@ namespace LoRaWan.Tests.Unit.LoRaTools.Regions { - using System.Collections.Generic; using System.Linq; using global::LoRaTools.Regions; + using LoRaWan.Tests.Common; + using Xunit; using static LoRaWan.DataRateIndex; using static LoRaWan.Metric; public static class RegionUS915TestData { - private static readonly Region region = RegionManager.US915; - - public static readonly IEnumerable TestRegionFrequencyDataDR1To3 = - from dr in new ushort[] { 0, 1, 2, 3 } - from freq in new (double Input, double Output)[] - { - (902.3, 923.3), - (902.5, 923.9), - (902.7, 924.5), - (902.9, 925.1), - (903.1, 925.7), - (903.3, 926.3), - (903.5, 926.9), - (903.7, 927.5), - (903.9, 923.3), - (904.1, 923.9), - (904.3, 924.5), - } - select new object[] { region, Hertz.Mega(freq.Input), dr, Hertz.Mega(freq.Output) }; - - public static readonly IEnumerable TestRegionFrequencyDataDR4 = - from freq in new (Hertz Input, Hertz Output)[] - { - (Mega(903 ), Mega(923.3)), - (Mega(904.6), Mega(923.9)), - (Mega(906.2), Mega(924.5)), - (Mega(907.8), Mega(925.1)), - (Mega(909.4), Mega(925.7)), - (Mega(911 ), Mega(926.3)), - (Mega(912.6), Mega(926.9)), - (Mega(914.2), Mega(927.5)), - } - select new object[] { region, freq.Input, /* data rate */ 4, freq.Output }; - - public static IEnumerable TestRegionDataRateDataDR1To3() - { - var dataRates = new List { 0, 1, 2, 3 }; - - var inputDrToExpectedDr = new Dictionary - { - { 0, 10 }, - { 1, 11 }, - { 2, 12 }, - { 3, 13 } - }; - - foreach (var dr in dataRates) - yield return new object[] { region, dr, inputDrToExpectedDr[dr] }; - } - - public static IEnumerable TestRegionDataRateDataDR4() => - new List - { - new object[]{ region, 4, 13 } - }; - - public static IEnumerable TestRegionDataRateData_InvalidOffset => - new List + private static readonly Region Region = RegionManager.US915; + + public static readonly TheoryData TestRegionFrequencyDataDR1To3 = + TheoryDataFactory.From(from dr in new[] { DR0, DR1, DR2, DR3 } + from freq in new (double Input, double Output)[] + { + (902.3, 923.3), + (902.5, 923.9), + (902.7, 924.5), + (902.9, 925.1), + (903.1, 925.7), + (903.3, 926.3), + (903.5, 926.9), + (903.7, 927.5), + (903.9, 923.3), + (904.1, 923.9), + (904.3, 924.5), + } + select (Region, Hertz.Mega(freq.Input), dr, Hertz.Mega(freq.Output))); + + public static readonly TheoryData TestRegionFrequencyDataDR4 = + TheoryDataFactory.From(from freq in new (Hertz Input, Hertz Output)[] + { + (Mega(903 ), Mega(923.3)), + (Mega(904.6), Mega(923.9)), + (Mega(906.2), Mega(924.5)), + (Mega(907.8), Mega(925.1)), + (Mega(909.4), Mega(925.7)), + (Mega(911 ), Mega(926.3)), + (Mega(912.6), Mega(926.9)), + (Mega(914.2), Mega(927.5)), + } + select (Region, freq.Input, /* data rate */ DR4, freq.Output)); + + public static TheoryData TestRegionDataRateDataDR1To3() => + TheoryDataFactory.From(new[] + { + (Region, DR0, DR10), + (Region, DR1, DR11), + (Region, DR2, DR12), + (Region, DR3, DR13) + }); + + public static TheoryData TestRegionDataRateDataDR4() => + TheoryDataFactory.From(new[] { (Region, DR4, DR13) }); + + public static TheoryData TestRegionDataRateData_InvalidOffset => + TheoryDataFactory.From(new[] { - new object[] { region, 0, 4 }, - new object[] { region, 0, 5 }, - }; + (Region, DR0, DR4), + (Region, DR0, DR5), + }); - public static IEnumerable TestRegionLimitData => - from x in new(Hertz Frequency, ushort DataRate)[] + public static TheoryData TestRegionLimitData => + TheoryDataFactory.From(new (Region, Hertz, DataRateIndex)[] { - (Mega( 700.0), 5), - (Mega(1024.0), 10), - (Mega( 901.2), 90), - (Mega( 928.5), 100), - } - select new object[] { region, x.Frequency, x.DataRate }; - - public static IEnumerable TestRegionMaxPayloadLengthData => - new List + (Region, Mega( 700.0), DR5), + (Region, Mega(1024.0), DR10), + (Region, Mega( 901.2), (DataRateIndex)90), + (Region, Mega( 928.5), (DataRateIndex)100), + }); + + public static TheoryData TestRegionMaxPayloadLengthData => + TheoryDataFactory.From(new (Region, DataRateIndex, uint)[] { - new object[] { region, 0, 19 }, - new object[] { region, 1, 61 }, - new object[] { region, 2, 133 }, - new object[] { region, 3, 250 }, - new object[] { region, 4, 250 }, - new object[] { region, 8, 61 }, - new object[] { region, 9, 137 }, - new object[] { region, 10, 250 }, - new object[] { region, 11, 250 }, - new object[] { region, 13, 250 }, - }; - - public static IEnumerable TestDownstreamRX2FrequencyData => - from x in new (Hertz? NwkSrvRx2Freq, Hertz ExpectedFreq)[] + (Region, DR0, 19), + (Region, DR1, 61), + (Region, DR2, 133), + (Region, DR3, 250), + (Region, DR4, 250), + (Region, DR8, 61), + (Region, DR9, 137), + (Region, DR10, 250), + (Region, DR11, 250), + (Region, DR13, 250), + }); + + public static TheoryData TestDownstreamRX2FrequencyData => + TheoryDataFactory.From(new (Region, Hertz? NwkSrvRx2Freq, Hertz ExpectedFreq)[] { - (null , Mega(923.3)), - (Mega(920.0), Mega(920.0)), - } - select new object[] { region, x.NwkSrvRx2Freq, x.ExpectedFreq }; + (Region, null , Mega(923.3)), + (Region, Mega(920.0), Mega(920.0)), + }); - public static IEnumerable TestDownstreamRX2DataRateData => - new List + public static TheoryData TestDownstreamRX2DataRateData => + TheoryDataFactory.From(new (Region, DataRateIndex?, DataRateIndex?, DataRateIndex)[] { - new object[] { region, null, null, DR8 }, - new object[] { region, DR11, null, DR11 }, - new object[] { region, DR11, DR12, DR12 }, - }; + (Region, null, null, DR8), + (Region, DR11, null, DR11), + (Region, DR11, DR12, DR12), + }); - public static IEnumerable TestTranslateToRegionData => - new List + public static TheoryData TestTranslateToRegionData => + TheoryDataFactory.From(new[] { - new object[] { region, LoRaRegionType.US915 }, - new object[] { region, LoRaRegionType.US902 }, - }; + (Region, LoRaRegionType.US915), + (Region, LoRaRegionType.US902), + }); - public static IEnumerable TestTryGetJoinChannelIndexData => - from freq in new Hertz[] { Mega(902.3), Mega(927.5) } - select new object[] { region, freq, /* expected index */ -1 }; + public static TheoryData TestTryGetJoinChannelIndexData => + TheoryDataFactory.From(from freq in new Hertz[] { Mega(902.3), Mega(927.5) } + select (Region, freq, /* expected index */ -1)); - public static IEnumerable TestIsValidRX1DROffsetData => - new List + public static TheoryData TestIsValidRX1DROffsetData => + TheoryDataFactory.From(new[] { - new object[] { region, 0, true }, - new object[] { region, 3, true }, - new object[] { region, 4, false }, - }; + (Region, 0, true), + (Region, 3, true), + (Region, 4, false), + }); - public static IEnumerable TestIsDRIndexWithinAcceptableValuesData => - new List + public static TheoryData TestIsDRIndexWithinAcceptableValuesData => + TheoryDataFactory.From(new[] { - new object[] { region, DR0, true, true }, - new object[] { region, DR2, true, true }, - new object[] { region, DR4, true, true }, - new object[] { region, DR10, false, true }, - new object[] { region, DR13, false, true }, - new object[] { region, DR2, false, false }, - new object[] { region, DR5, true, false }, - new object[] { region, DR7, true, false }, - new object[] { region, DR10, true, false }, - new object[] { region, DR12, true, false }, - new object[] { region, DR14, true, false }, - }; + (Region, DR0, true, true), + (Region, DR2, true, true), + (Region, DR4, true, true), + (Region, DR10, false, true), + (Region, DR13, false, true), + (Region, DR2, false, false), + (Region, DR5, true, false), + (Region, DR7, true, false), + (Region, DR10, true, false), + (Region, DR12, true, false), + (Region, DR14, true, false), + }); } } diff --git a/Tests/Unit/LoRaTools/TxParamSetupAnswerTests.cs b/Tests/Unit/LoRaTools/TxParamSetupAnswerTests.cs deleted file mode 100644 index 34dfe60511..0000000000 --- a/Tests/Unit/LoRaTools/TxParamSetupAnswerTests.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -#nullable enable - -namespace LoRaWan.Tests.Unit.LoRaTools -{ - using global::LoRaTools; - using global::LoRaTools.Mac; - using Xunit; - - public sealed class TxParamSetupAnswerTests - { - [Fact] - public void Length() - { - Assert.Equal(1, new TxParamSetupAnswer().Length); - } - - [Fact] - public void ToBytes() - { - Assert.Equal(new[] { (byte)Cid.TxParamSetupCmd }, new TxParamSetupAnswer().ToBytes()); - } - - [Fact] - public void Cid_Success() - { - Assert.Equal(Cid.TxParamSetupCmd, new TxParamSetupAnswer().Cid); - } - } -} diff --git a/Tests/Unit/LoRaTools/TxParamSetupRequestTests.cs b/Tests/Unit/LoRaTools/TxParamSetupRequestTests.cs deleted file mode 100644 index 5aa5f446b0..0000000000 --- a/Tests/Unit/LoRaTools/TxParamSetupRequestTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -#nullable enable - -namespace LoRaWan.Tests.Unit.LoRaTools -{ - using System; - using global::LoRaTools; - using global::LoRaTools.Mac; - using global::LoRaTools.Regions; - using LoRaWan.Tests.Common; - using Xunit; - - public sealed class TxParamSetupRequestTests - { - [Fact] - public void Init_Throws_When_Dwell_Time_Settings_Are_Empty() - { - Assert.Throws(() => new TxParamSetupRequest(null!)); - } - - [Fact] - public void Init_Throws_When_Eirp_Is_Invalid() - { - Assert.Throws(() => new TxParamSetupRequest(new DwellTimeSetting(false, false, 16))); - } - - public static TheoryData ToBytes_Theory_Data() => TheoryDataFactory.From(new[] - { - (new DwellTimeSetting(false, false, 0), (byte)0b0000_0000), - (new DwellTimeSetting(true, false, 0), (byte)0b0010_0000), - (new DwellTimeSetting(false, true, 0), (byte)0b0001_0000), - (new DwellTimeSetting(false, false, 13), (byte)0b0000_1101), - (new DwellTimeSetting(true, true, 15), (byte)0b0011_1111), - }); - - [Theory] - [MemberData(nameof(ToBytes_Theory_Data))] - public void ToByte_Success_Cases(DwellTimeSetting dwellTimeSetting, byte actualByte) - { - Assert.Equal(new[] { (byte)Cid.TxParamSetupCmd, actualByte }, new TxParamSetupRequest(dwellTimeSetting).ToBytes()); - } - - [Fact] - public void Length() - { - Assert.Equal(2, new TxParamSetupRequest(new DwellTimeSetting(false, false, 0)).Length); - } - - [Fact] - public void Cid_Success() - { - Assert.Equal(Cid.TxParamSetupCmd, new TxParamSetupRequest(new DwellTimeSetting(false, false, 0)).Cid); - } - } -} diff --git a/Tests/Unit/LoRaTools/WebSocketConnectionTests.cs b/Tests/Unit/LoRaTools/WebSocketConnectionTests.cs new file mode 100644 index 0000000000..530b1b38c6 --- /dev/null +++ b/Tests/Unit/LoRaTools/WebSocketConnectionTests.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Unit.LoRaTools +{ + using System; + using System.Net; + using System.Net.WebSockets; + using System.Threading; + using System.Threading.Tasks; + using global::LoRaTools; + using Microsoft.AspNetCore.Http; + using Moq; + using Xunit; + + public sealed class WebSocketConnectionTests + { + private static readonly Func NoopHandler = (_, _, _) => Task.CompletedTask; + private readonly WebSocketConnection subject; + private readonly Mock httpContextMock; + + public WebSocketConnectionTests() + { + this.httpContextMock = new Mock(); + this.subject = new WebSocketConnection(this.httpContextMock.Object, null); + } + + [Fact] + public async Task HandleAsync_Should_Return_400_If_Request_Is_Not_WebSocket() + { + // arrange + SetupWebSocketConnection(true); + + // act + var result = await this.subject.HandleAsync(NoopHandler, CancellationToken.None); + + // assert + Assert.Equal((int)HttpStatusCode.BadRequest, this.httpContextMock.Object.Response.StatusCode); + Assert.Equal((int)HttpStatusCode.BadRequest, result.Response.StatusCode); + } + + [Fact] + public async Task HandleAsync_Invokes_Handler() + { + // arrange + using var cts = new CancellationTokenSource(); + var handler = new Mock>(); + var socket = SetupWebSocketConnection(); + + // act + _ = await this.subject.HandleAsync(handler.Object, cts.Token); + + // assert + handler.Verify(h => h.Invoke(this.httpContextMock.Object, socket.Object, cts.Token), Times.Once); + } + + [Fact] + public async Task HandleAsync_Handles_Premature_Connection_Close() + { + // arrange + using var cts = new CancellationTokenSource(); + _ = SetupWebSocketConnection(); + + // act + assert (does not throw) + _ = await this.subject.HandleAsync((_, _, _) => throw new OperationCanceledException("Some exception", new WebSocketException(WebSocketError.ConnectionClosedPrematurely)), cts.Token); + } + + [Fact] + public async Task HandleAsync_Closes_Socket_After_Handling() + { + // arrange + using var cts = new CancellationTokenSource(); + var socket = SetupWebSocketConnection(); + + // act + _ = await this.subject.HandleAsync(NoopHandler, cts.Token); + + // assert + socket.Verify(s => s.CloseAsync(WebSocketCloseStatus.NormalClosure, It.IsAny(), cts.Token), Times.Once); + } + + + private Mock SetupWebSocketConnection(bool notWebSocketRequest = false) + { + var isWebSocketRequest = !notWebSocketRequest; + var webSocketsManager = new Mock(); + _ = webSocketsManager.SetupGet(x => x.IsWebSocketRequest).Returns(isWebSocketRequest); + + _ = this.httpContextMock.SetupGet(m => m.WebSockets).Returns(webSocketsManager.Object); + _ = this.httpContextMock.SetupGet(m => m.Response).Returns(Mock.Of()); + + var socketMock = new Mock(); + if (isWebSocketRequest) + { + _ = webSocketsManager.Setup(x => x.AcceptWebSocketAsync()).ReturnsAsync(socketMock.Object); + _ = this.httpContextMock.Setup(x => x.Connection).Returns(Mock.Of()); + } + + return socketMock; + } + } +} diff --git a/Tests/Unit/NetworkServer/WebSocketExtensionsTests.cs b/Tests/Unit/LoRaTools/WebSocketExtensionsTests.cs similarity index 98% rename from Tests/Unit/NetworkServer/WebSocketExtensionsTests.cs rename to Tests/Unit/LoRaTools/WebSocketExtensionsTests.cs index 1f0773c244..03207d7696 100644 --- a/Tests/Unit/NetworkServer/WebSocketExtensionsTests.cs +++ b/Tests/Unit/LoRaTools/WebSocketExtensionsTests.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace LoRaWan.Tests.Unit.NetworkServer +namespace LoRaWan.Tests.Unit.LoRaTools { using System; using System.Collections.Generic; @@ -10,7 +10,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer using System.Text; using System.Threading; using System.Threading.Tasks; - using LoRaWan.NetworkServer; + using global::LoRaTools; using LoRaWan.Tests.Common; using Moq; using Xunit; diff --git a/Tests/Unit/LoRaWan.Tests.Unit.csproj b/Tests/Unit/LoRaWan.Tests.Unit.csproj index 59cc6ba9e6..518dbfd9a6 100644 --- a/Tests/Unit/LoRaWan.Tests.Unit.csproj +++ b/Tests/Unit/LoRaWan.Tests.Unit.csproj @@ -11,6 +11,7 @@ + diff --git a/Tests/Unit/LoRaWan/EuiTests.cs b/Tests/Unit/LoRaWan/EuiTests.cs index d001c96814..7ad7e88281 100644 --- a/Tests/Unit/LoRaWan/EuiTests.cs +++ b/Tests/Unit/LoRaWan/EuiTests.cs @@ -7,6 +7,7 @@ namespace LoRaWan.Tests.Unit using System.Globalization; using System.Linq; using LoRaWan; + using LoRaWan.Tests.Common; using Xunit; internal static class EuiTests @@ -74,9 +75,9 @@ public void Parse_Returns_Parsed_Value_When_Input_Is_Valid() } #pragma warning disable CA1000 // Do not declare static members on generic types (necessary for unit tests) - public static object[][] SupportedFormatsTheoryData() => + public static TheoryData SupportedFormatsTheoryData() => #pragma warning restore CA1000 // Do not declare static members on generic types - EuiTests.SupportedFormats.Select(f => new object[] { f }).ToArray(); + TheoryDataFactory.From(EuiTests.SupportedFormats.Select(c => c?.ToString())); [Theory] [MemberData(nameof(SupportedFormatsTheoryData))] diff --git a/Tests/Unit/LoRaWan/MacCommandTest.cs b/Tests/Unit/LoRaWan/MacCommandJsonConverterTest.cs similarity index 88% rename from Tests/Unit/LoRaWan/MacCommandTest.cs rename to Tests/Unit/LoRaWan/MacCommandJsonConverterTest.cs index d9d0af330d..dea345b0e0 100644 --- a/Tests/Unit/LoRaWan/MacCommandTest.cs +++ b/Tests/Unit/LoRaWan/MacCommandJsonConverterTest.cs @@ -8,7 +8,7 @@ namespace LoRaWan.Tests.Unit.LoRaTools using Newtonsoft.Json; using Xunit; - public class MacCommandTest + public class MacCommandJsonConverterTest { [Fact] public void When_Serializing_List_Should_Create_Correct_Items() @@ -61,5 +61,13 @@ public void When_Serializing_Invalid_LinkAdrCmd_Should_Throw(string input, strin var ex = Assert.Throws(() => JsonConvert.DeserializeObject(input)); Assert.Equal($"Property '{missingProperty}' is missing", ex.Message); } + + [Theory] + [InlineData(Cid.TxParamSetupCmd)] + public void When_Serializing_Invalid_Cid_Should_Throw(Cid cid) + { + var ex = Assert.Throws(() => JsonConvert.DeserializeObject(@$"{{""cid"":{(int)cid}}}")); + Assert.Equal($"Unhandled command identifier: {cid}", ex.Message); + } } } diff --git a/Tests/Unit/LoraKeysManagerFacade/ADRFunctionTest.cs b/Tests/Unit/LoraKeysManagerFacade/ADRFunctionTest.cs index 9cef1d7a19..3954b549a1 100644 --- a/Tests/Unit/LoraKeysManagerFacade/ADRFunctionTest.cs +++ b/Tests/Unit/LoraKeysManagerFacade/ADRFunctionTest.cs @@ -45,7 +45,11 @@ public ADRFunctionTest() .Returns(adrStrategy.Object); this.adrStore = new LoRaADRInMemoryStore(); - this.adrManager = new LoRaADRServerManager(this.adrStore, strategyProvider.Object, new LoRaInMemoryDeviceStore(), NullLogger.Instance); + this.adrManager = new LoRaADRServerManager(this.adrStore, + strategyProvider.Object, + new LoRaInMemoryDeviceStore(), + NullLoggerFactory.Instance, + NullLogger.Instance); this.adrExecutionItem = new ADRExecutionItem(this.adrManager); } diff --git a/Tests/Unit/LoraKeysManagerFacade/ApiValidationTest.cs b/Tests/Unit/LoraKeysManagerFacade/ApiValidationTest.cs index 5c082a4b43..187d00cf9d 100644 --- a/Tests/Unit/LoraKeysManagerFacade/ApiValidationTest.cs +++ b/Tests/Unit/LoraKeysManagerFacade/ApiValidationTest.cs @@ -29,9 +29,9 @@ public async Task DevEUI_Validation(string devEUI) var dummyExecContext = new ExecutionContext(); var apiCalls = new Func>[] { - (req) => Task.Run(() => new FCntCacheCheck(null).NextFCntDownInvoke(req, NullLogger.Instance)), - (req) => Task.Run(() => new FunctionBundlerFunction(Array.Empty()).FunctionBundler(req, NullLogger.Instance, string.Empty)), - (req) => new DeviceGetter(null, null).GetDevice(req, NullLogger.Instance) + (req) => Task.Run(() => new FCntCacheCheck(null, NullLogger.Instance).NextFCntDownInvoke(req)), + (req) => Task.Run(() => new FunctionBundlerFunction(Array.Empty(), NullLogger.Instance).FunctionBundler(req, string.Empty)), + (req) => new DeviceGetter(null, null, NullLogger.Instance).GetDevice(req) }; foreach (var apiCall in apiCalls) diff --git a/Tests/Unit/LoraKeysManagerFacade/ClearLnsCacheTest.cs b/Tests/Unit/LoraKeysManagerFacade/ClearLnsCacheTest.cs new file mode 100644 index 0000000000..3eeb2dff15 --- /dev/null +++ b/Tests/Unit/LoraKeysManagerFacade/ClearLnsCacheTest.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Unit.LoraKeysManagerFacade +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using global::LoraKeysManagerFacade; + using global::LoRaTools; + using Microsoft.Azure.Devices; + using Microsoft.Azure.Devices.Common.Exceptions; + using Microsoft.Extensions.Logging.Abstractions; + using Moq; + using Xunit; + + public class ClearLnsCacheTest + { + private readonly Mock edgeDeviceGetter; + private readonly Mock serviceClient; + private readonly Mock channelPublisher; + private readonly ClearLnsCache clearLnsCache; + + public ClearLnsCacheTest() + { + this.edgeDeviceGetter = new Mock(); + this.serviceClient = new Mock(); + this.channelPublisher = new Mock(); + this.clearLnsCache = new ClearLnsCache(this.edgeDeviceGetter.Object, this.serviceClient.Object, this.channelPublisher.Object, NullLogger.Instance); + } + + [Fact] + public async Task ClearLnsCacheInternalAsync_Invokes_Both_Edge_And_Non_Edge_Devices() + { + //arrange + var listEdgeDevices = new List { "edge1", "edge2" }; + this.edgeDeviceGetter.Setup(m => m.ListEdgeDevicesAsync(It.IsAny())).ReturnsAsync(listEdgeDevices); + + this.serviceClient.Setup(m => m.InvokeDeviceMethodAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new CloudToDeviceMethodResult() { Status = 200 }); + + //act + await this.clearLnsCache.ClearLnsCacheInternalAsync(default); + + //assert + foreach (var edgeDevice in listEdgeDevices) + { + this.serviceClient.Verify(c => c.InvokeDeviceMethodAsync(edgeDevice, + Constants.NetworkServerModuleId, + It.Is(c => c.MethodName == LoraKeysManagerFacadeConstants.ClearCacheMethodName), + It.IsAny()), Times.Once()); + } + + this.channelPublisher.Verify(c => c.PublishAsync(LoraKeysManagerFacadeConstants.ClearCacheMethodName, It.Is(r => r.Kind == RemoteCallKind.ClearCache)), Times.Once()); + } + + [Fact] + public async Task ClearLnsCacheInternalAsync_Invokes_Only_Pub_Sub_When_No_Edge_Devices() + { + //arrange + this.edgeDeviceGetter.Setup(m => m.ListEdgeDevicesAsync(It.IsAny())).ReturnsAsync(Array.Empty()); + + this.serviceClient.Setup(m => m.InvokeDeviceMethodAsync(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new CloudToDeviceMethodResult() { Status = 200 }); + + //act + await this.clearLnsCache.ClearLnsCacheInternalAsync(default); + + //assert + this.serviceClient.VerifyNoOtherCalls(); + + this.channelPublisher.Verify(c => c.PublishAsync(LoraKeysManagerFacadeConstants.ClearCacheMethodName, + It.Is(r => r.Kind == RemoteCallKind.ClearCache)), Times.Once()); + } + } +} diff --git a/Tests/Unit/LoraKeysManagerFacade/ConcentratorCredentialTests.cs b/Tests/Unit/LoraKeysManagerFacade/ConcentratorCredentialTests.cs index cb696eeb2b..e845bc22fa 100644 --- a/Tests/Unit/LoraKeysManagerFacade/ConcentratorCredentialTests.cs +++ b/Tests/Unit/LoraKeysManagerFacade/ConcentratorCredentialTests.cs @@ -12,11 +12,12 @@ namespace LoRaWan.Tests.Unit.LoraKeysManagerFacade using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; using global::LoraKeysManagerFacade; + using global::LoRaTools; using global::LoRaTools.CommonAPI; + using global::LoRaTools.IoTHubImpl; using LoRaWan.Tests.Common; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; - using Microsoft.Azure.Devices; using Microsoft.Azure.Devices.Shared; using Microsoft.Extensions.Azure; using Microsoft.Extensions.Logging.Abstractions; @@ -26,7 +27,7 @@ namespace LoRaWan.Tests.Unit.LoraKeysManagerFacade public class ConcentratorCredentialTests { - private readonly Mock registryManager; + private readonly Mock registryManager; private readonly Mock> azureClientFactory; private readonly ConcentratorCredentialsFunction concentratorCredential; private readonly StationEui stationEui = StationEui.Parse("001122FFFEAABBCC"); @@ -35,7 +36,7 @@ public class ConcentratorCredentialTests public ConcentratorCredentialTests() { - this.registryManager = new Mock(); + this.registryManager = new Mock(); this.azureClientFactory = new Mock>(); this.concentratorCredential = new ConcentratorCredentialsFunction(registryManager.Object, azureClientFactory.Object, NullLogger.Instance); } @@ -133,7 +134,7 @@ public async Task RunFetchConcentratorCredentials_Returns_InternalServerError_Fo var twin = new Twin(); twin.Properties.Desired = new TwinCollection(JsonUtil.Strictify(@"{'key': 'value'}")); this.registryManager.Setup(m => m.GetTwinAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(twin); + .ReturnsAsync(new IoTHubDeviceTwin(twin)); var actual = await this.concentratorCredential.RunFetchConcentratorCredentials(httpRequest.Object, CancellationToken.None); @@ -177,7 +178,7 @@ private Mock SetupHttpRequest(ConcentratorCredentialType credential return httpRequest; } - private static Twin SetupDeviceTwin() + private static IDeviceTwin SetupDeviceTwin() { var twin = new Twin(); twin.Properties.Desired = new TwinCollection(JsonUtil.Strictify(@"{'cups': { @@ -189,7 +190,7 @@ private static Twin SetupDeviceTwin() 'tcCredentialUrl': 'https://storage.blob.core.windows.net/container/blob' }}")); - return twin; + return new IoTHubDeviceTwin(twin); } } } diff --git a/Tests/Unit/LoraKeysManagerFacade/ConcentratorFirmwareFunctionTests.cs b/Tests/Unit/LoraKeysManagerFacade/ConcentratorFirmwareFunctionTests.cs index 8a26151adb..4e3f65f709 100644 --- a/Tests/Unit/LoraKeysManagerFacade/ConcentratorFirmwareFunctionTests.cs +++ b/Tests/Unit/LoraKeysManagerFacade/ConcentratorFirmwareFunctionTests.cs @@ -3,6 +3,7 @@ namespace LoRaWan.Tests.Unit.LoraKeysManagerFacade { + using System; using System.Collections.Generic; using System.IO; using System.Text; @@ -12,10 +13,11 @@ namespace LoRaWan.Tests.Unit.LoraKeysManagerFacade using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; using global::LoraKeysManagerFacade; + using global::LoRaTools; + using global::LoRaTools.IoTHubImpl; using LoRaWan.Tests.Common; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; - using Microsoft.Azure.Devices; using Microsoft.Azure.Devices.Shared; using Microsoft.Extensions.Azure; using Microsoft.Extensions.Logging.Abstractions; @@ -27,9 +29,9 @@ public class ConcentratorFirmwareFunctionTests { private const string BlobContent = "testcontents"; - private readonly StationEui TestStationEui = StationEui.Parse("11-11-11-11-11-11-11-11"); + private readonly StationEui testStationEui = StationEui.Parse("11-11-11-11-11-11-11-11"); - private readonly Mock registryManager; + private readonly Mock registryManager; private readonly Mock blobClient; private readonly ConcentratorFirmwareFunction concentratorFirmware; @@ -51,7 +53,7 @@ public ConcentratorFirmwareFunctionTests() azureClientFactory.Setup(m => m.CreateClient(FacadeStartup.WebJobsStorageClientName)) .Returns(blobServiceClient.Object); - this.registryManager = new Mock(); + this.registryManager = new Mock(); this.concentratorFirmware = new ConcentratorFirmwareFunction(this.registryManager.Object, azureClientFactory.Object, NullLogger.Instance); } @@ -62,7 +64,7 @@ public async Task RunFetchConcentratorFirmware_Succeeds() var httpRequest = new Mock(); var queryCollection = new QueryCollection(new Dictionary() { - { "StationEui", new StringValues(this.TestStationEui.ToString()) } + { "StationEui", new StringValues(this.testStationEui.ToString()) } }); httpRequest.SetupGet(x => x.Query).Returns(queryCollection); @@ -74,12 +76,12 @@ public async Task RunFetchConcentratorFirmware_Succeeds() 'fwSignature': '123' }}")); this.registryManager.Setup(m => m.GetTwinAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(twin)); + .Returns(Task.FromResult(new IoTHubDeviceTwin(twin))); var blobBytes = Encoding.UTF8.GetBytes(BlobContent); using var blobContentStream = new MemoryStream(blobBytes); using var streamingResult = BlobsModelFactory.BlobDownloadStreamingResult(blobContentStream); - this.blobClient.Setup(m => m.DownloadStreamingAsync(default, null, false, It.IsAny())) + this.blobClient.Setup(m => m.DownloadStreamingAsync(default, null, false, null, It.IsAny())) .Returns(Task.FromResult(Response.FromValue(streamingResult, new Mock().Object))); this.blobClient.Setup(m => m.GetPropertiesAsync(null, It.IsAny())) @@ -102,7 +104,7 @@ public async Task RunFetchConcentratorFirmware_Returns_NotFound_ForMissingTwin() var httpRequest = new Mock(); var queryCollection = new QueryCollection(new Dictionary() { - { "StationEui", new StringValues(this.TestStationEui.ToString()) } + { "StationEui", new StringValues(this.testStationEui.ToString()) } }); httpRequest.SetupGet(x => x.Query).Returns(queryCollection); @@ -114,7 +116,7 @@ public async Task RunFetchConcentratorFirmware_Returns_NotFound_ForMissingTwin() 'fwSignature': '123' }}")); this.registryManager.Setup(m => m.GetTwinAsync("AnotherTwin", It.IsAny())) - .Returns(Task.FromResult(twin)); + .Returns(Task.FromResult(new IoTHubDeviceTwin(twin))); var result = await this.concentratorFirmware.RunFetchConcentratorFirmware(httpRequest.Object, CancellationToken.None); @@ -142,14 +144,14 @@ public async Task RunFetchConcentratorFirmware_Returns_InternalServerError_ForTw var httpRequest = new Mock(); var queryCollection = new QueryCollection(new Dictionary() { - { "StationEui", new StringValues(this.TestStationEui.ToString()) } + { "StationEui", new StringValues(this.testStationEui.ToString()) } }); httpRequest.SetupGet(x => x.Query).Returns(queryCollection); var twin = new Twin(); twin.Properties.Desired = new TwinCollection(JsonUtil.Strictify(@"{'a': 'b'}")); this.registryManager.Setup(m => m.GetTwinAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(twin)); + .Returns(Task.FromResult(new IoTHubDeviceTwin(twin))); var actual = await this.concentratorFirmware.RunFetchConcentratorFirmware(httpRequest.Object, CancellationToken.None); @@ -164,7 +166,7 @@ public async Task RunFetchConcentratorFirmware_Returns_InternalServerError_ForTw var httpRequest = new Mock(); var queryCollection = new QueryCollection(new Dictionary() { - { "StationEui", new StringValues(this.TestStationEui.ToString()) } + { "StationEui", new StringValues(this.testStationEui.ToString()) } }); httpRequest.SetupGet(x => x.Query).Returns(queryCollection); @@ -175,7 +177,7 @@ public async Task RunFetchConcentratorFirmware_Returns_InternalServerError_ForTw 'fwSignature': '123' }}")); this.registryManager.Setup(m => m.GetTwinAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(twin)); + .Returns(Task.FromResult(new IoTHubDeviceTwin(twin))); var actual = await this.concentratorFirmware.RunFetchConcentratorFirmware(httpRequest.Object, CancellationToken.None); @@ -190,7 +192,7 @@ public async Task RunFetchConcentratorFirmware_Returns_InternalServerError_WhenD var httpRequest = new Mock(); var queryCollection = new QueryCollection(new Dictionary() { - { "StationEui", new StringValues(this.TestStationEui.ToString()) } + { "StationEui", new StringValues(this.testStationEui.ToString()) } }); httpRequest.SetupGet(x => x.Query).Returns(queryCollection); @@ -202,9 +204,13 @@ public async Task RunFetchConcentratorFirmware_Returns_InternalServerError_WhenD 'fwSignature': '123' }}")); this.registryManager.Setup(m => m.GetTwinAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(twin)); + .Returns(Task.FromResult(new IoTHubDeviceTwin(twin))); - this.blobClient.Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + this.blobClient.Setup(m => m.DownloadStreamingAsync(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) .ThrowsAsync(new RequestFailedException("download failed")); var actual = await this.concentratorFirmware.RunFetchConcentratorFirmware(httpRequest.Object, CancellationToken.None); diff --git a/Tests/Unit/LoraKeysManagerFacade/DeviceGetterTest.cs b/Tests/Unit/LoraKeysManagerFacade/DeviceGetterTest.cs index 28e5f45d09..eba4ba4a1a 100644 --- a/Tests/Unit/LoraKeysManagerFacade/DeviceGetterTest.cs +++ b/Tests/Unit/LoraKeysManagerFacade/DeviceGetterTest.cs @@ -5,10 +5,14 @@ namespace LoRaWan.Tests.Unit.LoraKeysManagerFacade { using System; using System.Text; + using System.Threading; using global::LoraKeysManagerFacade; + using global::LoRaTools; + using global::LoRaTools.IoTHubImpl; using LoRaWan.Tests.Common; using Microsoft.Azure.Devices; using Microsoft.Azure.Devices.Shared; + using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -22,24 +26,24 @@ public async void DeviceGetter_OTAA_Join() var devEui = TestEui.GenerateDevEui(); var gatewayId = NewUniqueEUI64(); - var deviceGetter = new DeviceGetter(InitRegistryManager(devEui), new LoRaInMemoryDeviceStore()); + var deviceGetter = new DeviceGetter(InitRegistryManager(devEui), new LoRaInMemoryDeviceStore(), NullLogger.Instance); var items = await deviceGetter.GetDeviceList(devEui, gatewayId, new DevNonce(0xABCD), null); Assert.Single(items); Assert.Equal(devEui, items[0].DevEUI); } - private static RegistryManager InitRegistryManager(DevEui devEui) + private static IDeviceRegistryManager InitRegistryManager(DevEui devEui) { - var mockRegistryManager = new Mock(MockBehavior.Strict); + var mockRegistryManager = new Mock(MockBehavior.Strict); var primaryKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(PrimaryKey)); mockRegistryManager - .Setup(x => x.GetDeviceAsync(It.Is(devEui.ToString(), StringComparer.Ordinal))) - .ReturnsAsync((string deviceId) => new Device(deviceId) { Authentication = new AuthenticationMechanism() { SymmetricKey = new SymmetricKey() { PrimaryKey = primaryKey } } }); + .Setup(x => x.GetDevicePrimaryKeyAsync(It.Is(devEui.ToString(), StringComparer.Ordinal))) + .ReturnsAsync((string _) => primaryKey); mockRegistryManager - .Setup(x => x.GetTwinAsync(It.Is(devEui.ToString(), StringComparer.Ordinal))) - .ReturnsAsync((string deviceId) => new Twin(deviceId)); + .Setup(x => x.GetLoRaDeviceTwinAsync(It.Is(devEui.ToString(), StringComparer.Ordinal), It.IsAny())) + .ReturnsAsync((string deviceId, CancellationToken _) => new IoTHubLoRaDeviceTwin (new Twin(deviceId))); return mockRegistryManager.Object; } diff --git a/Tests/Unit/LoraKeysManagerFacade/EdgeDeviceGetterTests.cs b/Tests/Unit/LoraKeysManagerFacade/EdgeDeviceGetterTests.cs new file mode 100644 index 0000000000..cb6f25d3cb --- /dev/null +++ b/Tests/Unit/LoraKeysManagerFacade/EdgeDeviceGetterTests.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Unit.LoraKeysManagerFacade +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using global::LoraKeysManagerFacade; + using global::LoRaTools; + using Microsoft.Azure.Devices; + using Microsoft.Extensions.Logging.Abstractions; + using Moq; + using Xunit; + + public class EdgeDeviceGetterTests + { + private const string EdgeDevice1 = "edgeDevice1"; + private Mock mockRegistryManager; + + public EdgeDeviceGetterTests() + { + InitRegistryManager(); + } + + [Theory] + [InlineData(EdgeDevice1, true)] + [InlineData("another", false)] + public async Task IsEdgeDeviceAsync_Returns_Proper_Answer(string lnsId, bool isEdge) + { + var edgeDeviceGetter = new EdgeDeviceGetter(InitRegistryManager(), new LoRaInMemoryDeviceStore(), NullLogger.Instance); + Assert.Equal(isEdge, await edgeDeviceGetter.IsEdgeDeviceAsync(lnsId, default)); + } + + [Fact] + public async Task IsEdgeDeviceAsync_Should_Not_Reach_IoTHub_Twice_If_Invoked_In_Less_Than_One_Minute() + { + var edgeDeviceGetter = new EdgeDeviceGetter(InitRegistryManager(), new LoRaInMemoryDeviceStore(), NullLogger.Instance); + Assert.True(await edgeDeviceGetter.IsEdgeDeviceAsync(EdgeDevice1, default)); + Assert.True(await edgeDeviceGetter.IsEdgeDeviceAsync(EdgeDevice1, default)); + Assert.False(await edgeDeviceGetter.IsEdgeDeviceAsync("anotherDevice", default)); + Assert.False(await edgeDeviceGetter.IsEdgeDeviceAsync("anotherDevice", default)); + + this.mockRegistryManager.Verify(x => x.GetEdgeDevices(), Times.Once); + } + + [Fact] + public async Task ListEdgeDevicesAsync_Returns_Expected_Device_List() + { + var edgeDeviceGetter = new EdgeDeviceGetter(InitRegistryManager(), new LoRaInMemoryDeviceStore(), NullLogger.Instance); + + var list = await edgeDeviceGetter.ListEdgeDevicesAsync(default); + + Assert.Contains(EdgeDevice1, list); + } + + [Fact] + public async Task ListEdgeDevicesAsync_Returns_Empty_Device_List() + { + this.mockRegistryManager = new Mock(); + var query = new Mock>(MockBehavior.Strict); + + query.Setup(x => x.HasMoreResults).Returns(false); + query.Setup(x => x.GetNextPageAsync()) + .ReturnsAsync(Array.Empty()); + + this.mockRegistryManager.Setup(c => c.GetEdgeDevices()) + .Returns(query.Object); + + var edgeDeviceGetter = new EdgeDeviceGetter(this.mockRegistryManager.Object, new LoRaInMemoryDeviceStore(), NullLogger.Instance); + + var list = await edgeDeviceGetter.ListEdgeDevicesAsync(default); + + Assert.Empty(list); + } + + private IDeviceRegistryManager InitRegistryManager() + { + this.mockRegistryManager = new Mock(); + + var mockDeviceTwin = new Mock(); + mockDeviceTwin.SetupGet(c => c.DeviceId).Returns(EdgeDevice1); + var query = new Mock>(MockBehavior.Strict); + + query.Setup(x => x.HasMoreResults).Returns(false); + query.Setup(x => x.GetNextPageAsync()) + .ReturnsAsync(new[] { mockDeviceTwin.Object }); + + mockRegistryManager.Setup(c => c.GetEdgeDevices()) + .Returns(query.Object); + + return mockRegistryManager.Object; + } + } +} diff --git a/Tests/Unit/LoraKeysManagerFacade/FCntCacheCheckTest.cs b/Tests/Unit/LoraKeysManagerFacade/FCntCacheCheckTest.cs index 0426c1ddcc..b151bda790 100644 --- a/Tests/Unit/LoraKeysManagerFacade/FCntCacheCheckTest.cs +++ b/Tests/Unit/LoraKeysManagerFacade/FCntCacheCheckTest.cs @@ -5,6 +5,7 @@ namespace LoRaWan.Tests.Unit.LoraKeysManagerFacade { using global::LoraKeysManagerFacade; using LoRaWan.Tests.Common; + using Microsoft.Extensions.Logging.Abstractions; using System.Threading.Tasks; using Xunit; @@ -14,7 +15,7 @@ public class FCntCacheCheckTest : FunctionTestBase public FCntCacheCheckTest() { - this.fcntCheck = new FCntCacheCheck(new LoRaInMemoryDeviceStore()); + this.fcntCheck = new FCntCacheCheck(new LoRaInMemoryDeviceStore(), NullLogger.Instance); } [Fact] diff --git a/Tests/Unit/LoraKeysManagerFacade/FunctionBundlerProviderTests.cs b/Tests/Unit/LoraKeysManagerFacade/FunctionBundlerProviderTests.cs index 1be1941e45..2f8338f58a 100644 --- a/Tests/Unit/LoraKeysManagerFacade/FunctionBundlerProviderTests.cs +++ b/Tests/Unit/LoraKeysManagerFacade/FunctionBundlerProviderTests.cs @@ -19,7 +19,7 @@ public async Task CreateIfRequired_Result_Has_Correct_Bundler_Request() const string gatewayId = "foo"; const double rssi = 2.3; var device = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(0)); - using var loRaDevice = TestUtils.CreateFromSimulatedDevice(device, new Mock().Object); + await using var loRaDevice = TestUtils.CreateFromSimulatedDevice(device, new Mock().Object); var payload = device.CreateConfirmedDataUpMessage("foo"); using var request = WaitableLoRaRequest.Create(TestUtils.GenerateTestRadioMetadata(rssi: rssi), payload); var deviceApiServiceMock = new Mock(); diff --git a/Tests/Unit/LoraKeysManagerFacade/FunctionBundlerTest.cs b/Tests/Unit/LoraKeysManagerFacade/FunctionBundlerTest.cs index b82f942e6f..85985542a2 100644 --- a/Tests/Unit/LoraKeysManagerFacade/FunctionBundlerTest.cs +++ b/Tests/Unit/LoraKeysManagerFacade/FunctionBundlerTest.cs @@ -12,6 +12,7 @@ namespace LoRaWan.Tests.Unit.LoraKeysManagerFacade.FunctionBundler using global::LoRaTools.ADR; using global::LoRaTools.CommonAPI; using LoRaWan.Tests.Common; + using Microsoft.ApplicationInsights.Extensibility; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -22,6 +23,7 @@ public sealed class FunctionBundlerTest : FunctionTestBase, IDisposable private readonly ILoRaADRManager adrManager; private readonly FunctionBundlerFunction functionBundler; private readonly ADRExecutionItem adrExecutionItem; + private readonly TelemetryConfiguration telemetryConfiguration; private readonly Random rnd = new Random(); public FunctionBundlerTest() @@ -50,18 +52,23 @@ public FunctionBundlerTest() // .Returns(new LoRaADRStandardStrategy()); var cacheStore = new LoRaInMemoryDeviceStore(); this.adrStore = new LoRaADRInMemoryStore(); - this.adrManager = new LoRaADRServerManager(this.adrStore, strategyProvider.Object, cacheStore, NullLogger.Instance); + this.adrManager = new LoRaADRServerManager(this.adrStore, strategyProvider.Object, cacheStore, NullLoggerFactory.Instance, NullLogger.Instance); this.adrExecutionItem = new ADRExecutionItem(this.adrManager); + this.telemetryConfiguration = new TelemetryConfiguration(); var items = new IFunctionBundlerExecutionItem[] { - new DeduplicationExecutionItem(cacheStore), + new DeduplicationExecutionItem(cacheStore, + Mock.Of(), + Mock.Of(), + Mock.Of(), + this.telemetryConfiguration), this.adrExecutionItem, - new NextFCntDownExecutionItem(new FCntCacheCheck(cacheStore)), + new NextFCntDownExecutionItem(new FCntCacheCheck(cacheStore, NullLogger.Instance)), new PreferredGatewayExecutionItem(cacheStore, new NullLogger(), null), }; - this.functionBundler = new FunctionBundlerFunction(items); + this.functionBundler = new FunctionBundlerFunction(items, NullLogger.Instance); } [Fact] @@ -336,9 +343,13 @@ public void Execution_Items_Should_Have_Correct_Priority() var items = new IFunctionBundlerExecutionItem[] { - new DeduplicationExecutionItem(cacheStore), + new DeduplicationExecutionItem(cacheStore, + Mock.Of(), + Mock.Of(), + Mock.Of(), + this.telemetryConfiguration), new ADRExecutionItem(this.adrManager), - new NextFCntDownExecutionItem(new FCntCacheCheck(cacheStore)), + new NextFCntDownExecutionItem(new FCntCacheCheck(cacheStore, NullLogger.Instance)), new PreferredGatewayExecutionItem(cacheStore, new NullLogger(), null), }; @@ -352,6 +363,10 @@ public void Execution_Items_Should_Have_Correct_Priority() Assert.Empty(items.GroupBy(x => x.Priority).Where(x => x.Count() > 1)); } - public void Dispose() => this.adrStore.Dispose(); + public void Dispose() + { + this.adrStore.Dispose(); + this.telemetryConfiguration.Dispose(); + } } } diff --git a/Tests/Unit/LoraKeysManagerFacade/MessageDeduplicationTests.cs b/Tests/Unit/LoraKeysManagerFacade/MessageDeduplicationTests.cs index b48646265a..12be079c00 100644 --- a/Tests/Unit/LoraKeysManagerFacade/MessageDeduplicationTests.cs +++ b/Tests/Unit/LoraKeysManagerFacade/MessageDeduplicationTests.cs @@ -3,18 +3,39 @@ namespace LoRaWan.Tests.Unit.LoraKeysManagerFacade.FunctionBundler { + using System; + using System.Threading; using System.Threading.Tasks; + using global::LoraKeysManagerFacade; using global::LoraKeysManagerFacade.FunctionBundler; + using global::LoRaTools; using LoRaWan.Tests.Common; + using Microsoft.ApplicationInsights.Extensibility; + using Microsoft.Azure.Devices; + using Microsoft.Azure.Devices.Client.Exceptions; + using Moq; using Xunit; - public class MessageDeduplicationTests : FunctionTestBase + public sealed class MessageDeduplicationTests : FunctionTestBase, IDisposable { private readonly DeduplicationExecutionItem deduplicationExecutionItem; + private readonly Mock serviceClientMock; + private readonly TelemetryConfiguration telemetryConfiguration; + private readonly Mock edgeDeviceGetter; public MessageDeduplicationTests() { - this.deduplicationExecutionItem = new DeduplicationExecutionItem(new LoRaInMemoryDeviceStore()); + this.serviceClientMock = new Mock(); + + this.telemetryConfiguration = new TelemetryConfiguration(); + this.edgeDeviceGetter = new Mock(); + this.edgeDeviceGetter.Setup(m => m.IsEdgeDeviceAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); + + this.deduplicationExecutionItem = new DeduplicationExecutionItem(new LoRaInMemoryDeviceStore(), + this.serviceClientMock.Object, + this.edgeDeviceGetter.Object, + Mock.Of(), + this.telemetryConfiguration); } [Fact] @@ -56,6 +77,10 @@ public async Task MessageDeduplication_DifferentDevices_Allowed() var dev1EUI = TestEui.GenerateDevEui(); var dev2EUI = TestEui.GenerateDevEui(); + this.serviceClientMock.Setup(x => x.InvokeDeviceMethodAsync( + It.IsAny(), Constants.NetworkServerModuleId, It.IsAny(), It.IsAny())) + .ReturnsAsync(new CloudToDeviceMethodResult() { Status = 200 }); + var result = await this.deduplicationExecutionItem.GetDuplicateMessageResultAsync(dev1EUI, gateway1Id, 1, 1); Assert.False(result.IsDuplicate); Assert.Equal(gateway1Id, result.GatewayId); @@ -64,6 +89,15 @@ public async Task MessageDeduplication_DifferentDevices_Allowed() Assert.False(result.IsDuplicate); Assert.Equal(gateway2Id, result.GatewayId); + // Make sure direct method was invoked on the LNS module to notify gateway 1 + // that it is no longer the owning gateway for device 1 + this.serviceClientMock.Verify( + x => x.InvokeDeviceMethodAsync(gateway1Id.ToString(), Constants.NetworkServerModuleId, + It.Is( + m => m.MethodName == LoraKeysManagerFacadeConstants.CloudToDeviceCloseConnection + && m.GetPayloadAsJson().Contains(dev1EUI.ToString())), It.IsAny()), + Times.Once); + result = await this.deduplicationExecutionItem.GetDuplicateMessageResultAsync(dev2EUI, gateway1Id, 1, 1); Assert.False(result.IsDuplicate); Assert.Equal(gateway1Id, result.GatewayId); @@ -71,6 +105,43 @@ public async Task MessageDeduplication_DifferentDevices_Allowed() result = await this.deduplicationExecutionItem.GetDuplicateMessageResultAsync(dev2EUI, gateway2Id, 2, 1); Assert.False(result.IsDuplicate); Assert.Equal(gateway2Id, result.GatewayId); + + // Make sure direct method was invoked for gateway 1 and device 2 + this.serviceClientMock.Verify( + x => x.InvokeDeviceMethodAsync(gateway1Id.ToString(), Constants.NetworkServerModuleId, + It.Is( + m => m.MethodName == LoraKeysManagerFacadeConstants.CloudToDeviceCloseConnection + && m.GetPayloadAsJson().Contains(dev2EUI.ToString())), It.IsAny()), + Times.Once); + } + + [Fact] + public async Task MessageDeduplication_When_Direct_Method_Throws_Does_Not_Throw() + { + var gateway1Id = NewUniqueEUI64(); + var gateway2Id = NewUniqueEUI64(); + var dev1EUI = TestEui.GenerateDevEui(); + + this.serviceClientMock.Setup(x => x.InvokeDeviceMethodAsync( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new IotHubException("Failed to invoke direct method")); + + var result = await this.deduplicationExecutionItem.GetDuplicateMessageResultAsync(dev1EUI, gateway1Id, 1, 1); + Assert.False(result.IsDuplicate); + Assert.Equal(gateway1Id, result.GatewayId); + + result = await this.deduplicationExecutionItem.GetDuplicateMessageResultAsync(dev1EUI, gateway2Id, 2, 1); + + this.serviceClientMock.Verify( + x => x.InvokeDeviceMethodAsync(gateway1Id.ToString(), Constants.NetworkServerModuleId, + It.Is(m => m.MethodName == LoraKeysManagerFacadeConstants.CloudToDeviceCloseConnection), It.IsAny()), + Times.Once); + + Assert.False(result.IsDuplicate); + Assert.Equal(gateway2Id, result.GatewayId); } + + public void Dispose() + => this.telemetryConfiguration.Dispose(); } } diff --git a/Tests/Unit/LoraKeysManagerFacade/SearchDeviceByDevEUITest.cs b/Tests/Unit/LoraKeysManagerFacade/SearchDeviceByDevEUITest.cs index 32aae4e7e2..fccc9fb75b 100644 --- a/Tests/Unit/LoraKeysManagerFacade/SearchDeviceByDevEUITest.cs +++ b/Tests/Unit/LoraKeysManagerFacade/SearchDeviceByDevEUITest.cs @@ -7,6 +7,7 @@ namespace LoRaWan.Tests.Unit.LoraKeysManagerFacade using System.Text; using System.Threading.Tasks; using global::LoraKeysManagerFacade; + using global::LoRaTools; using global::LoRaTools.CommonAPI; using LoRaWan.Tests.Common; using Microsoft.AspNetCore.Http; @@ -28,9 +29,9 @@ public async Task When_Query_String_Is_Not_Found_Should_Return_BadResult() { var ctx = new DefaultHttpContext(); ctx.Request.QueryString = new QueryString($"?{ApiVersion.QueryStringParamName}={ApiVersion.Version_2019_02_12_Preview.Version}"); - var registryManager = new Mock(MockBehavior.Strict); - var searchDeviceByDevEUI = new SearchDeviceByDevEUI(registryManager.Object); - var result = await searchDeviceByDevEUI.GetDeviceByDevEUI(ctx.Request, NullLogger.Instance); + var registryManager = new Mock(MockBehavior.Strict); + var searchDeviceByDevEUI = SetupSubject(registryManager.Object); + var result = await searchDeviceByDevEUI.GetDeviceByDevEUI(ctx.Request); Assert.IsType(result); } @@ -39,9 +40,9 @@ public async Task When_Version_Is_Missing_Should_Return_BadResult() { var ctx = new DefaultHttpContext(); ctx.Request.QueryString = new QueryString($"?devEUI=193123"); - var registryManager = new Mock(MockBehavior.Strict); - var searchDeviceByDevEUI = new SearchDeviceByDevEUI(registryManager.Object); - var result = await searchDeviceByDevEUI.GetDeviceByDevEUI(ctx.Request, NullLogger.Instance); + var registryManager = new Mock(MockBehavior.Strict); + var searchDeviceByDevEUI = SetupSubject(registryManager.Object); + var result = await searchDeviceByDevEUI.GetDeviceByDevEUI(ctx.Request); Assert.IsType(result); } @@ -52,9 +53,9 @@ public async Task When_Version_Is_Not_Supported_Should_Return_BadResult(string v { var ctx = new DefaultHttpContext(); ctx.Request.QueryString = new QueryString($"?devEUI=193123&{ApiVersion.QueryStringParamName}={version}"); - var registryManager = new Mock(MockBehavior.Strict); - var searchDeviceByDevEUI = new SearchDeviceByDevEUI(registryManager.Object); - var result = await searchDeviceByDevEUI.GetDeviceByDevEUI(ctx.Request, NullLogger.Instance); + var registryManager = new Mock(MockBehavior.Strict); + var searchDeviceByDevEUI = SetupSubject(registryManager.Object); + var result = await searchDeviceByDevEUI.GetDeviceByDevEUI(ctx.Request); Assert.IsType(result); } @@ -67,13 +68,13 @@ public async Task When_Device_Is_Not_Found_Should_Returns_NotFound(string format var ctx = new DefaultHttpContext(); ctx.Request.QueryString = new QueryString($"?devEUI={devEUI.ToString(format, null)}&{ApiVersion.QueryStringParamName}={ApiVersion.LatestVersion}"); - var registryManager = new Mock(MockBehavior.Strict); - registryManager.Setup(x => x.GetDeviceAsync(devEUI.ToString())) - .ReturnsAsync((Device)null); + var registryManager = new Mock(MockBehavior.Strict); + registryManager.Setup(x => x.GetDevicePrimaryKeyAsync(devEUI.ToString())) + .ReturnsAsync((string)null); - var searchDeviceByDevEUI = new SearchDeviceByDevEUI(registryManager.Object); + var searchDeviceByDevEUI = SetupSubject(registryManager.Object); - var result = await searchDeviceByDevEUI.GetDeviceByDevEUI(ctx.Request, NullLogger.Instance); + var result = await searchDeviceByDevEUI.GetDeviceByDevEUI(ctx.Request); Assert.IsType(result); registryManager.VerifyAll(); @@ -86,10 +87,10 @@ public async Task When_Device_Is_Found_Should_Returns_Device_Information() var devEui = new DevEui(13213123212131); var primaryKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(PrimaryKey)); var (registryManager, request) = SetupIotHubQuery(devEui.ToString(), PrimaryKey); - var searchDeviceByDevEUI = new SearchDeviceByDevEUI(registryManager.Object); + var searchDeviceByDevEUI = SetupSubject(registryManager.Object); // act - var result = await searchDeviceByDevEUI.GetDeviceByDevEUI(request, NullLogger.Instance); + var result = await searchDeviceByDevEUI.GetDeviceByDevEUI(request); // assert var okObjectResult = Assert.IsType(result); @@ -104,10 +105,10 @@ public async Task Complies_With_Contract() var devEui = SearchByDevEuiContract.Eui; var primaryKey = SearchByDevEuiContract.PrimaryKey; var (registryManager, request) = SetupIotHubQuery(devEui, primaryKey); - var searchDeviceByDevEUI = new SearchDeviceByDevEUI(registryManager.Object); + var searchDeviceByDevEUI = SetupSubject(registryManager.Object); // act - var result = await searchDeviceByDevEUI.GetDeviceByDevEUI(request, NullLogger.Instance); + var result = await searchDeviceByDevEUI.GetDeviceByDevEUI(request); // assert var okObjectResult = Assert.IsType(result); @@ -115,21 +116,20 @@ public async Task Complies_With_Contract() registryManager.VerifyAll(); } - private static (Mock, HttpRequest) SetupIotHubQuery(string devEui, string primaryKey) + private static (Mock, HttpRequest) SetupIotHubQuery(string devEui, string primaryKey) { var ctx = new DefaultHttpContext(); ctx.Request.QueryString = new QueryString($"?devEUI={devEui}&{ApiVersion.QueryStringParamName}={ApiVersion.LatestVersion}"); - var registryManager = new Mock(MockBehavior.Strict); - var deviceInfo = new Device(devEui) - { - Authentication = new AuthenticationMechanism() { SymmetricKey = new SymmetricKey() { PrimaryKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(primaryKey)) } }, - }; + var registryManager = new Mock(MockBehavior.Strict); - registryManager.Setup(x => x.GetDeviceAsync(devEui)) - .ReturnsAsync(deviceInfo); + registryManager.Setup(x => x.GetDevicePrimaryKeyAsync(devEui)) + .ReturnsAsync(Convert.ToBase64String(Encoding.UTF8.GetBytes(primaryKey))); return (registryManager, ctx.Request); } + + private static SearchDeviceByDevEUI SetupSubject(IDeviceRegistryManager registryManager) => + new SearchDeviceByDevEUI(registryManager, NullLogger.Instance); } } diff --git a/Tests/Unit/LoraKeysManagerFacade/SendCloudToDeviceMessageTest.cs b/Tests/Unit/LoraKeysManagerFacade/SendCloudToDeviceMessageTest.cs index 1d9d0abe6f..6a5175279a 100644 --- a/Tests/Unit/LoraKeysManagerFacade/SendCloudToDeviceMessageTest.cs +++ b/Tests/Unit/LoraKeysManagerFacade/SendCloudToDeviceMessageTest.cs @@ -8,10 +8,13 @@ namespace LoRaWan.Tests.Unit.LoraKeysManagerFacade using System.IO; using System.Net; using System.Text; + using System.Threading; using System.Threading.Tasks; using global::LoraKeysManagerFacade; + using global::LoRaTools; using global::LoRaTools.CommonAPI; using LoRaWan.Tests.Common; + using LoRaWan.Tests.Unit.IoTHubImpl; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.Devices; @@ -28,15 +31,24 @@ public class SendCloudToDeviceMessageTest private readonly LoRaInMemoryDeviceStore cacheStore; private readonly Mock serviceClient; - private readonly Mock registryManager; + private readonly Mock registryManager; + private readonly Mock edgeDeviceGetter; + private readonly Mock channelPublisher; private readonly SendCloudToDeviceMessage sendCloudToDeviceMessage; public SendCloudToDeviceMessageTest() { this.cacheStore = new LoRaInMemoryDeviceStore(); this.serviceClient = new Mock(MockBehavior.Strict); - this.registryManager = new Mock(MockBehavior.Strict); - this.sendCloudToDeviceMessage = new SendCloudToDeviceMessage(this.cacheStore, this.registryManager.Object, this.serviceClient.Object, new NullLogger()); + this.registryManager = new Mock(MockBehavior.Strict); + this.edgeDeviceGetter = new Mock(); + this.channelPublisher = new Mock(); + this.sendCloudToDeviceMessage = new SendCloudToDeviceMessage(this.cacheStore, + this.registryManager.Object, + this.serviceClient.Object, + this.edgeDeviceGetter.Object, + this.channelPublisher.Object, + new NullLogger()); } [Theory] @@ -52,7 +64,7 @@ public async Task When_DevEUI_Is_Missing_Should_Return_BadRequest(string devEUI) var request = new DefaultHttpContext().Request; request.Body = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(c2dMessage))); - var result = await this.sendCloudToDeviceMessage.Run(request, devEUI); + var result = await this.sendCloudToDeviceMessage.Run(request, devEUI, default); Assert.IsType(result); @@ -63,7 +75,7 @@ public async Task When_DevEUI_Is_Missing_Should_Return_BadRequest(string devEUI) [Fact] public async Task When_Request_Is_Missing_Should_Return_BadRequest() { - var actual = await this.sendCloudToDeviceMessage.Run(null, new DevEui(123456789).ToString()); + var actual = await this.sendCloudToDeviceMessage.Run(null, new DevEui(123456789).ToString(), default); Assert.IsType(actual); @@ -76,7 +88,8 @@ public async Task When_Message_Is_Missing_Should_Return_BadRequest() { var actual = await this.sendCloudToDeviceMessage.SendCloudToDeviceMessageImplementationAsync( new DevEui(123456789), - null); + null, + default); Assert.IsType(actual); @@ -97,7 +110,8 @@ public async Task When_Message_Is_Invalid_Should_Return_BadRequest() var actual = await this.sendCloudToDeviceMessage.SendCloudToDeviceMessageImplementationAsync( devEui, - c2dMessage); + c2dMessage, + default); Assert.IsType(actual); @@ -105,9 +119,13 @@ public async Task When_Message_Is_Invalid_Should_Return_BadRequest() this.registryManager.VerifyAll(); } - [Fact] - public async Task When_Device_Is_Found_In_Cache_Should_Send_Via_Direct_Method() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task When_Device_Is_Found_In_Cache_Should_Send_Via_Direct_Method_Or_Pub_Sub(bool isEdgeDevice) { + this.edgeDeviceGetter.Setup(m => m.IsEdgeDeviceAsync(It.IsAny(), It.IsAny())).ReturnsAsync(isEdgeDevice); + var devEui = new DevEui(123456789); var preferredGateway = new LoRaDevicePreferredGateway("gateway1", 100); LoRaDevicePreferredGateway.SaveToCache(this.cacheStore, devEui, preferredGateway); @@ -119,13 +137,23 @@ public async Task When_Device_Is_Found_In_Cache_Should_Send_Via_Direct_Method() }; LoRaCloudToDeviceMessage receivedC2DMessage = null; - this.serviceClient.Setup(x => x.InvokeDeviceMethodAsync("gateway1", LoraKeysManagerFacadeConstants.NetworkServerModuleId, It.IsNotNull())) - .Callback((device, methodName, method) => receivedC2DMessage = JsonConvert.DeserializeObject(method.GetPayloadAsJson())) - .ReturnsAsync(new CloudToDeviceMethodResult() { Status = (int)HttpStatusCode.OK }); + + if (isEdgeDevice) + { + this.serviceClient.Setup(x => x.InvokeDeviceMethodAsync("gateway1", Constants.NetworkServerModuleId, It.IsNotNull(), default)) + .Callback((device, methodName, method, _) => receivedC2DMessage = JsonConvert.DeserializeObject(method.GetPayloadAsJson())) + .ReturnsAsync(new CloudToDeviceMethodResult() { Status = (int)HttpStatusCode.OK }); + } + else + { + this.channelPublisher.Setup(x => x.PublishAsync("gateway1", It.IsNotNull())) + .Callback((device, remoteCall) => receivedC2DMessage = JsonConvert.DeserializeObject(remoteCall.JsonData)); + } var actual = await this.sendCloudToDeviceMessage.SendCloudToDeviceMessageImplementationAsync( devEui, - actualMessage); + actualMessage, + default); Assert.IsType(actual); var responseValue = ((OkObjectResult)actual).Value as SendCloudToDeviceMessageResult; @@ -143,11 +171,13 @@ public async Task When_Device_Is_Found_In_Cache_Should_Send_Via_Direct_Method() [Fact] public async Task When_Direct_Method_Returns_Error_Code_Should_Forward_Status_Error() { + this.edgeDeviceGetter.Setup(m => m.IsEdgeDeviceAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); + var devEui = new DevEui(0123456789); var preferredGateway = new LoRaDevicePreferredGateway("gateway1", 100); LoRaDevicePreferredGateway.SaveToCache(this.cacheStore, devEui, preferredGateway); - this.serviceClient.Setup(x => x.InvokeDeviceMethodAsync("gateway1", LoraKeysManagerFacadeConstants.NetworkServerModuleId, It.IsNotNull())) + this.serviceClient.Setup(x => x.InvokeDeviceMethodAsync("gateway1", Constants.NetworkServerModuleId, It.IsNotNull(), default)) .ReturnsAsync(new CloudToDeviceMethodResult() { Status = (int)HttpStatusCode.BadRequest }); var actual = await this.sendCloudToDeviceMessage.SendCloudToDeviceMessageImplementationAsync( @@ -155,7 +185,7 @@ public async Task When_Direct_Method_Returns_Error_Code_Should_Forward_Status_Er new LoRaCloudToDeviceMessage() { Fport = TestPort, - }); + }, default); Assert.IsType(actual); Assert.Equal((int)HttpStatusCode.BadRequest, ((ObjectResult)actual).StatusCode); @@ -167,11 +197,13 @@ public async Task When_Direct_Method_Returns_Error_Code_Should_Forward_Status_Er [Fact] public async Task When_Direct_Method_Throws_Exception_Should_Return_Application_Error() { + this.edgeDeviceGetter.Setup(m => m.IsEdgeDeviceAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); + var devEui = new DevEui(123456789); var preferredGateway = new LoRaDevicePreferredGateway("gateway1", 100); LoRaDevicePreferredGateway.SaveToCache(this.cacheStore, devEui, preferredGateway); - this.serviceClient.Setup(x => x.InvokeDeviceMethodAsync("gateway1", LoraKeysManagerFacadeConstants.NetworkServerModuleId, It.IsNotNull())) + this.serviceClient.Setup(x => x.InvokeDeviceMethodAsync("gateway1", Constants.NetworkServerModuleId, It.IsNotNull(), default)) .ThrowsAsync(new IotHubCommunicationException(string.Empty)); var actual = await this.sendCloudToDeviceMessage.SendCloudToDeviceMessageImplementationAsync( @@ -179,7 +211,7 @@ public async Task When_Direct_Method_Throws_Exception_Should_Return_Application_ new LoRaCloudToDeviceMessage() { Fport = TestPort, - }); + }, default); Assert.IsType(actual); Assert.Equal((int)HttpStatusCode.InternalServerError, ((ObjectResult)actual).StatusCode); @@ -193,12 +225,16 @@ public async Task When_Device_Does_Not_Have_DevAddr_Should_Return_BadRequest() { var devEui = new DevEui(123456789); - var query = new Mock(MockBehavior.Strict); + var mockDeviceTwin = new Mock(); + mockDeviceTwin.SetupGet(c => c.Properties) + .Returns(new TwinProperties()); + + var query = new Mock>(MockBehavior.Strict); query.Setup(x => x.HasMoreResults).Returns(true); - query.Setup(x => x.GetNextAsTwinAsync()) - .ReturnsAsync(new[] { new Twin() }); + query.Setup(x => x.GetNextPageAsync()) + .ReturnsAsync(new[] { mockDeviceTwin.Object }); - this.registryManager.Setup(x => x.CreateQuery(It.IsNotNull(), It.IsAny())) + this.registryManager.Setup(x => x.FindDeviceByDevEUI(It.IsNotNull())) .Returns(query.Object); var actual = await this.sendCloudToDeviceMessage.SendCloudToDeviceMessageImplementationAsync( @@ -206,7 +242,7 @@ public async Task When_Device_Does_Not_Have_DevAddr_Should_Return_BadRequest() new LoRaCloudToDeviceMessage() { Fport = TestPort, - }); + }, default); Assert.IsType(actual); @@ -220,12 +256,12 @@ public async Task When_Querying_Devices_Throws_Exception_Should_Return_Applicati { var devEui = new DevEui(123456789); - var query = new Mock(MockBehavior.Strict); + var query = new Mock>(MockBehavior.Strict); query.Setup(x => x.HasMoreResults).Returns(true); - query.Setup(x => x.GetNextAsTwinAsync()) + query.Setup(x => x.GetNextPageAsync()) .ThrowsAsync(new IotHubCommunicationException(string.Empty)); - this.registryManager.Setup(x => x.CreateQuery(It.IsNotNull(), It.IsAny())) + this.registryManager.Setup(x => x.FindDeviceByDevEUI(It.IsNotNull())) .Returns(query.Object); var actual = await this.sendCloudToDeviceMessage.SendCloudToDeviceMessageImplementationAsync( @@ -233,7 +269,7 @@ public async Task When_Querying_Devices_Throws_Exception_Should_Return_Applicati new LoRaCloudToDeviceMessage() { Fport = TestPort, - }); + }, default); Assert.IsType(actual); Assert.Equal((int)HttpStatusCode.InternalServerError, ((ObjectResult)actual).StatusCode); @@ -248,12 +284,12 @@ public async Task When_Querying_Devices_Is_Empty_Should_Return_NotFound() { var devEui = new DevEui(123456789); - var query = new Mock(MockBehavior.Strict); + var query = new Mock>(MockBehavior.Strict); query.Setup(x => x.HasMoreResults).Returns(true); - query.Setup(x => x.GetNextAsTwinAsync()) - .ReturnsAsync(Array.Empty()); + query.Setup(x => x.GetNextPageAsync()) + .ReturnsAsync(Array.Empty()); - this.registryManager.Setup(x => x.CreateQuery(It.IsNotNull(), It.IsAny())) + this.registryManager.Setup(x => x.FindDeviceByDevEUI(It.IsNotNull())) .Returns(query.Object); var actual = await this.sendCloudToDeviceMessage.SendCloudToDeviceMessageImplementationAsync( @@ -261,7 +297,7 @@ public async Task When_Querying_Devices_Is_Empty_Should_Return_NotFound() new LoRaCloudToDeviceMessage() { Fport = TestPort, - }); + }, default); Assert.IsType(actual); Assert.Equal((int)HttpStatusCode.NotFound, ((ObjectResult)actual).StatusCode); @@ -271,26 +307,30 @@ public async Task When_Querying_Devices_Is_Empty_Should_Return_NotFound() query.VerifyAll(); } - [Fact] - public async Task When_Querying_Devices_And_Finds_Class_C_Should_Update_Cache_And_Send_Direct_Method() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task When_Querying_Devices_And_Finds_Class_C_Should_Update_Cache_And_Send_Direct_Method_Or_Pub_Sub(bool isEdgeDevice) { + this.edgeDeviceGetter.Setup(m => m.IsEdgeDeviceAsync(It.IsAny(), It.IsAny())).ReturnsAsync(isEdgeDevice); + var devEui = new DevEui(123456789); - var deviceTwin = new Twin - { - Properties = new TwinProperties() + var mockDeviceTwin = new Mock(); + + mockDeviceTwin.SetupGet(c => c.Properties) + .Returns(new TwinProperties() { Desired = new TwinCollection($"{{\"DevAddr\": \"03010101\", \"ClassType\": \"C\"}}"), Reported = new TwinCollection($"{{\"PreferredGatewayID\": \"gateway1\" }}"), - } - }; + }); - var query = new Mock(MockBehavior.Strict); + var query = new Mock>(MockBehavior.Strict); query.Setup(x => x.HasMoreResults).Returns(true); - query.Setup(x => x.GetNextAsTwinAsync()) - .ReturnsAsync(new[] { deviceTwin }); + query.Setup(x => x.GetNextPageAsync()) + .ReturnsAsync(new[] { mockDeviceTwin.Object }); - this.registryManager.Setup(x => x.CreateQuery(It.IsNotNull(), It.IsAny())) + this.registryManager.Setup(x => x.FindDeviceByDevEUI(It.IsNotNull())) .Returns(query.Object); var actualMessage = new LoRaCloudToDeviceMessage() @@ -300,13 +340,23 @@ public async Task When_Querying_Devices_And_Finds_Class_C_Should_Update_Cache_An }; LoRaCloudToDeviceMessage receivedC2DMessage = null; - this.serviceClient.Setup(x => x.InvokeDeviceMethodAsync("gateway1", LoraKeysManagerFacadeConstants.NetworkServerModuleId, It.IsNotNull())) - .Callback((device, methodName, method) => receivedC2DMessage = JsonConvert.DeserializeObject(method.GetPayloadAsJson())) - .ReturnsAsync(new CloudToDeviceMethodResult() { Status = (int)HttpStatusCode.OK }); + + if (isEdgeDevice) + { + this.serviceClient.Setup(x => x.InvokeDeviceMethodAsync("gateway1", Constants.NetworkServerModuleId, It.IsNotNull(), default)) + .Callback((device, methodName, method, _) => receivedC2DMessage = JsonConvert.DeserializeObject(method.GetPayloadAsJson())) + .ReturnsAsync(new CloudToDeviceMethodResult() { Status = (int)HttpStatusCode.OK }); + } + else + { + this.channelPublisher.Setup(x => x.PublishAsync("gateway1", It.IsNotNull())) + .Callback((device, remoteCall) => receivedC2DMessage = JsonConvert.DeserializeObject(remoteCall.JsonData)); + } + var actual = await this.sendCloudToDeviceMessage.SendCloudToDeviceMessageImplementationAsync( devEui, - actualMessage); + actualMessage, default); Assert.IsType(actual); var responseValue = ((OkObjectResult)actual).Value as SendCloudToDeviceMessageResult; @@ -326,26 +376,30 @@ public async Task When_Querying_Devices_And_Finds_Class_C_Should_Update_Cache_An query.VerifyAll(); } - [Fact] - public async Task When_Querying_Devices_And_Finds_Single_Gateway_Class_C_Should_Update_Cache_And_Send_Direct_Method() + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task When_Querying_Devices_And_Finds_Single_Gateway_Class_C_Should_Update_Cache_And_Send_Direct_Method_Or_Pub_Sub(bool isEdgeDevice) { + this.edgeDeviceGetter.Setup(m => m.IsEdgeDeviceAsync(It.IsAny(), It.IsAny())).ReturnsAsync(isEdgeDevice); var devEui = new DevEui(123456789); - var deviceTwin = new Twin - { - Properties = new TwinProperties() + var mockDeviceTwin = new Mock(); + + mockDeviceTwin.SetupGet(c => c.Properties) + .Returns(new TwinProperties() { Desired = new TwinCollection($"{{\"DevAddr\": \"{new DevAddr(100)}\", \"ClassType\": \"C\", \"GatewayID\":\"mygateway\"}}"), Reported = new TwinCollection(), - } - }; + }); - var query = new Mock(MockBehavior.Strict); + var query = new Mock>(MockBehavior.Strict); query.Setup(x => x.HasMoreResults).Returns(true); - query.Setup(x => x.GetNextAsTwinAsync()) - .ReturnsAsync(new[] { deviceTwin }); + query.Setup(x => x.GetNextPageAsync()) + .ReturnsAsync(new[] { mockDeviceTwin.Object }); - this.registryManager.Setup(x => x.CreateQuery(It.IsNotNull(), It.IsAny())) + this.registryManager.Setup(x => x.FindDeviceByDevEUI(It.IsNotNull())) .Returns(query.Object); var actualMessage = new LoRaCloudToDeviceMessage() @@ -355,13 +409,21 @@ public async Task When_Querying_Devices_And_Finds_Single_Gateway_Class_C_Should_ }; LoRaCloudToDeviceMessage receivedC2DMessage = null; - this.serviceClient.Setup(x => x.InvokeDeviceMethodAsync("mygateway", LoraKeysManagerFacadeConstants.NetworkServerModuleId, It.IsNotNull())) - .Callback((device, methodName, method) => receivedC2DMessage = JsonConvert.DeserializeObject(method.GetPayloadAsJson())) - .ReturnsAsync(new CloudToDeviceMethodResult() { Status = (int)HttpStatusCode.OK }); + if (isEdgeDevice) + { + this.serviceClient.Setup(x => x.InvokeDeviceMethodAsync("mygateway", Constants.NetworkServerModuleId, It.IsNotNull(), default)) + .Callback((device, methodName, method, _) => receivedC2DMessage = JsonConvert.DeserializeObject(method.GetPayloadAsJson())) + .ReturnsAsync(new CloudToDeviceMethodResult() { Status = (int)HttpStatusCode.OK }); + } + else + { + this.channelPublisher.Setup(x => x.PublishAsync("mygateway", It.IsNotNull())) + .Callback((device, remoteCall) => receivedC2DMessage = JsonConvert.DeserializeObject(remoteCall.JsonData)); + } var actual = await this.sendCloudToDeviceMessage.SendCloudToDeviceMessageImplementationAsync( devEui, - actualMessage); + actualMessage, default); Assert.IsType(actual); var responseValue = ((OkObjectResult)actual).Value as SendCloudToDeviceMessageResult; @@ -387,21 +449,21 @@ public async Task When_Querying_Devices_And_Finds_No_Gateway_For_Class_C_Should_ { var devEui = new DevEui(0123456789); var devAddr = new DevAddr(03010101); - var deviceTwin = new Twin - { - Properties = new TwinProperties() + var mockDeviceTwin = new Mock(); + + mockDeviceTwin.SetupGet(c => c.Properties) + .Returns(new TwinProperties() { Desired = new TwinCollection($"{{\"DevAddr\": \"{devAddr}\", \"ClassType\": \"C\"}}"), Reported = new TwinCollection(), - } - }; + }); - var query = new Mock(MockBehavior.Strict); + var query = new Mock>(MockBehavior.Strict); query.Setup(x => x.HasMoreResults).Returns(true); - query.Setup(x => x.GetNextAsTwinAsync()) - .ReturnsAsync(new[] { deviceTwin }); + query.Setup(x => x.GetNextPageAsync()) + .ReturnsAsync(new[] { mockDeviceTwin.Object }); - this.registryManager.Setup(x => x.CreateQuery(It.IsNotNull(), It.IsAny())) + this.registryManager.Setup(x => x.FindDeviceByDevEUI(It.IsNotNull())) .Returns(query.Object); var actualMessage = new LoRaCloudToDeviceMessage() @@ -412,7 +474,7 @@ public async Task When_Querying_Devices_And_Finds_No_Gateway_For_Class_C_Should_ var actual = await this.sendCloudToDeviceMessage.SendCloudToDeviceMessageImplementationAsync( devEui, - actualMessage); + actualMessage, default); var result = Assert.IsType(actual); Assert.Equal(500, result.StatusCode); @@ -428,20 +490,20 @@ public async Task When_Querying_Devices_And_Finds_Class_A_Should_Send_Message() { var devEui = new DevEui(123456789); - var deviceTwin = new Twin - { - Properties = new TwinProperties() + var mockDeviceTwin = new Mock(); + + mockDeviceTwin.SetupGet(c => c.Properties) + .Returns(new TwinProperties() { Desired = new TwinCollection($"{{\"DevAddr\": \"03010101\"}}"), - } - }; + }); - var query = new Mock(MockBehavior.Strict); + var query = new Mock>(MockBehavior.Strict); query.Setup(x => x.HasMoreResults).Returns(true); - query.Setup(x => x.GetNextAsTwinAsync()) - .ReturnsAsync(new[] { deviceTwin }); + query.Setup(x => x.GetNextPageAsync()) + .ReturnsAsync(new[] { mockDeviceTwin.Object }); - this.registryManager.Setup(x => x.CreateQuery(It.IsNotNull(), It.IsAny())) + this.registryManager.Setup(x => x.FindDeviceByDevEUI(It.IsNotNull())) .Returns(query.Object); var actualMessage = new LoRaCloudToDeviceMessage() @@ -459,7 +521,7 @@ public async Task When_Querying_Devices_And_Finds_Class_A_Should_Send_Message() var actual = await this.sendCloudToDeviceMessage.SendCloudToDeviceMessageImplementationAsync( devEui, - actualMessage); + actualMessage, default); Assert.IsType(actual); var responseValue = ((OkObjectResult)actual).Value as SendCloudToDeviceMessageResult; @@ -483,20 +545,20 @@ public async Task When_Sending_Message_Throws_Error_Should_Return_Application_Er { var devEui = new DevEui(123456789); - var deviceTwin = new Twin - { - Properties = new TwinProperties() + var mockDeviceTwin = new Mock(); + + mockDeviceTwin.SetupGet(c => c.Properties) + .Returns(new TwinProperties() { Desired = new TwinCollection($"{{\"DevAddr\": \"03010101\"}}"), - } - }; + }); - var query = new Mock(MockBehavior.Strict); + var query = new Mock>(MockBehavior.Strict); query.Setup(x => x.HasMoreResults).Returns(true); - query.Setup(x => x.GetNextAsTwinAsync()) - .ReturnsAsync(new[] { deviceTwin }); + query.Setup(x => x.GetNextPageAsync()) + .ReturnsAsync(new[] { mockDeviceTwin.Object }); - this.registryManager.Setup(x => x.CreateQuery(It.IsNotNull(), It.IsAny())) + this.registryManager.Setup(x => x.FindDeviceByDevEUI(It.IsNotNull())) .Returns(query.Object); var actualMessage = new LoRaCloudToDeviceMessage() @@ -511,7 +573,7 @@ public async Task When_Sending_Message_Throws_Error_Should_Return_Application_Er var actual = await this.sendCloudToDeviceMessage.SendCloudToDeviceMessageImplementationAsync( devEui, - actualMessage); + actualMessage, default); Assert.IsType(actual); Assert.Equal((int)HttpStatusCode.InternalServerError, ((ObjectResult)actual).StatusCode); diff --git a/Tests/Unit/NetworkServer/ADRMessageProcessorTest.cs b/Tests/Unit/NetworkServer/ADRMessageProcessorTest.cs index 5f6af0c8ef..bccaef13f0 100644 --- a/Tests/Unit/NetworkServer/ADRMessageProcessorTest.cs +++ b/Tests/Unit/NetworkServer/ADRMessageProcessorTest.cs @@ -51,11 +51,12 @@ public async Task Perform_Rate_Adapatation_When_Possible(uint deviceId, int coun .ReturnsAsync(true); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -182,11 +183,12 @@ public async Task Perform_DR_Adaptation_When_Needed(uint deviceId, float current .ReturnsAsync(true); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -282,11 +284,12 @@ public async Task Perform_TXPower_Adaptation_When_Needed() .ReturnsAsync(true); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -422,11 +425,12 @@ public async Task Perform_NbRep_Adaptation_When_Needed() .ReturnsAsync(true); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); diff --git a/Tests/Unit/NetworkServer/ApplicationInsightsTracingTests.cs b/Tests/Unit/NetworkServer/ApplicationInsightsTracingTests.cs new file mode 100644 index 0000000000..7423014fa0 --- /dev/null +++ b/Tests/Unit/NetworkServer/ApplicationInsightsTracingTests.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Unit.NetworkServer +{ + using System; + using System.Collections.Generic; + using LoRaWan.NetworkServer; + using Microsoft.ApplicationInsights; + using Microsoft.ApplicationInsights.Channel; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Extensibility; + using Xunit; + + public sealed class ApplicationInsightsTracingTests : IDisposable + { + private static readonly NetworkServerConfiguration NetworkServerConfiguration = new NetworkServerConfiguration { IoTHubHostName = "somehub.azure-devices.net" }; + private readonly StubTelemetryChannel stubTelemetryChannel; + private readonly TelemetryConfiguration configuration; + + public ApplicationInsightsTracingTests() + { + this.stubTelemetryChannel = new StubTelemetryChannel(); + this.configuration = new TelemetryConfiguration + { + TelemetryChannel = this.stubTelemetryChannel, + ConnectionString = $"InstrumentationKey={Guid.NewGuid()};IngestionEndpoint=https://westeurope-2.in.applicationinsights.azure.com/;LiveEndpoint=https://westeurope.livediagnostics.monitor.azure.com/", + TelemetryInitializers = { new OperationCorrelationTelemetryInitializer() } + }; + } + + [Fact] + public void TrackDataMessage_Starts_ApplicationInsights_Operation() + { + // arrange + var subject = Setup(); + + // act + using (var operationHolder = subject.TrackDataMessage()) { /* noop */ } + + // assert + var telemetry = Assert.Single(this.stubTelemetryChannel.SentTelemetry); + var requestTelemetry = Assert.IsType(telemetry); + Assert.Equal("Data message", requestTelemetry.Name); + } + + [Fact] + public void TrackIotHubDependency_Starts_ApplicationInsights_Operation() + { + // arrange + const string dependencyName = "SDK GetTwin"; + const string data = "id=deviceFoo"; + var subject = Setup(); + + // act + using (var operationHolder = subject.TrackIotHubDependency(dependencyName, data)) { /* noop */ } + + // assert + var telemetry = Assert.Single(this.stubTelemetryChannel.SentTelemetry); + var dependencyTelemetry = Assert.IsType(telemetry); + Assert.Equal("Azure IoT Hub", dependencyTelemetry.Type); + Assert.Equal(NetworkServerConfiguration.IoTHubHostName, dependencyTelemetry.Target); + Assert.Equal($"{dependencyName} (Gateway)", dependencyTelemetry.Name); + Assert.Equal(data, dependencyTelemetry.Data); + } + + [Theory] + [InlineData(true, "(Gateway)")] + [InlineData(false, "(Direct)")] + public void TrackIotHubDependency_Tracks_Gateway_Or_Direct_Mode(bool enableGateway, string expectedSuffix) + { + // arrange + var networkServerConfiguration = new NetworkServerConfiguration + { + IoTHubHostName = NetworkServerConfiguration.IoTHubHostName, + EnableGateway = enableGateway + }; + var subject = Setup(networkServerConfiguration); + + // act + using (var operationHolder = subject.TrackIotHubDependency("foo", "bar")) { /* noop */ } + + // assert + var telemetry = Assert.Single(this.stubTelemetryChannel.SentTelemetry); + var dependencyTelemetry = Assert.IsType(telemetry); + Assert.True(dependencyTelemetry.Name.EndsWith(expectedSuffix, StringComparison.Ordinal), $"Expected '{dependencyTelemetry.Name}' to end with '{expectedSuffix}'."); + } + + public void Dispose() + { + this.stubTelemetryChannel.Dispose(); + this.configuration.Dispose(); + } + + private ApplicationInsightsTracing Setup() => Setup(NetworkServerConfiguration); + + private ApplicationInsightsTracing Setup(NetworkServerConfiguration networkServerConfiguration) => + new ApplicationInsightsTracing(new TelemetryClient(this.configuration), networkServerConfiguration); + + private sealed class StubTelemetryChannel : ITelemetryChannel + { + private readonly List sentTelemetry = new List(); + + public IReadOnlyList SentTelemetry => this.sentTelemetry; + + public bool? DeveloperMode { get; set; } = false; + public string EndpointAddress { get; set; } = "https://sometesturi.ms"; + + public void Dispose() { } + + public void Flush() { } + + public void Send(ITelemetry item) => this.sentTelemetry.Add(item); + } + } +} diff --git a/Tests/Unit/NetworkServer/BasicsStation/BasicsStationConfigurationServiceTests.cs b/Tests/Unit/NetworkServer/BasicsStation/BasicsStationConfigurationServiceTests.cs index 36e5103f6f..5d37e20c48 100644 --- a/Tests/Unit/NetworkServer/BasicsStation/BasicsStationConfigurationServiceTests.cs +++ b/Tests/Unit/NetworkServer/BasicsStation/BasicsStationConfigurationServiceTests.cs @@ -233,7 +233,7 @@ public async Task Fails_WithoutProperty() // arrange const string primaryKey = "foo"; SetupDeviceKeyLookup(primaryKey); - SetupTwinResponse(primaryKey, JsonUtil.Strictify("{ 'anotherProp': '1'}")); + SetupTwinResponse(primaryKey, JsonUtil.Strictify(/*lang=json*/ "{ 'anotherProp': '1'}")); // act and assert var exception = await Assert.ThrowsAsync(() => this.sut.GetAllowedClientThumbprintsAsync(this.stationEui, CancellationToken.None)); @@ -246,7 +246,7 @@ public async Task Fails_With_InvalidCast() // arrange const string primaryKey = "foo"; SetupDeviceKeyLookup(primaryKey); - SetupTwinResponse(primaryKey, JsonUtil.Strictify("{ 'clientThumbprint': 'x'}")); + SetupTwinResponse(primaryKey, JsonUtil.Strictify(/*lang=json*/ "{ 'clientThumbprint': 'x'}")); // act and assert var exception = await Assert.ThrowsAsync(() => this.sut.GetAllowedClientThumbprintsAsync(this.stationEui, CancellationToken.None)); @@ -281,7 +281,7 @@ public async Task Fails_WithoutProperty() // arrange const string primaryKey = "foo"; SetupDeviceKeyLookup(primaryKey); - SetupTwinResponse(primaryKey, JsonUtil.Strictify("{ 'anotherProp': '1'}")); + SetupTwinResponse(primaryKey, JsonUtil.Strictify(/*lang=json*/ "{ 'anotherProp': '1'}")); // act and assert var exception = await Assert.ThrowsAsync(() => this.sut.GetCupsConfigAsync(this.stationEui, CancellationToken.None)); @@ -331,7 +331,7 @@ public async Task Caches_And_Handles_Concurrent_Access() select this.sut.GetRouterConfigMessageAsync(this.stationEui, CancellationToken.None)); // assert - Assert.Equal(result.Length, numberOfConcurrentAccess); + Assert.Equal(numberOfConcurrentAccess, result.Length); this.loRaDeviceFactoryMock.Verify(ldf => ldf.CreateDeviceClient(It.IsAny(), It.IsAny()), Times.Once); this.loRaDeviceApiServiceMock.Verify(ldf => ldf.GetPrimaryKeyByEuiAsync(It.IsAny()), Times.Once); foreach (var r in result) diff --git a/Tests/Unit/NetworkServer/BasicsStation/BasicsStationNetworkServerStartupTests.cs b/Tests/Unit/NetworkServer/BasicsStation/BasicsStationNetworkServerStartupTests.cs index 76ecb2383e..e940590c78 100644 --- a/Tests/Unit/NetworkServer/BasicsStation/BasicsStationNetworkServerStartupTests.cs +++ b/Tests/Unit/NetworkServer/BasicsStation/BasicsStationNetworkServerStartupTests.cs @@ -3,7 +3,9 @@ namespace LoRaWan.Tests.Unit.NetworkServer.BasicsStation { + using System; using LoRaWan.NetworkServer.BasicsStation; + using LoRaWan.NetworkServer.BasicsStation.ModuleConnection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -16,16 +18,83 @@ public void All_Dependencies_Are_Registered_Correctly() // arrange var services = new ServiceCollection(); var config = new ConfigurationBuilder().Build(); + var envVariables = new[] + { + ("HOSTNAME", "test"), + ("IOTHUBHOSTNAME", "test") + }; + + try + { + foreach (var (key, value) in envVariables) + Environment.SetEnvironmentVariable(key, value); + + // act + assert + var startup = new BasicsStationNetworkServerStartup(config); + startup.ConfigureServices(services); + + services.BuildServiceProvider(new ServiceProviderOptions + { + ValidateOnBuild = true, + ValidateScopes = true + }); + + } + finally + { + foreach (var (key, _) in envVariables) + Environment.SetEnvironmentVariable(key, string.Empty); + } + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + public void ModuleConnectionHostIsInjectedOrNot(bool cloud_deployment, bool enable_gateway) + { + var envVariables = new[] + { + ("CLOUD_DEPLOYMENT", cloud_deployment.ToString()), + ("ENABLE_GATEWAY", enable_gateway.ToString()), + ("REDIS_CONNECTION_STRING", "someString"), + ("HOSTNAME", "test"), + ("IOTHUBHOSTNAME", "test") + }; + + try + { + foreach (var (key, value) in envVariables) + Environment.SetEnvironmentVariable(key, value); + + var services = new ServiceCollection(); + var config = new ConfigurationBuilder().Build(); + + // act + assert + var startup = new BasicsStationNetworkServerStartup(config); + startup.ConfigureServices(services); + + var serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions + { + ValidateOnBuild = true, + ValidateScopes = true + }); - // act + assert - var startup = new BasicsStationNetworkServerStartup(config); - startup.ConfigureServices(services); + var result = serviceProvider.GetService(); + if (cloud_deployment) + { + Assert.Null(result); + } + else + { + Assert.NotNull(result); + } - services.BuildServiceProvider(new ServiceProviderOptions + } + finally { - ValidateOnBuild = true, - ValidateScopes = true - }); + foreach (var (key, _) in envVariables) + Environment.SetEnvironmentVariable(key, string.Empty); + } } } } diff --git a/Tests/Unit/NetworkServer/BasicsStation/ClientCertificateValidatorServiceTests.cs b/Tests/Unit/NetworkServer/BasicsStation/ClientCertificateValidatorServiceTests.cs index 6a77d37736..d0dda463b5 100644 --- a/Tests/Unit/NetworkServer/BasicsStation/ClientCertificateValidatorServiceTests.cs +++ b/Tests/Unit/NetworkServer/BasicsStation/ClientCertificateValidatorServiceTests.cs @@ -11,6 +11,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer.BasicsStation using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; + using Common; using LoRaWan.NetworkServer.BasicsStation; using Microsoft.Extensions.Logging; using Moq; @@ -96,7 +97,7 @@ public async Task ValidateAsync_ReturnsFalse_WithInvalidCNCertificate() var result = await this.clientCertValidatorSvc.ValidateAsync(cert, chain, System.Net.Security.SslPolicyErrors.None, default); Assert.False(result); - Assert.Contains(this.logger.Invocations, i => i.Arguments.Any(a => a.ToString()!.Contains(InvalidStationEui, StringComparison.OrdinalIgnoreCase))); + Assert.Contains(this.logger.GetLogInvocations(), args => args.Message.Contains(InvalidStationEui, StringComparison.OrdinalIgnoreCase)); } finally { diff --git a/Tests/Unit/NetworkServer/BasicsStation/DownstreamSenderTests.cs b/Tests/Unit/NetworkServer/BasicsStation/DownstreamSenderTests.cs index 29b0aac727..5267257aa6 100644 --- a/Tests/Unit/NetworkServer/BasicsStation/DownstreamSenderTests.cs +++ b/Tests/Unit/NetworkServer/BasicsStation/DownstreamSenderTests.cs @@ -19,7 +19,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer.BasicsStation public class DownstreamSenderTests { - private const string loraDataBase64 = "REFUQQ=="; + private const string LoraDataBase64 = "REFUQQ=="; private readonly StationEui stationEui = new StationEui(ulong.MaxValue); private readonly DevEui devEui = new DevEui(ulong.MaxValue); private readonly Mock> webSocketWriter; @@ -37,7 +37,7 @@ public DownstreamSenderTests() socketWriterRegistry.Register(stationEui, this.webSocketWriter.Object); - loraDataByteArray = Encoding.UTF8.GetBytes(loraDataBase64); + loraDataByteArray = Encoding.UTF8.GetBytes(LoraDataBase64); downlinkSender = new DownstreamMessageSender(socketWriterRegistry, basicStationConfigurationService.Object, diff --git a/Tests/Unit/NetworkServer/BasicsStation/LocalLnsDiscoveryTests.cs b/Tests/Unit/NetworkServer/BasicsStation/LocalLnsDiscoveryTests.cs new file mode 100644 index 0000000000..6c3b6f7ab7 --- /dev/null +++ b/Tests/Unit/NetworkServer/BasicsStation/LocalLnsDiscoveryTests.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Unit.NetworkServer.BasicsStation +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using LoRaWan.NetworkServer.BasicsStation; + using Xunit; + + public sealed class LocalLnsDiscoveryTests + { + [Fact] + public async Task ResolveLnsAsync_Returns_Initialization_Uri() + { + // arrange + var url = new Uri("wss://localhost:5001/router-data"); + var subject = new LocalLnsDiscovery(url); + + // act + var result = await subject.ResolveLnsAsync(new StationEui(1), CancellationToken.None); + + // assert + Assert.Equal(url, result); + } + } +} diff --git a/Tests/Unit/NetworkServer/CloudControlHostTests.cs b/Tests/Unit/NetworkServer/CloudControlHostTests.cs new file mode 100644 index 0000000000..0ef185c8ef --- /dev/null +++ b/Tests/Unit/NetworkServer/CloudControlHostTests.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Unit.NetworkServer +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using global::LoRaTools; + using LoRaWan.NetworkServer; + using Moq; + using Xunit; + + public sealed class CloudControlHostTests + { + private const string GatewayId = "lns-1"; + private readonly Mock lnsRemoteCallHandler; + private readonly Mock lnsRemoteCallListener; + private readonly CloudControlHost subject; + + public CloudControlHostTests() + { + this.lnsRemoteCallHandler = new Mock(); + this.lnsRemoteCallListener = new Mock(); + this.subject = new CloudControlHost(this.lnsRemoteCallListener.Object, this.lnsRemoteCallHandler.Object, new NetworkServerConfiguration { GatewayID = GatewayId }); + } + + [Fact] + public async Task ExecuteAsync_Subscribes_To_LnsRemoteCallHandler() + { + // arrange + Func actualHandler = _ => Task.CompletedTask; + this.lnsRemoteCallListener + .Setup(l => l.SubscribeAsync(GatewayId, It.IsAny>(), It.IsAny())) + .Callback((string _, Func handler, CancellationToken _) => actualHandler = handler); + + // act + await this.subject.StartAsync(CancellationToken.None); + + // assert + this.lnsRemoteCallListener.Verify(l => l.SubscribeAsync(GatewayId, It.IsAny>(), It.IsAny())); + await actualHandler.Invoke(new LnsRemoteCall(RemoteCallKind.CloseConnection, null)); + this.lnsRemoteCallHandler.Verify(l => l.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task StopAsync_Unsubscribes() + { + // act + await this.subject.StopAsync(CancellationToken.None); + + // assert + this.lnsRemoteCallListener.Verify(l => l.UnsubscribeAsync(GatewayId, It.IsAny()), Times.Once); + } + } +} diff --git a/Tests/Unit/NetworkServer/ConcentratorDeduplicationTest.cs b/Tests/Unit/NetworkServer/ConcentratorDeduplicationTest.cs index 92ad86f371..21a3c2966a 100644 --- a/Tests/Unit/NetworkServer/ConcentratorDeduplicationTest.cs +++ b/Tests/Unit/NetworkServer/ConcentratorDeduplicationTest.cs @@ -6,6 +6,8 @@ namespace LoRaWan.Tests.Unit.NetworkServer { using System; + using System.Threading.Tasks; + using System.Linq; using global::LoRaTools.LoRaMessage; using LoRaWan.NetworkServer; using LoRaWan.Tests.Common; @@ -13,7 +15,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer using Microsoft.Extensions.Logging.Abstractions; using Xunit; - public sealed class ConcentratorDeduplicationTest : IDisposable + public sealed class ConcentratorDeduplicationTest : IAsyncDisposable { private readonly MemoryCache cache; // ownership passed to ConcentratorDeduplication private readonly LoRaDeviceClientConnectionManager connectionManager; @@ -51,12 +53,12 @@ public ConcentratorDeduplicationTest() [Theory] [InlineData(true)] [InlineData(false)] - public void When_Data_Message_Not_Encountered_Should_Not_Find_Duplicates_And_Should_Add_To_Cache(bool isCacheEmpty) + public async Task When_Data_Message_Not_Encountered_Should_Not_Find_Duplicates_And_Should_Add_To_Cache(bool isCacheEmpty) { // arrange if (!isCacheEmpty) { - using var testDevice = new LoRaDevice(this.simulatedABPDevice.DevAddr, new DevEui(0x1111111111111111UL), this.connectionManager); + await using var testDevice = new LoRaDevice(this.simulatedABPDevice.DevAddr, new DevEui(0x1111111111111111UL), this.connectionManager); _ = this.concentratorDeduplication.CheckDuplicateData(this.dataRequest, testDevice); } @@ -95,30 +97,39 @@ public void When_Data_Message_Encountered_Should_Find_Duplicates_For_Different_D Assert.Equal(station1Eui, foundStation); } - public static TheoryData CreateKeyDataMessagesTheoryData - => TheoryDataFactory.From( - new (object, ulong, ushort, ushort, string?)[] - { - (new ConcentratorDeduplication.DataMessageKey(new DevEui(0), new Mic(0), 0), 0, 0, 0, null), - (new ConcentratorDeduplication.DataMessageKey(new DevEui(0), new Mic(0), 0), 0, 0, 0, "1"), // a non-relevant field should not influence the key - (new ConcentratorDeduplication.DataMessageKey(new DevEui(0x1010101010101010UL), new Mic(0), 0), 0x1010101010101010UL, 0, 0, null), - (new ConcentratorDeduplication.DataMessageKey(new DevEui(0), new Mic(1), 0), 0, 1, 0, null), - (new ConcentratorDeduplication.DataMessageKey(new DevEui(0), new Mic(0), 1), 0, 0, 1, null) - } - ); + // Ensures that ConcentratorDeduplication.DataMessageKey can be internal, even though it is part of the return type of RawCreateKeyDataMessagesTheoryData. + public sealed class DataMessageKeyHolder + { + internal DataMessageKeyHolder(ConcentratorDeduplication.DataMessageKey dataMessageKey) => Value = dataMessageKey; + internal ConcentratorDeduplication.DataMessageKey Value { get; } + } + + private static readonly (DevEui DevEui, Mic Mic, ushort FCnt, string? FieldNotUsedInKey)[] RawCreateKeyDataMessagesTheoryData = new (DevEui, Mic, ushort, string?)[] + { + (new DevEui(0), new Mic(0), 0, null), + (new DevEui(0), new Mic(0), 0, "1"), // a non-relevant field should not influence the key + (new DevEui(0x1010101010101010UL), new Mic(0), 0, null), + (new DevEui(0), new Mic(1), 0, null), + (new DevEui(0), new Mic(0), 1, null) + }; + + public static TheoryData CreateKeyDataMessagesTheoryData => + TheoryDataFactory.From(from dataPoint in RawCreateKeyDataMessagesTheoryData + select (new DataMessageKeyHolder(new ConcentratorDeduplication.DataMessageKey(dataPoint.DevEui, dataPoint.Mic, dataPoint.FCnt)), + dataPoint.DevEui, dataPoint.Mic, dataPoint.FCnt, dataPoint.FieldNotUsedInKey)); [Theory] [MemberData(nameof(CreateKeyDataMessagesTheoryData))] - internal void CreateKeyMethod_Should_Return_Expected_Keys_For_Different_Data_Messages(ConcentratorDeduplication.DataMessageKey expectedKey, ulong devEui, ushort mic, ushort frameCounter, string? fieldNotUsedInKey = null) + public async Task CreateKeyMethod_Should_Return_Expected_Keys_For_Different_Data_Messages(DataMessageKeyHolder expectedKey, DevEui devEui, Mic mic, ushort frameCounter, string? fieldNotUsedInKey = null) { var options = fieldNotUsedInKey ?? string.Empty; - using var testDevice = new LoRaDevice(this.simulatedABPDevice.DevAddr, new DevEui(devEui), this.connectionManager); + await using var testDevice = new LoRaDevice(this.simulatedABPDevice.DevAddr, devEui, this.connectionManager); var payload = new LoRaPayloadData(this.dataPayload.DevAddr, new MacHeader(MacMessageType.ConfirmedDataUp), - FrameControlFlags.None, frameCounter, options, "payload", FramePort.AppMin, new Mic(mic), + FrameControlFlags.None, frameCounter, options, "payload", FramePort.AppMin, mic, NullLogger.Instance); - Assert.Equal(expectedKey, ConcentratorDeduplication.CreateCacheKey(payload, testDevice)); + Assert.Equal(expectedKey.Value, ConcentratorDeduplication.CreateCacheKey(payload, testDevice)); } #endregion @@ -170,37 +181,43 @@ public void When_Join_Request_Encountered_Should_Find_Duplicate(string station1, Assert.Equal(station1Eui, addedStation); } - public static TheoryData CreateKeyJoinMessagesTheoryData - => TheoryDataFactory.From( - new (object, ulong, ulong, ushort, int?)[] - { - (new ConcentratorDeduplication.JoinMessageKey(new JoinEui(0), new DevEui(0), new DevNonce(0)), 0, 0, 0, null), - (new ConcentratorDeduplication.JoinMessageKey(new JoinEui(0), new DevEui(0), new DevNonce(0)), 0, 0, 0, 1 ), // a non-relevant field should not influence the key - (new ConcentratorDeduplication.JoinMessageKey(new JoinEui(0x1010101010101010UL), new DevEui(0), new DevNonce(0)), 0x1010101010101010UL, 0, 0, null), - (new ConcentratorDeduplication.JoinMessageKey(new JoinEui(0), new DevEui(0x1010101010101010UL), new DevNonce(0)), 0, 0x1010101010101010UL, 0, null), - (new ConcentratorDeduplication.JoinMessageKey(new JoinEui(0), new DevEui(0), new DevNonce(1)), 0, 0, 1, null), - } - ); + // Ensures that ConcentratorDeduplication.JoinMessageKey can be internal, even though it is part of the return type of RawCreateKeyJoinMessagesTheoryData. + public sealed class JoinMessageKeyHolder + { + internal JoinMessageKeyHolder(ConcentratorDeduplication.JoinMessageKey joinMessageKey) => Value = joinMessageKey; + internal ConcentratorDeduplication.JoinMessageKey Value { get; } + } + + private static readonly (JoinEui JoinEui, DevEui DevEui, DevNonce DevNonce, int? FieldNotUsedInKey)[] RawCreateKeyJoinMessagesTheoryData = new (JoinEui, DevEui, DevNonce, int?)[] + { + (new JoinEui(0), new DevEui(0), new DevNonce(0), null), + (new JoinEui(0), new DevEui(0), new DevNonce(0), 1), + (new JoinEui(0x1010101010101010UL), new DevEui(0), new DevNonce(0), null), + (new JoinEui(0), new DevEui(0x1010101010101010UL), new DevNonce(0), null), + (new JoinEui(0), new DevEui(0), new DevNonce(1), null), + }; + + public static TheoryData CreateKeyJoinMessagesTheoryData => + TheoryDataFactory.From(from dataPoint in RawCreateKeyJoinMessagesTheoryData + select (new JoinMessageKeyHolder(new ConcentratorDeduplication.JoinMessageKey(dataPoint.JoinEui, dataPoint.DevEui, dataPoint.DevNonce)), + dataPoint.JoinEui, dataPoint.DevEui, dataPoint.DevNonce, new Mic(dataPoint.FieldNotUsedInKey ?? 0))); [Theory] [MemberData(nameof(CreateKeyJoinMessagesTheoryData))] - internal void CreateCacheKey_Should_Return_Expected_Keys_For_Different_JoinRequests(ConcentratorDeduplication.JoinMessageKey expectedKey, ulong joinEui, ulong devEui, ushort devNonce, int? fieldNotUsedInKey = null) + public void CreateCacheKey_Should_Return_Expected_Keys_For_Different_JoinRequests(JoinMessageKeyHolder expectedKey, JoinEui joinEui, DevEui devEui, DevNonce devNonce, Mic mic) { - var micValue = fieldNotUsedInKey ?? 0; - var payload = new LoRaPayloadJoinRequest(new JoinEui(joinEui), new DevEui(devEui), - new DevNonce(devNonce), new Mic(micValue)); - - Assert.Equal(expectedKey, ConcentratorDeduplication.CreateCacheKey(payload)); + var payload = new LoRaPayloadJoinRequest(joinEui, devEui, devNonce, mic); + Assert.Equal(expectedKey.Value, ConcentratorDeduplication.CreateCacheKey(payload)); } #endregion - public void Dispose() + public async ValueTask DisposeAsync() { - this.loRaDevice.Dispose(); + await this.loRaDevice.DisposeAsync(); this.dataRequest.Dispose(); this.joinRequest.Dispose(); - this.connectionManager.Dispose(); + await this.connectionManager.DisposeAsync(); this.cache?.Dispose(); } } diff --git a/Tests/Unit/NetworkServer/ConfigurationTest.cs b/Tests/Unit/NetworkServer/ConfigurationTest.cs index e1607fb7e0..93e2beaf98 100644 --- a/Tests/Unit/NetworkServer/ConfigurationTest.cs +++ b/Tests/Unit/NetworkServer/ConfigurationTest.cs @@ -14,7 +14,13 @@ public class ConfigurationTest [MemberData(nameof(AllowedDevAddressesInput))] public void Should_Setup_Allowed_Dev_Addresses_Correctly(string inputAllowedDevAddrValues, DevAddr[] expectedAllowedDevAddrValues) { - var envVariables = new[] { ("AllowedDevAddresses", inputAllowedDevAddrValues), ("FACADE_SERVER_URL", "https://aka.ms") }; + var envVariables = new[] + { + ("AllowedDevAddresses", inputAllowedDevAddrValues), + ("FACADE_SERVER_URL", "https://aka.ms"), + ("HOSTNAME", "test"), + ("IOTHUBHOSTNAME", "test") + }; try { @@ -35,9 +41,117 @@ public void Should_Setup_Allowed_Dev_Addresses_Correctly(string inputAllowedDevA } } + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void Should_Throw_On_Invalid_Cloud_Configuration_When_Redis_Connection_String_Not_Set(bool shouldSetRedisString, bool isCloudDeployment) + { + // arrange + var cloudDeploymentKey = "CLOUD_DEPLOYMENT"; + var key = "REDIS_CONNECTION_STRING"; + var value = "someValue"; + var lnsConfigurationCreation = () => NetworkServerConfiguration.CreateFromEnvironmentVariables(); + + Environment.SetEnvironmentVariable("HOSTNAME", "test"); + Environment.SetEnvironmentVariable("IOTHUBHOSTNAME", "test"); + + if (isCloudDeployment) + { + Environment.SetEnvironmentVariable(cloudDeploymentKey, true.ToString()); + Environment.SetEnvironmentVariable("ENABLE_GATEWAY", false.ToString()); + } + + if (shouldSetRedisString) + Environment.SetEnvironmentVariable(key, value); + + // act and assert + if (isCloudDeployment && !shouldSetRedisString) + { + _ = Assert.Throws(lnsConfigurationCreation); + } + else + { + _ = lnsConfigurationCreation(); + } + + Environment.SetEnvironmentVariable(key, string.Empty); + Environment.SetEnvironmentVariable(cloudDeploymentKey, string.Empty); + Environment.SetEnvironmentVariable("ENABLE_GATEWAY", string.Empty); + } + + public static TheoryData AllowedDevAddressesInput => TheoryDataFactory.From(("0228B1B1;", new[] { new DevAddr(0x0228b1b1) }), ("0228B1B1;0228B1B2", new DevAddr[] { new DevAddr(0x0228b1b1), new DevAddr(0x0228b1b2) }), ("ads;0228B1B2;", new DevAddr[] { new DevAddr(0x0228b1b2) })); + + [Theory] + [CombinatorialData] + public void EnableGatewayTrue_IoTModuleFalse_IsNotSupported(bool cloud_deployment, bool enable_gateway) + { + var envVariables = new[] + { + ("CLOUD_DEPLOYMENT", cloud_deployment.ToString()), + ("ENABLE_GATEWAY", enable_gateway.ToString()), + ("REDIS_CONNECTION_STRING", "someString"), + ("HOSTNAME", "test"), + ("IOTHUBHOSTNAME", "test") + }; + + try + { + foreach (var (key, value) in envVariables) + Environment.SetEnvironmentVariable(key, value); + + if (cloud_deployment && enable_gateway) + { + Assert.Throws(() => { + _ = NetworkServerConfiguration.CreateFromEnvironmentVariables(); + }); + } + else + { + _ = NetworkServerConfiguration.CreateFromEnvironmentVariables(); + } + } + finally + { + foreach (var (key, _) in envVariables) + Environment.SetEnvironmentVariable(key, string.Empty); + } + } + + [Theory] + [InlineData("500")] + [InlineData("x")] + public void ProcessingDelayIsConfigurable(string processing_delay) + { + var envVariables = new[] + { + ("PROCESSING_DELAY_IN_MS", processing_delay), + ("HOSTNAME", "test") + }; + + try + { + foreach (var (key, value) in envVariables) + Environment.SetEnvironmentVariable(key, value); + + var networkServerConfiguration = NetworkServerConfiguration.CreateFromEnvironmentVariables(); + + if (!int.TryParse(processing_delay, out var int_processing_delay)) + { + int_processing_delay = Constants.DefaultProcessingDelayInMilliseconds; + } + Assert.Equal(int_processing_delay, networkServerConfiguration.ProcessingDelayInMilliseconds); + } + finally + { + foreach (var (key, _) in envVariables) + Environment.SetEnvironmentVariable(key, string.Empty); + } + } } } diff --git a/Tests/Unit/NetworkServer/CupsProtocolMessageProcessorTests.cs b/Tests/Unit/NetworkServer/CupsProtocolMessageProcessorTests.cs index ddc62be029..626e8d9abd 100644 --- a/Tests/Unit/NetworkServer/CupsProtocolMessageProcessorTests.cs +++ b/Tests/Unit/NetworkServer/CupsProtocolMessageProcessorTests.cs @@ -6,7 +6,6 @@ namespace LoRaWan.Tests.Unit.NetworkServer using System; using System.IO; using System.IO.Pipelines; - using System.Linq; using System.Net; using System.Net.Http; using System.Text; @@ -84,7 +83,7 @@ public async Task HandleUpdateInfoAsync_Succeeds() [InlineData(null)] [InlineData(1)] [InlineData(int.MaxValue)] - public async Task HandleUpdateInfoAsync_Fails_WithInvalidContentLength(long? requestContentLength) + public async Task HandleUpdateInfoAsync_Fails_WithInvalidContentLength(int? requestContentLength) { // setup var (httpContext, httpRequest, _) = SetupHttpContextWithRequest(CupsRequestJson, null); @@ -117,7 +116,7 @@ public async Task HandleUpdateInfoAsync_Fails_WithInvalidInput(string input, Typ // assert Assert.Equal((int)HttpStatusCode.BadRequest, httpContext.Object.Response.StatusCode); - Assert.Contains(this.logger.Invocations, i => i.Arguments.Any(a => a.GetType() == exceptionType)); + Assert.Contains(this.logger.GetLogInvocations(), args => args.Exception is { } exception && exception.GetType() == exceptionType); } [Fact] diff --git a/Tests/Unit/NetworkServer/DefaultClassCDevicesMessageSenderTest.cs b/Tests/Unit/NetworkServer/DefaultClassCDevicesMessageSenderTest.cs index 7cbe7604d6..a3fb099fb4 100644 --- a/Tests/Unit/NetworkServer/DefaultClassCDevicesMessageSenderTest.cs +++ b/Tests/Unit/NetworkServer/DefaultClassCDevicesMessageSenderTest.cs @@ -18,7 +18,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer using Moq; using Xunit; - public sealed class DefaultClassCDevicesMessageSenderTest : IDisposable + public sealed class DefaultClassCDevicesMessageSenderTest : IAsyncDisposable { private const string ServerGatewayID = "test-gateway"; private const FramePort TestPort = FramePorts.App10; @@ -73,17 +73,18 @@ private static void EnsureDownlinkIsCorrect(DownlinkMessage downlink, SimulatedD [InlineData(ServerGatewayID)] public async Task When_Sending_Message_Should_Send_Downlink_To_DownstreamMessageSender(string deviceGatewayID) { - var simDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, deviceClassType: 'c', gatewayID: deviceGatewayID)); + var simDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, deviceClassType: LoRaDeviceClassType.C, gatewayID: deviceGatewayID)); var devEUI = simDevice.DevEUI; this.deviceApi.Setup(x => x.GetPrimaryKeyByEuiAsync(devEUI)) .ReturnsAsync("123"); - var twin = simDevice.CreateABPTwin(reportedProperties: new Dictionary - { - { TwinProperty.Region, LoRaRegionType.EU868.ToString() }, - { TwinProperty.LastProcessingStationEui, new StationEui(ulong.MaxValue).ToString() } - }); + var twin = LoRaDeviceTwin.Create(simDevice.LoRaDevice.GetAbpDesiredTwinProperties(), + simDevice.GetAbpReportedTwinProperties() with + { + Region = LoRaRegionType.EU868, + LastProcessingStation = new StationEui(ulong.MaxValue) + }); this.deviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); @@ -136,7 +137,7 @@ public async Task When_Device_Is_Not_Class_C_Should_Fail() .ReturnsAsync("123"); this.deviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(simDevice.CreateABPTwin()); + .ReturnsAsync(simDevice.GetDefaultAbpTwin()); var c2dToDeviceMessage = new ReceivedLoRaCloudToDeviceMessage() { @@ -189,14 +190,14 @@ public async Task When_Fport_Is_Not_Set_Should_Fail() [Fact] public async Task When_Device_Is_Not_Joined_Should_Fail() { - var simDevice = new SimulatedDevice(TestDeviceInfo.CreateOTAADevice(1, deviceClassType: 'c', gatewayID: ServerGatewayID)); + var simDevice = new SimulatedDevice(TestDeviceInfo.CreateOTAADevice(1, deviceClassType: LoRaDeviceClassType.C, gatewayID: ServerGatewayID)); var devEUI = simDevice.DevEUI; this.deviceApi.Setup(x => x.GetPrimaryKeyByEuiAsync(devEUI)) .ReturnsAsync("123"); this.deviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(simDevice.CreateOTAATwin()); + .ReturnsAsync(LoRaDeviceTwin.Create(simDevice.LoRaDevice.GetOtaaDesiredTwinProperties(), simDevice.GetOtaaReportedTwinProperties())); var c2dToDeviceMessage = new ReceivedLoRaCloudToDeviceMessage() { @@ -224,17 +225,18 @@ public async Task When_Device_Is_Not_Joined_Should_Fail() [Fact] public async Task When_Fails_To_Get_FcntDown_Should_Fail() { - var simDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, deviceClassType: 'c')); + var simDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, deviceClassType: LoRaDeviceClassType.C)); var devEUI = simDevice.DevEUI; this.deviceApi.Setup(x => x.GetPrimaryKeyByEuiAsync(devEUI)) .ReturnsAsync("123"); - var twin = simDevice.CreateABPTwin(reportedProperties: new Dictionary - { - { TwinProperty.Region, LoRaRegionType.EU868.ToString() }, - { TwinProperty.LastProcessingStationEui, new StationEui(ulong.MaxValue).ToString() } - }); + var twin = LoRaDeviceTwin.Create(simDevice.LoRaDevice.GetAbpDesiredTwinProperties(), + simDevice.GetAbpReportedTwinProperties() with + { + Region = LoRaRegionType.EU868, + LastProcessingStation = new StationEui(ulong.MaxValue) + }); this.deviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); @@ -269,7 +271,7 @@ public async Task When_Fails_To_Get_FcntDown_Should_Fail() [Fact] public async Task When_Message_Is_Invalid_Should_Fail() { - var simDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, deviceClassType: 'c', gatewayID: ServerGatewayID)); + var simDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, deviceClassType: LoRaDeviceClassType.C, gatewayID: ServerGatewayID)); var devEUI = simDevice.DevEUI; var c2dToDeviceMessage = new ReceivedLoRaCloudToDeviceMessage() @@ -298,7 +300,7 @@ public async Task When_Message_Is_Invalid_Should_Fail() [Fact] public async Task When_Regions_Is_Not_Defined_Should_Fail() { - var simDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, deviceClassType: 'c', gatewayID: ServerGatewayID)); + var simDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, deviceClassType: LoRaDeviceClassType.C, gatewayID: ServerGatewayID)); var devEUI = simDevice.DevEUI; var c2dToDeviceMessage = new ReceivedLoRaCloudToDeviceMessage() @@ -330,27 +332,27 @@ public async Task When_Has_Custom_RX2DR_Should_Send_Correctly() var devAddr = new DevAddr(0x023637F8); var appSKey = TestKeys.CreateAppSessionKey(0xABC0200000000000, 0x09); var nwkSKey = TestKeys.CreateNetworkSessionKey(0xABC0200000000000, 0x09); - var simDevice = new SimulatedDevice(TestDeviceInfo.CreateOTAADevice(1, deviceClassType: 'c', gatewayID: ServerGatewayID)); + var simDevice = new SimulatedDevice(TestDeviceInfo.CreateOTAADevice(1, deviceClassType: LoRaDeviceClassType.C, gatewayID: ServerGatewayID)); var devEUI = simDevice.DevEUI; simDevice.SetupJoin(appSKey, nwkSKey, devAddr); this.deviceApi.Setup(x => x.GetPrimaryKeyByEuiAsync(devEUI)) .ReturnsAsync("123"); - var twin = simDevice.CreateOTAATwin( - desiredProperties: new Dictionary + var twin = LoRaDeviceTwin.Create( + simDevice.LoRaDevice.GetOtaaDesiredTwinProperties() with { - { TwinProperty.RX2DataRate, "10" } + Rx2DataRate = DataRateIndex.DR10 }, - reportedProperties: new Dictionary + simDevice.GetOtaaReportedTwinProperties() with { - { TwinProperty.RX2DataRate, 10 }, - { TwinProperty.Region, LoRaRegionType.US915.ToString() }, + Rx2DataRate = DataRateIndex.DR10, + Region = LoRaRegionType.US915, // OTAA device, already joined - { TwinProperty.DevAddr, devAddr.ToString() }, - { TwinProperty.AppSKey, appSKey.ToString() }, - { TwinProperty.NwkSKey, nwkSKey.ToString() }, - { TwinProperty.LastProcessingStationEui, new StationEui(ulong.MaxValue).ToString() } + DevAddr = devAddr, + AppSessionKey = appSKey, + NetworkSessionKey = nwkSKey, + LastProcessingStation = new StationEui(ulong.MaxValue) }); this.deviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) @@ -396,26 +398,26 @@ public async Task When_StationEui_Missing_Should_Fail() var devAddr = new DevAddr(0x023637F8); var appSKey = TestKeys.CreateAppSessionKey(0xABC0200000000000, 0x09); var nwkSKey = TestKeys.CreateNetworkSessionKey(0xABC0200000000000, 0x09); - var simDevice = new SimulatedDevice(TestDeviceInfo.CreateOTAADevice(1, deviceClassType: 'c', gatewayID: ServerGatewayID)); + var simDevice = new SimulatedDevice(TestDeviceInfo.CreateOTAADevice(1, deviceClassType: LoRaDeviceClassType.C, gatewayID: ServerGatewayID)); var devEUI = simDevice.DevEUI; simDevice.SetupJoin(appSKey, nwkSKey, devAddr); this.deviceApi.Setup(x => x.GetPrimaryKeyByEuiAsync(devEUI)) .ReturnsAsync("123"); - var twin = simDevice.CreateOTAATwin( - desiredProperties: new Dictionary + var twin = LoRaDeviceTwin.Create( + simDevice.LoRaDevice.GetOtaaDesiredTwinProperties() with { - { TwinProperty.RX2DataRate, "10" } + Rx2DataRate = DataRateIndex.DR10 }, - reportedProperties: new Dictionary + simDevice.GetOtaaReportedTwinProperties() with { - { TwinProperty.RX2DataRate, 10 }, - { TwinProperty.Region, LoRaRegionType.US915.ToString() }, + Rx2DataRate = DataRateIndex.DR10, + Region = LoRaRegionType.US915, // OTAA device, already joined - { TwinProperty.DevAddr, devAddr.ToString() }, - { TwinProperty.AppSKey, appSKey.ToString() }, - { TwinProperty.NwkSKey, nwkSKey.ToString() }, + DevAddr = devAddr, + AppSessionKey = appSKey, + NetworkSessionKey = nwkSKey }); this.deviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) @@ -444,11 +446,11 @@ public async Task When_StationEui_Missing_Should_Fail() this.deviceClient.VerifyAll(); } - public void Dispose() + public async ValueTask DisposeAsync() { - this.loRaDeviceRegistry.Dispose(); + await this.loRaDeviceRegistry.DisposeAsync(); this.cache.Dispose(); - this.deviceCache.Dispose(); + await this.deviceCache.DisposeAsync(); } } } diff --git a/Tests/Unit/NetworkServer/DeviceLoaderSynchronizerTest.cs b/Tests/Unit/NetworkServer/DeviceLoaderSynchronizerTest.cs index db506cb1ff..927e4e4c9a 100644 --- a/Tests/Unit/NetworkServer/DeviceLoaderSynchronizerTest.cs +++ b/Tests/Unit/NetworkServer/DeviceLoaderSynchronizerTest.cs @@ -14,7 +14,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer using Moq; using Xunit; - public sealed class DeviceLoaderSynchronizerTest : IDisposable + public sealed class DeviceLoaderSynchronizerTest : IAsyncDisposable { private readonly MemoryCache memoryCache; private readonly LoRaDeviceClientConnectionManager connectionManager; @@ -25,9 +25,9 @@ public DeviceLoaderSynchronizerTest() this.connectionManager = new LoRaDeviceClientConnectionManager(this.memoryCache, NullLogger.Instance); } - public void Dispose() + public async ValueTask DisposeAsync() { - this.connectionManager.Dispose(); + await this.connectionManager.DisposeAsync(); this.memoryCache.Dispose(); } @@ -42,7 +42,7 @@ public async Task When_Device_Api_Throws_Error_Should_Not_Create_Any_Device() var deviceFactory = new Mock(MockBehavior.Strict); - using var deviceCache = LoRaDeviceCacheDefault.CreateDefault(); + await using var deviceCache = LoRaDeviceCacheDefault.CreateDefault(); var target = new DeviceLoaderSynchronizer( devAddr, apiService.Object, @@ -83,7 +83,7 @@ public async Task When_Device_Does_Not_Exist_Should_Complete_Requests_As_Failed( var deviceFactory = new Mock(MockBehavior.Strict); - using var deviceCache = LoRaDeviceCacheDefault.CreateDefault(); + await using var deviceCache = LoRaDeviceCacheDefault.CreateDefault(); var target = new DeviceLoaderSynchronizer( devAddr, apiService.Object, @@ -129,7 +129,7 @@ public async Task When_Device_Does_Not_Match_Gateway_After_Loading_Should_Fail_R .ReturnsAsync(new SearchDevicesResult(iotHubDeviceInfo.AsList())); var loRaDeviceClient = new Mock(MockBehavior.Loose); - using var deviceCache = LoRaDeviceCacheDefault.CreateDefault(); + await using var deviceCache = LoRaDeviceCacheDefault.CreateDefault(); var deviceFactory = new TestLoRaDeviceFactory(loRaDeviceClient.Object, deviceCache, this.connectionManager); var target = new DeviceLoaderSynchronizer( simulatedDevice.DevAddr.Value, @@ -174,7 +174,7 @@ public async Task When_Device_Does_Not_Match_Gateway_Should_Fail_Request() var loRaDeviceClient = new Mock(MockBehavior.Loose); - using var deviceCache = LoRaDeviceCacheDefault.CreateDefault(); + await using var deviceCache = LoRaDeviceCacheDefault.CreateDefault(); var deviceFactory = new TestLoRaDeviceFactory(loRaDeviceClient.Object, deviceCache, this.connectionManager); var target = new DeviceLoaderSynchronizer( simulatedDevice.DevAddr.Value, @@ -196,8 +196,8 @@ public async Task When_Device_Does_Not_Match_Gateway_Should_Fail_Request() // device should not be initialized, since it belongs to another gateway loRaDeviceClient.Verify(x => x.GetTwinAsync(CancellationToken.None), Times.Never()); - // device should not be disconnected (was never connected) - loRaDeviceClient.Verify(x => x.Disconnect(), Times.Never()); + // device should be disconnected after initialization + loRaDeviceClient.Verify(x => x.DisconnectAsync(CancellationToken.None), Times.Once); // Device was searched by DevAddr apiService.VerifyAll(); @@ -221,9 +221,9 @@ public async Task When_Device_Is_Not_In_Cache_And_Found_In_Api_Does_Not_Match_Mi // Will get device twin var loRaDeviceClient = new Mock(MockBehavior.Loose); loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(TestUtils.CreateABPTwin(simulatedDevice)); + .ReturnsAsync(simulatedDevice.GetDefaultAbpTwin()); - using var deviceCache = LoRaDeviceCacheDefault.CreateDefault(); + await using var deviceCache = LoRaDeviceCacheDefault.CreateDefault(); var deviceFactory = new TestLoRaDeviceFactory(loRaDeviceClient.Object, deviceCache, this.connectionManager); var target = new DeviceLoaderSynchronizer( simulatedDevice.DevAddr.Value, @@ -282,7 +282,7 @@ public void When_No_Devices_Found() } [Fact] - public void When_Not_Our_Device_In_Cache() + public async Task When_Not_Our_Device_In_Cache() { var loraRequestMock = CreateVerifyableRequest(); var request = loraRequestMock.Object; @@ -299,7 +299,7 @@ public void When_Not_Our_Device_In_Cache() VerifyFailedReason(loraRequestMock, LoRaDeviceRequestFailedReason.BelongsToAnotherGateway); - loRaDevice.Dispose(); + await loRaDevice.DisposeAsync(); } [Theory] @@ -367,7 +367,7 @@ public class DeviceLoaderSynchronizerCreateDevicesTest [Fact] public async Task When_Cache_Contains_Join_Device_It_Is_Reloaded() { - using var deviceCache = LoRaDeviceCacheDefault.CreateDefault(); + await using var deviceCache = LoRaDeviceCacheDefault.CreateDefault(); var devEui = new DevEui(0x123); var loRaDevice = new Mock(null, devEui, null); @@ -377,15 +377,38 @@ public async Task When_Cache_Contains_Join_Device_It_Is_Reloaded() var deviceLoader = new TestDeviceLoaderSynchronizer(new DevAddr(0), null, null, deviceCache); - await deviceLoader.ExecuteCreateDevicesAsync(new[] { new IoTHubDeviceInfo { DevAddr = new DevAddr(0x456), DevEUI = devEui } }); + await deviceLoader.ExecuteCreateDevicesAsync(new[] { new IoTHubDeviceInfo { DevAddr = new DevAddr(0x456), DevEUI = devEui } }); loRaDevice.Verify(x => x.InitializeAsync(It.IsAny(), CancellationToken.None), Times.Once); + loRaDevice.Verify(x => x.CloseConnectionAsync(CancellationToken.None, false), Times.Once); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task When_Cache_Contains_Device_With_Outdated_DevAddr_Connection_Is_Closed(bool cachedDevAddrMatchesIoTHubInfo) + { + await using var deviceCache = LoRaDeviceCacheDefault.CreateDefault(); + + var devEui = new DevEui(0x123); + var devAddr = new DevAddr(0x456); + var loRaDevice = new Mock(devAddr, devEui, null); + loRaDevice.Setup(x => x.InitializeAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(true); + deviceCache.Register(loRaDevice.Object); + + var deviceLoader = new TestDeviceLoaderSynchronizer(new DevAddr(0), null, null, deviceCache); + + await deviceLoader.ExecuteCreateDevicesAsync(new[] { new IoTHubDeviceInfo { DevAddr = cachedDevAddrMatchesIoTHubInfo ? devAddr : new DevAddr(0x789), DevEUI = devEui } }); + + loRaDevice.Verify(x => x.InitializeAsync(It.IsAny(), CancellationToken.None), Times.Never); + loRaDevice.Verify(x => x.CloseConnectionAsync(CancellationToken.None, false), Times.Once); } [Fact] public async Task When_Reload_Fails_Loader_Updates_State() { - using var deviceCache = LoRaDeviceCacheDefault.CreateDefault(); + await using var deviceCache = LoRaDeviceCacheDefault.CreateDefault(); var devEui = new DevEui(0x123); var loRaDevice = new Mock(null, devEui, null); @@ -398,10 +421,51 @@ public async Task When_Reload_Fails_Loader_Updates_State() await deviceLoader.ExecuteCreateDevicesAsync(new[] { new IoTHubDeviceInfo { DevAddr = new DevAddr(0x456), DevEUI = devEui } }); loRaDevice.Verify(x => x.InitializeAsync(It.IsAny(), CancellationToken.None), Times.Once); + loRaDevice.Verify(x => x.CloseConnectionAsync(CancellationToken.None, false), Times.Once); Assert.True(deviceLoader.HasLoadingDeviceError); } + [Fact] + public async Task When_Initializer_Fails_Connection_Is_Closed_And_Exception_Thrown() + { + await using var deviceCache = LoRaDeviceCacheDefault.CreateDefault(); + + // setting up the LoRaDevice + var devEui = new DevEui(0x123); + var devAddr = new DevAddr(0x456); + var loRaDevice = new Mock(devAddr, devEui, null); + loRaDevice.Setup(x => x.InitializeAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(true); + loRaDevice.Object.IsOurDevice = true; + + // setting up the ILoRaDeviceFactory mock so that CreateAndRegister returns above LoRaDevice + var deviceFactory = new Mock(); + deviceFactory.Setup(x => x.CreateAndRegisterAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(loRaDevice.Object); + + // setting up one ILoRaDeviceInitializer to throw + var exceptionThrown = new InvalidOperationException("Mocked exception"); + var failingInitializer = new Mock(); + failingInitializer.Setup(x => x.Initialize(It.IsAny())) + .Throws(exceptionThrown); + + var deviceLoader = new TestDeviceLoaderSynchronizer(new DevAddr(0), + null, + deviceFactory.Object, + deviceCache, + new HashSet { failingInitializer.Object }); + // acting and asserting that the method throws + var iotHubDevicesInfo = new[] { new IoTHubDeviceInfo { DevAddr = devAddr, DevEUI = devEui } }; + var actualException = await Assert.ThrowsAsync(() => deviceLoader.ExecuteCreateDevicesAsync(iotHubDevicesInfo)); + + // asserting that the inner exception of the aggregate exception is actually what was expected + Assert.Equal(exceptionThrown, actualException.InnerException); + + // asserting that the connection was closed nevertheless + loRaDevice.Verify(x => x.CloseConnectionAsync(CancellationToken.None, false), Times.Once); + } + private class TestDeviceLoaderSynchronizer : DeviceLoaderSynchronizer { internal TestDeviceLoaderSynchronizer(DevAddr devAddr, diff --git a/Tests/Unit/NetworkServer/DisposableExtensionsTests.cs b/Tests/Unit/NetworkServer/DisposableExtensionsTests.cs new file mode 100644 index 0000000000..1ce13d2747 --- /dev/null +++ b/Tests/Unit/NetworkServer/DisposableExtensionsTests.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Unit.NetworkServer +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using LoRaWan.NetworkServer; + using Moq; + using Xunit; + + public class DisposableExtensionsTests + { + [Fact] + public async Task DisposeAllAsync_With_Null_Disposables_Throws() + { + var ex = await Assert.ThrowsAsync(async () => + await DisposableExtensions.DisposeAllAsync(null!, 0)); + + Assert.Equal("disposables", ex.ParamName); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public async Task DisposeAllAsync_With_Invalid_Concurrency_Throws(int concurrency) + { + var ex = await Assert.ThrowsAsync(async () => + await Enumerable.Empty().DisposeAllAsync(concurrency)); + + Assert.Equal("concurrency", ex.ParamName); + Assert.Equal(concurrency, ex.ActualValue); + } + + [Fact] + public void DisposeAllAsync_With_Empty_Sequence_Completes_Immediately() + { + var task = Array.Empty().DisposeAllAsync(42); + Assert.True(task.IsCompletedSuccessfully); + } + + public enum DisposablesSourceKind { Sequence, Array, ReadOnlyCollection } + + [Theory] + [InlineData(DisposablesSourceKind.Sequence, 10, 1)] + [InlineData(DisposablesSourceKind.Sequence, 10, 5)] + [InlineData(DisposablesSourceKind.Sequence, 10, 10)] + [InlineData(DisposablesSourceKind.Array, 10, 1)] + [InlineData(DisposablesSourceKind.Array, 10, 5)] + [InlineData(DisposablesSourceKind.Array, 10, 10)] + [InlineData(DisposablesSourceKind.ReadOnlyCollection, 10, 1)] + [InlineData(DisposablesSourceKind.ReadOnlyCollection, 10, 5)] + [InlineData(DisposablesSourceKind.ReadOnlyCollection, 10, 10)] + public async Task DisposeAllAsync_Disposes_Each_Disposable(DisposablesSourceKind sourceKind, int count, int concurrency) + { + var disposableMocks = Enumerable.Range(1, count).Select(_ => new Mock()).ToArray(); + + var disposables = disposableMocks.Select(m => m.Object); + switch (sourceKind) + { + case DisposablesSourceKind.Sequence: + break; + case DisposablesSourceKind.Array: + disposables = disposables.ToArray(); + break; + case DisposablesSourceKind.ReadOnlyCollection: + disposables = disposables.ToArray().WrapInReadOnlyCollection(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(sourceKind), sourceKind, null); + } + await disposables.DisposeAllAsync(concurrency); + + foreach (var disposableMock in disposableMocks) + disposableMock.Verify(x => x.DisposeAsync(), Times.Once); + } + + [Theory] + [InlineData(10, 1)] + [InlineData(10, 2)] + [InlineData(10, 3)] + [InlineData(10, 4)] + [InlineData(10, 5)] + [InlineData(10, 6)] + [InlineData(10, 7)] + [InlineData(10, 8)] + [InlineData(10, 9)] + [InlineData(10, 10)] + [InlineData(10, 11)] + public async Task DisposeAllAsync_Respects_Requested_Concurrency(int count, int concurrency) + { + var disposableMocks = Enumerable.Range(1, count).Select(_ => new Mock()).ToArray(); + + using var semaphore = new SemaphoreSlim(0, concurrency); + var flights = 0; + + async ValueTask DisposeAsync(Task task) + { + // Release semaphore when # of call in flight equal concurrency. + if (concurrency == Interlocked.Increment(ref flights)) + semaphore.Release(); + + await task; + } + + var tcsQueue = new Queue(); + foreach (var disposableMock in disposableMocks) + { + var tcs = new TaskCompletionSource(); + tcsQueue.Enqueue(tcs); + disposableMock.Setup(x => x.DisposeAsync()).Returns(() => DisposeAsync(tcs.Task)); + } + + var task = disposableMocks.Select(m => m.Object).DisposeAllAsync(concurrency); + + for (var remaining = count - concurrency; remaining > 0; remaining--) + { + // Wait until expected # of calls are in flight + Assert.True(await semaphore.WaitAsync(TimeSpan.FromSeconds(5))); + Assert.Equal(concurrency, flights); + // Decrement calls in flight because one will be completed. + flights--; + tcsQueue.Dequeue().SetResult(); + } + + Assert.Equal(Math.Min(concurrency, count), tcsQueue.Count); + + // Complete all remaining tasks. + while (tcsQueue.Count > 0) + tcsQueue.Dequeue().SetResult(); + + await task; + } + } +} diff --git a/Tests/Unit/NetworkServer/ExclusiveProcessorTests.cs b/Tests/Unit/NetworkServer/ExclusiveProcessorTests.cs new file mode 100644 index 0000000000..2e92a74c1d --- /dev/null +++ b/Tests/Unit/NetworkServer/ExclusiveProcessorTests.cs @@ -0,0 +1,230 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Unit.NetworkServer +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using LoRaWan.NetworkServer; + using Xunit; + using static MoreLinq.Extensions.IndexExtension; + using static MoreLinq.Extensions.ShuffleExtension; + + public class ExclusiveProcessorTests + { + private readonly ExclusiveProcessor subject = new(); + private readonly DateTimeOffset testStartTime = DateTimeOffset.UtcNow; + + [Fact] + public async Task TryProcessAsync_With_Successful_Function_Returns_Outcome() + { + var outcome = await this.subject.TryProcessAsync(1, () => Task.FromResult(42)); + + Assert.True(outcome.SubmissionTime >= this.testStartTime); + Assert.NotEqual(TimeSpan.Zero, outcome.WaitDuration); + Assert.NotEqual(TimeSpan.Zero, outcome.RunDuration); + Assert.True(outcome.Result.IsCompletedSuccessfully); + Assert.Equal(42, await outcome.Result); + } + + [Fact] + public async Task TryProcessAsync_With_Canceled_Function_Returns_Outcome() + { + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var outcome = await this.subject.TryProcessAsync(1, () => Task.FromCanceled(cts.Token)); + + Assert.True(outcome.SubmissionTime >= this.testStartTime); + Assert.NotEqual(TimeSpan.Zero, outcome.WaitDuration); + Assert.NotEqual(TimeSpan.Zero, outcome.RunDuration); + Assert.True(outcome.Result.IsCanceled); + } + + [Fact] + public async Task TryProcessAsync_With_Erroneous_Function_Returns_Outcome() + { +#pragma warning disable CA2201 // Do not raise reserved exception types + var ex = new ApplicationException(); +#pragma warning restore CA2201 // Do not raise reserved exception types + + var outcome = await this.subject.TryProcessAsync(1, () => Task.FromException(ex)); + + Assert.True(outcome.SubmissionTime >= this.testStartTime); + Assert.NotEqual(TimeSpan.Zero, outcome.WaitDuration); + Assert.NotEqual(TimeSpan.Zero, outcome.RunDuration); + Assert.True(outcome.Result.IsFaulted); + var aggregateException = outcome.Result.Exception; + Assert.NotNull(aggregateException); + Assert.Same(ex, Assert.Single(aggregateException.InnerExceptions)); + } + + [Fact] + public async Task TryProcessAsync_Raises_Events() + { + (object Sender, int Args) submittedEvent = default; + (object Sender, int Args) processingEvent = default; + (object Sender, (int, ExclusiveProcessor.IProcessingOutcome) Args) processedEvent = default; + + this.subject.Submitted += (sender, args) => submittedEvent = (sender, args); + this.subject.Processing += (sender, args) => processingEvent = (sender, args); + this.subject.Processed += (sender, args) => processedEvent = (sender, args); + + const int id = 1; + var outcome = await this.subject.TryProcessAsync(id, () => Task.FromResult(42)); + + Assert.Same(this.subject, submittedEvent.Sender); + Assert.Equal(id, submittedEvent.Args); + + Assert.Same(this.subject, processingEvent.Sender); + Assert.Equal(id, processingEvent.Args); + + Assert.Same(this.subject, processedEvent.Sender); + var (_, (processedId, processedOutcome)) = processedEvent; + Assert.Equal(id, processedId); + Assert.Equal((object)outcome, processedOutcome); + } + + [Fact] + public async Task ProcessAsync_With_Successful_Function_Returns_Outcome() + { + var outcome = await this.subject.ProcessAsync(1, () => Task.FromResult(42)); + + Assert.True(outcome.SubmissionTime >= this.testStartTime); + Assert.NotEqual(TimeSpan.Zero, outcome.WaitDuration); + Assert.NotEqual(TimeSpan.Zero, outcome.RunDuration); + Assert.Equal(42, outcome.Result); + } + + [Fact] + public async Task ProcessAsync_With_Canceled_Function_Throws_TaskCanceledException() + { + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var ex = await Assert.ThrowsAsync(() => + this.subject.ProcessAsync(1, () => Task.FromCanceled(cts.Token))); + + Assert.Equal(cts.Token, ex.CancellationToken); + } + + [Fact] + public async Task ProcessAsync_With_Erroneous_Function_Throws_Thrown_Exception() + { +#pragma warning disable CA2201 // Do not raise reserved exception types + var ex = new ApplicationException(); +#pragma warning restore CA2201 // Do not raise reserved exception types + + var actual = await Assert.ThrowsAsync(() => + this.subject.ProcessAsync(1, () => Task.FromException(ex))); + + Assert.Same(ex, actual); + } + + [Fact] + public async Task ProcessAsync_Raises_Events() + { + (object Sender, int Args) submittedEvent = default; + (object Sender, int Args) processingEvent = default; + (object Sender, (int, ExclusiveProcessor.IProcessingOutcome) Args) processedEvent = default; + + this.subject.Submitted += (sender, args) => submittedEvent = (sender, args); + this.subject.Processing += (sender, args) => processingEvent = (sender, args); + this.subject.Processed += (sender, args) => processedEvent = (sender, args); + + const int id = 1; + var outcome = await this.subject.ProcessAsync(id, () => Task.FromResult(42)); + + Assert.Same(this.subject, submittedEvent.Sender); + Assert.Equal(id, submittedEvent.Args); + + Assert.Same(this.subject, processingEvent.Sender); + Assert.Equal(id, processingEvent.Args); + + Assert.Same(this.subject, processedEvent.Sender); + var (_, (processedId, processedOutcome)) = processedEvent; + Assert.Equal(id, processedId); + Assert.True(processedOutcome.Task.IsCompletedSuccessfully); + Assert.Equal(outcome.SubmissionTime, processedOutcome.SubmissionTime); + Assert.Equal(outcome.WaitDuration, processedOutcome.WaitDuration); + Assert.Equal(outcome.RunDuration, processedOutcome.RunDuration); + } + + [Theory] + [InlineData(10)] + public async Task ProcessAsync_Is_Fifo_By_Default(int processorCount) + { + var processorIds = Enumerable.Range(1, processorCount).ToArray(); + var (submittedList, processedList) = await TestProcessing(this.subject, processorIds); + + Assert.Equal(processedList, submittedList); + Assert.Equal(processedList, processorIds); + } + + private sealed class LifoScheduler : ExclusiveProcessor.IScheduler + { + public List SelectionList { get; } = new(); + + public T SelectNext(IReadOnlyList processes) + { + var next = processes[^1]; + if (SelectionList.Count == 0 || !EqualityComparer.Default.Equals(SelectionList[^1], next)) + SelectionList.Add(next); + return next; + } + } + + [Theory] + [InlineData(10)] + public async Task ProcessAsync_Uses_Scheduler(int processorCount) + { + var processorIds = Enumerable.Range(1, processorCount).ToArray(); + var scheduler = new LifoScheduler(); + var subject = new ExclusiveProcessor(scheduler); + var (submittedList, processedList) = await TestProcessing(subject, processorIds); + + Assert.Equal(submittedList, processorIds); + Assert.Equal(scheduler.SelectionList, processedList); + } + + private static async Task<(List SubmittedList, List ProcessedList)> + TestProcessing(ExclusiveProcessor subject, IEnumerable processorIds) + { + var submittedList = new List(); + var processedList = new List(); + + subject.Submitted += (_, args) => submittedList.Add(args); + subject.Processed += (_, args) => processedList.Add(args.Processor); + + var processors = processorIds.Select(id => (Id: id, TaskCompletionSource: new TaskCompletionSource())) + .ToArray(); + + var tasks = new Task.ProcessingOutcome>[processors.Length]; + + foreach (var (i, (id, process)) in processors.Index()) + { + tasks[i] = subject.ProcessAsync(id, () => process.Task); + + Assert.Equal(i + 1, submittedList.Count); + Assert.Empty(processedList); + } + + var task = Task.WhenAll(tasks); + + var results = processors.Select(e => e.Id.ToString(CultureInfo.InvariantCulture)).ToArray(); + + // Complete each in some random (shuffled) order. + + foreach (var ((_, processor), result) in processors.Zip(results).Shuffle()) + processor.SetResult(result); + + Assert.Equal(results, from outcome in await task select outcome.Result); + + return (submittedList, processedList); + } + } +} diff --git a/Tests/Unit/NetworkServer/FcntLimitTest.cs b/Tests/Unit/NetworkServer/FcntLimitTest.cs index 0110497f80..23347af87a 100644 --- a/Tests/Unit/NetworkServer/FcntLimitTest.cs +++ b/Tests/Unit/NetworkServer/FcntLimitTest.cs @@ -57,7 +57,21 @@ public class FcntLimitTest : MessageProcessorTestBase var devAddr = simulatedDevice.LoRaDevice.DevAddr.Value; LoRaDeviceClient.Setup(x => x.SendEventAsync(It.IsNotNull(), null)).ReturnsAsync(true); - var initialTwin = SetupTwins(deviceFcntUp, deviceFcntDown, startFcntUp, startFcntDown, abpRelaxed, supports32Bit, simulatedDevice, devEui, devAddr); + + var initialTwin = LoRaDeviceTwin.Create( + simulatedDevice.LoRaDevice.GetAbpDesiredTwinProperties() with + { + DevEui = devEui, + AbpRelaxMode = abpRelaxed, + Supports32BitFCnt = supports32Bit, + FCntUpStart = startFcntUp, + FCntDownStart = startFcntDown + }, + new LoRaReportedTwinProperties + { + FCntUp = deviceFcntUp, + FCntDown = deviceFcntDown, + }); LoRaDeviceClient .Setup(x => x.GetTwinAsync(CancellationToken.None)).Returns(() => @@ -90,10 +104,11 @@ public class FcntLimitTest : MessageProcessorTestBase .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEui, "abc").AsList())); using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -133,23 +148,23 @@ public class FcntLimitTest : MessageProcessorTestBase [Theory] // not saving, reported match, reset counter is lower - [InlineData(11, 10U, 10U, 10U, 10U, 1, 3, 10, 10, false)] + [InlineData(11, 10U, 10U, 10U, 10U, 1U, 3U, 10, 10, false)] // saving, reported match, but reset counter is set, reported null - [InlineData(11, 10U, 10U, 10U, 10U, 1, null, 10, 10, true)] + [InlineData(11, 10U, 10U, 10U, 10U, 1U, null, 10, 10, true)] // saving, reported match, but reset counter is set - [InlineData(11, 10U, 10U, 10U, 10U, 1, 0, 10, 10, true)] + [InlineData(11, 10U, 10U, 10U, 10U, 1U, 0U, 10, 10, true)] // save reporting do not match - [InlineData(2, 1U, 1U, 0U, 0U, 0, 0, 1U, 1U, true)] + [InlineData(2, 1U, 1U, 0U, 0U, 0U, 0U, 1U, 1U, true)] // save reporting do not match - [InlineData(11, 10U, 20U, 0U, 0U, 0, 0, 10U, 20U, true)] + [InlineData(11, 10U, 20U, 0U, 0U, 0U, 0U, 10U, 20U, true)] public async Task ValidateFcnt_Start_Values_And_ResetCounter( short fcntUp, uint startFcntUpDesired, uint startFcntDownDesired, uint? startFcntUpReported, uint? startFcntDownReported, - int? fcntResetCounterDesired, - int? fcntResetCounterReported, + uint? fcntResetCounterDesired, + uint? fcntResetCounterReported, uint startUpExpected, uint startDownExpected, bool saveExpected) @@ -161,17 +176,24 @@ public class FcntLimitTest : MessageProcessorTestBase LoRaDeviceClient.Setup(x => x.SendEventAsync(It.IsNotNull(), null)).ReturnsAsync(true); - var initialTwin = SetupTwins((uint)fcntUp, startFcntDownDesired, startFcntUpDesired, startFcntDownDesired, true, false, simulatedDevice, devEui, devAddr); - - if (startFcntUpReported.HasValue) - initialTwin.Properties.Reported[TwinProperty.FCntUpStart] = startFcntUpReported.Value; - if (startFcntDownReported.HasValue) - initialTwin.Properties.Reported[TwinProperty.FCntDownStart] = startFcntDownReported.Value; - - if (fcntResetCounterDesired.HasValue) - initialTwin.Properties.Desired[TwinProperty.FCntResetCounter] = fcntResetCounterDesired.Value; - if (fcntResetCounterReported.HasValue) - initialTwin.Properties.Reported[TwinProperty.FCntResetCounter] = fcntResetCounterReported.Value; + var initialTwin = LoRaDeviceTwin.Create( + simulatedDevice.LoRaDevice.GetAbpDesiredTwinProperties() with + { + DevEui = devEui, + AbpRelaxMode = true, + Supports32BitFCnt = false, + FCntUpStart = startFcntUpDesired, + FCntDownStart = startFcntDownDesired, + FCntResetCounter = fcntResetCounterDesired + }, + new LoRaReportedTwinProperties + { + FCntUp = (uint)fcntUp, + FCntDown = startFcntDownDesired, + FCntUpStart = startFcntUpReported, + FCntDownStart = startFcntDownReported, + FCntResetCounter = fcntResetCounterReported + }); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(initialTwin); @@ -195,10 +217,11 @@ public class FcntLimitTest : MessageProcessorTestBase LoRaDeviceApi.Setup(x => x.SearchByDevAddrAsync(devAddr)).ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEui, "abc").AsList())); using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -224,31 +247,5 @@ public class FcntLimitTest : MessageProcessorTestBase LoRaDeviceClient.Verify(x => x.UpdateReportedPropertiesAsync(It.IsNotNull(), It.IsAny()), Times.Never()); } } - - private static Twin SetupTwins(uint? deviceFcntUp, uint? deviceFcntDown, uint? startFcntUp, uint? startFcntDown, bool abpRelaxed, bool supports32Bit, SimulatedDevice simulatedDevice, DevEui devEui, DevAddr devAddr) - { - var initialTwin = new Twin(); - initialTwin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - initialTwin.Properties.Desired[TwinProperty.AppEui] = simulatedDevice.LoRaDevice.AppEui?.ToString(); - initialTwin.Properties.Desired[TwinProperty.AppKey] = simulatedDevice.LoRaDevice.AppKey?.ToString(); - initialTwin.Properties.Desired[TwinProperty.NwkSKey] = simulatedDevice.LoRaDevice.NwkSKey?.ToString(); - initialTwin.Properties.Desired[TwinProperty.AppSKey] = simulatedDevice.LoRaDevice.AppSKey?.ToString(); - initialTwin.Properties.Desired[TwinProperty.DevAddr] = devAddr.ToString(); - initialTwin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; - initialTwin.Properties.Desired[TwinProperty.GatewayID] = simulatedDevice.LoRaDevice.GatewayID; - initialTwin.Properties.Desired[TwinProperty.ABPRelaxMode] = abpRelaxed; - - initialTwin.Properties.Desired[TwinProperty.Supports32BitFCnt] = supports32Bit; - - if (deviceFcntUp.HasValue) - initialTwin.Properties.Reported[TwinProperty.FCntUp] = deviceFcntUp.Value; - if (deviceFcntDown.HasValue) - initialTwin.Properties.Reported[TwinProperty.FCntDown] = deviceFcntDown.Value; - if (startFcntUp.HasValue) - initialTwin.Properties.Desired[TwinProperty.FCntUpStart] = startFcntUp.Value; - if (startFcntDown.HasValue) - initialTwin.Properties.Desired[TwinProperty.FCntDownStart] = startFcntDown.Value; - return initialTwin; - } } } diff --git a/Tests/Unit/NetworkServer/JoinDeviceLoaderTest.cs b/Tests/Unit/NetworkServer/JoinDeviceLoaderTest.cs index c43a8a13e3..9295edf577 100644 --- a/Tests/Unit/NetworkServer/JoinDeviceLoaderTest.cs +++ b/Tests/Unit/NetworkServer/JoinDeviceLoaderTest.cs @@ -16,39 +16,39 @@ public sealed class JoinDeviceLoaderTest [Fact] public async Task When_Not_In_Cache_It_Is_Loaded() { - using var cache = LoRaDeviceCacheDefault.CreateDefault(); + await using var cache = LoRaDeviceCacheDefault.CreateDefault(); var factory = new Mock(); - using var joinDeviceLoader = new JoinDeviceLoader(DefaultDeviceInfo, factory.Object, cache, NullLogger.Instance); + using var joinDeviceLoader = new JoinDeviceLoader(defaultDeviceInfo, factory.Object, cache, NullLogger.Instance); await joinDeviceLoader.LoadAsync(); - factory.Verify(x => x.CreateAndRegisterAsync(DefaultDeviceInfo, It.IsAny()), Times.Once); + factory.Verify(x => x.CreateAndRegisterAsync(defaultDeviceInfo, It.IsAny()), Times.Once); Assert.True(joinDeviceLoader.CanCache); } [Fact] public async Task When_In_Cache_It_Is_Not_Loaded() { - using var cache = LoRaDeviceCacheDefault.CreateDefault(); + await using var cache = LoRaDeviceCacheDefault.CreateDefault(); var factory = new Mock(); - using var joinDeviceLoader = new JoinDeviceLoader(DefaultDeviceInfo, factory.Object, cache, NullLogger.Instance); + using var joinDeviceLoader = new JoinDeviceLoader(defaultDeviceInfo, factory.Object, cache, NullLogger.Instance); - using var device = new LoRaDevice(DefaultDeviceInfo.DevAddr, DefaultDeviceInfo.DevEUI, null); + await using var device = new LoRaDevice(defaultDeviceInfo.DevAddr, defaultDeviceInfo.DevEUI, null); cache.Register(device); Assert.Equal(device, await joinDeviceLoader.LoadAsync()); - factory.Verify(x => x.CreateAndRegisterAsync(DefaultDeviceInfo, It.IsAny()), Times.Never); + factory.Verify(x => x.CreateAndRegisterAsync(defaultDeviceInfo, It.IsAny()), Times.Never); Assert.True(joinDeviceLoader.CanCache); } [Fact] public async Task When_The_Load_Fails_CanCache_Is_False() { - using var cache = LoRaDeviceCacheDefault.CreateDefault(); + await using var cache = LoRaDeviceCacheDefault.CreateDefault(); var factory = new Mock(); - factory.Setup(x => x.CreateAndRegisterAsync(DefaultDeviceInfo, It.IsAny())) + factory.Setup(x => x.CreateAndRegisterAsync(defaultDeviceInfo, It.IsAny())) .ThrowsAsync(new LoRaProcessingException()); - using var joinDeviceLoader = new JoinDeviceLoader(DefaultDeviceInfo, factory.Object, cache, NullLogger.Instance); + using var joinDeviceLoader = new JoinDeviceLoader(defaultDeviceInfo, factory.Object, cache, NullLogger.Instance); Assert.Null(await joinDeviceLoader.LoadAsync()); Assert.False(joinDeviceLoader.CanCache); @@ -57,28 +57,28 @@ public async Task When_The_Load_Fails_CanCache_Is_False() [Fact] public async Task When_One_Load_Is_Pending_Other_Is_Waiting() { - using var cache = LoRaDeviceCacheDefault.CreateDefault(); + await using var cache = LoRaDeviceCacheDefault.CreateDefault(); var factory = new Mock(); - using var device = new LoRaDevice(DefaultDeviceInfo.DevAddr, DefaultDeviceInfo.DevEUI, null); + await using var device = new LoRaDevice(defaultDeviceInfo.DevAddr, defaultDeviceInfo.DevEUI, null); - factory.Setup(x => x.CreateAndRegisterAsync(DefaultDeviceInfo, It.IsAny())) + factory.Setup(x => x.CreateAndRegisterAsync(defaultDeviceInfo, It.IsAny())) .ReturnsAsync(() => { cache.Register(device); return device; }); - using var joinDeviceLoader = new JoinDeviceLoader(DefaultDeviceInfo, factory.Object, cache, NullLogger.Instance); + using var joinDeviceLoader = new JoinDeviceLoader(defaultDeviceInfo, factory.Object, cache, NullLogger.Instance); var t1 = joinDeviceLoader.LoadAsync(); var t2 = joinDeviceLoader.LoadAsync(); Assert.All(await Task.WhenAll(t1, t2), x => Assert.Equal(device, x)); - factory.Verify(x => x.CreateAndRegisterAsync(DefaultDeviceInfo, It.IsAny()), Times.Once); + factory.Verify(x => x.CreateAndRegisterAsync(defaultDeviceInfo, It.IsAny()), Times.Once); } - private readonly IoTHubDeviceInfo DefaultDeviceInfo = new() + private readonly IoTHubDeviceInfo defaultDeviceInfo = new() { DevEUI = new DevEui(0), PrimaryKey = "AAAA", diff --git a/Tests/Unit/NetworkServer/JoinRequestMessageHandlerTests.cs b/Tests/Unit/NetworkServer/JoinRequestMessageHandlerTests.cs new file mode 100644 index 0000000000..3816cb08d3 --- /dev/null +++ b/Tests/Unit/NetworkServer/JoinRequestMessageHandlerTests.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Unit.NetworkServer +{ + using System.Diagnostics.Metrics; + using System.Linq; + using System.Threading.Tasks; + using LoRaWan.NetworkServer; + using LoRaWan.Tests.Common; + using Microsoft.Extensions.Logging.Abstractions; + using Moq; + using Xunit; + + public sealed class JoinRequestMessageHandlerTests + { + private const string MetricNamespace = nameof(JoinRequestMessageHandlerTests); + + [Theory] + [InlineData(ConcentratorDeduplicationResult.Duplicate, 0)] + [InlineData(ConcentratorDeduplicationResult.DuplicateDueToResubmission, 0)] + [InlineData(ConcentratorDeduplicationResult.SoftDuplicateDueToDeduplicationStrategy, 0)] + [InlineData(ConcentratorDeduplicationResult.NotDuplicate, 1)] + public async Task Increases_Join_Request_Counter_If_Not_Duplicate(ConcentratorDeduplicationResult deduplicationResult, int joinRequestCount) + { + // arrange + using var subject = Setup(deduplicationResult); + using var request = WaitableLoRaRequest.CreateWaitableRequest(new SimulatedDevice(TestDeviceInfo.CreateOTAADevice(1)).CreateJoinRequest()); + using var metricListener = new TestMetricListener(MetricNamespace); + metricListener.Start(); + + // act + await subject.Value.ProcessJoinRequestAsync(request); + + // assert + Assert.Equal(joinRequestCount, metricListener.RecordedMetrics.Count(m => m.Instrument.Name == MetricRegistry.JoinRequests.Name && m.Value == 1)); + } + + private static DisposableValue Setup(ConcentratorDeduplicationResult deduplicationResult) + { + var deduplicationMock = new Mock(); + _ = deduplicationMock.Setup(d => d.CheckDuplicateJoin(It.IsAny())).Returns(deduplicationResult); +#pragma warning disable CA2000 // Dispose objects before losing scope (dispose handled in DisposableValue) + var meter = new Meter(MetricNamespace); +#pragma warning restore CA2000 // Dispose objects before losing scope + return new DisposableValue( + new JoinRequestMessageHandler(new NetworkServerConfiguration(), + deduplicationMock.Object, + Mock.Of(), + NullLogger.Instance, + Mock.Of(), + meter), + meter); + } + } +} diff --git a/Tests/Unit/NetworkServer/JsonHandlers/LnsDataTests.cs b/Tests/Unit/NetworkServer/JsonHandlers/LnsDataTests.cs index 91eb721dd7..a8b8797227 100644 --- a/Tests/Unit/NetworkServer/JsonHandlers/LnsDataTests.cs +++ b/Tests/Unit/NetworkServer/JsonHandlers/LnsDataTests.cs @@ -5,7 +5,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer.BasicsStation.JsonHandlers { using System; using System.Text.Json; - using LoRaWan.NetworkServer; + using global::LoRaTools; using LoRaWan.NetworkServer.BasicsStation; using LoRaWan.NetworkServer.BasicsStation.JsonHandlers; using LoRaWan.Tests.Common; diff --git a/Tests/Unit/NetworkServer/ListExtensions.cs b/Tests/Unit/NetworkServer/ListExtensions.cs new file mode 100644 index 0000000000..a6e39760ed --- /dev/null +++ b/Tests/Unit/NetworkServer/ListExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Unit.NetworkServer +{ + using System.Collections; + using System.Collections.Generic; + + internal static class ListExtensions + { + public static IReadOnlyCollection WrapInReadOnlyCollection(this IList list) => new ReadOnlyCollection(list); + + internal sealed class ReadOnlyCollection : IReadOnlyCollection + { + private readonly IList list; + + public ReadOnlyCollection(IList list) => this.list = list; + public int Count => this.list.Count; + public IEnumerator GetEnumerator() => this.list.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this.list).GetEnumerator(); + } + } +} diff --git a/Tests/Unit/NetworkServer/LnsProtocolMessageProcessorTests.cs b/Tests/Unit/NetworkServer/LnsProtocolMessageProcessorTests.cs index dd3dd0c0f6..477cab3806 100644 --- a/Tests/Unit/NetworkServer/LnsProtocolMessageProcessorTests.cs +++ b/Tests/Unit/NetworkServer/LnsProtocolMessageProcessorTests.cs @@ -5,20 +5,23 @@ namespace LoRaWan.Tests.Unit.NetworkServer { using System; using System.Linq; - using System.Net.NetworkInformation; + using System.Net; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; using Common; using global::LoRaTools.LoRaMessage; + using global::LoRaTools.NetworkServerDiscovery; using global::LoRaTools.Regions; using LoRaWan.NetworkServer; using LoRaWan.NetworkServer.BasicsStation; using LoRaWan.NetworkServer.BasicsStation.Processors; + using LoRaWan.Tests.Unit.LoRaTools; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -27,6 +30,7 @@ public class LnsProtocolMessageProcessorTests private readonly Mock basicsStationConfigurationMock; private readonly Mock messageDispatcher; private readonly Mock downstreamMessageSender; + private readonly Mock tracingMock; private readonly LnsProtocolMessageProcessor lnsMessageProcessorMock; private readonly Mock socketMock; private readonly Mock httpContextMock; @@ -42,15 +46,18 @@ public LnsProtocolMessageProcessorTests() .Returns(Task.FromResult(RegionManager.EU868)); this.messageDispatcher = new Mock(); this.downstreamMessageSender = new Mock(); + this.tracingMock = new Mock(); this.lnsMessageProcessorMock = new LnsProtocolMessageProcessor(this.basicsStationConfigurationMock.Object, new WebSocketWriterRegistry(Mock.Of>>(), null), this.downstreamMessageSender.Object, this.messageDispatcher.Object, + NullLoggerFactory.Instance, loggerMock, new RegistryMetricTagBag(new NetworkServerConfiguration { GatewayID = "foogateway" }), // Do not pass meter since metric testing will be unreliable due to interference from test classes running in parallel. - null); + null, + this.tracingMock.Object); } [Fact] @@ -79,82 +86,12 @@ public async Task CloseSocketAsync_WhenNonOpenSocket_ShouldNotClose() this.socketMock.Verify(x => x.CloseAsync(WebSocketCloseStatus.NormalClosure, nameof(WebSocketCloseStatus.NormalClosure), It.IsAny()), Times.Never); } - [Fact] - public async Task ProcessIncomingRequestAsync_ShouldNotProcess_NonWebsocketRequests() - { - // mocking a non-websocket request - _ = SetupWebSocketConnection(isWebSocketRequest: false); - - // providing a mocked HttpResponse so that it's possible to verify stubbed properties - InitializeHttpContextMockWithHttpResponse(); - - // act - await this.lnsMessageProcessorMock.ProcessIncomingRequestAsync(this.httpContextMock.Object, - delegate { return Task.CompletedTask; }, - CancellationToken.None); - - // assert - Assert.Equal(400, this.httpContextMock.Object.Response.StatusCode); - } - - [Fact] - public async Task ProcessIncomingRequestAsync_Should_Handle_OperationCanceledException() - { - // arrange - _ = SetupWebSocketConnection(isWebSocketRequest: true); - InitializeHttpContextMockWithHttpResponse(); - _ = this.socketMock.Setup(ws => ws.CloseAsync(WebSocketCloseStatus.NormalClosure, It.IsAny(), It.IsAny())) - .Throws(new OperationCanceledException("websocketexception", new WebSocketException(WebSocketError.ConnectionClosedPrematurely))); - - // act - var ex = - await Record.ExceptionAsync(() => - this.lnsMessageProcessorMock.ProcessIncomingRequestAsync(this.httpContextMock.Object, - delegate { return Task.CompletedTask; }, - CancellationToken.None)); - - // assert - Assert.Null(ex); - } - - [Fact] - public async Task ProcessIncomingRequestAsync_ShouldProcess_WebsocketRequests() - { - // arrange - var httpContextMock = new Mock(); - - // mocking a websocket request - var webSocketsManager = SetupWebSocketConnection(isWebSocketRequest: true); - // initially the WebSocketState is Open - this.socketMock.Setup(x => x.State).Returns(WebSocketState.Open); - // when the CloseAsync is invoked, the State should be set to Closed (useful for verifying later on) - this.socketMock.Setup(x => x.CloseAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((wscs, reason, c) => - { - this.socketMock.Setup(x => x.State).Returns(WebSocketState.Closed); - this.socketMock.Setup(x => x.CloseStatus).Returns(wscs); - this.socketMock.Setup(x => x.CloseStatusDescription).Returns(reason); - }); - SetupSocketReceiveAsync(MessageFormat.Text, "test"); - httpContextMock.Setup(m => m.WebSockets).Returns(webSocketsManager.Object); - - // this is needed for logging the Basic Station (caller) remote ip address - var connectionInfo = new Mock(); - connectionInfo.Setup(c => c.RemoteIpAddress).Returns(System.Net.IPAddress.Loopback); - httpContextMock.Setup(m => m.Connection).Returns(connectionInfo.Object); - - // act and assert - await this.lnsMessageProcessorMock.ProcessIncomingRequestAsync(httpContextMock.Object, - delegate { return Task.CompletedTask; }, - CancellationToken.None); - - // assert that websocket is closed, as the input string was verified through local function handler - Assert.Equal(WebSocketState.Closed, this.socketMock.Object.State); - Assert.Equal(WebSocketCloseStatus.NormalClosure, this.socketMock.Object.CloseStatus); - } - - private Mock SetupWebSocketConnection(bool isWebSocketRequest) + /// + /// Ensures that a WebSocket connection can (or cannot) be established from the instance's . + /// + private Mock SetupWebSocketConnection(bool notAWebSocketRequest = false) { + var isWebSocketRequest = !notAWebSocketRequest; var webSocketsManager = new Mock(); _ = webSocketsManager.SetupGet(x => x.IsWebSocketRequest).Returns(isWebSocketRequest); _ = this.httpContextMock.SetupGet(m => m.WebSockets).Returns(webSocketsManager.Object); @@ -168,47 +105,35 @@ private Mock SetupWebSocketConnection(bool isWebSocketRequest) return webSocketsManager; } - private void InitializeHttpContextMockWithHttpResponse() - { - var httpResponseMock = new Mock(); - _ = httpResponseMock.SetupAllProperties(); - _ = this.httpContextMock.Setup(m => m.Response).Returns(httpResponseMock.Object); - } - [Theory] - [InlineData(true, true)] - [InlineData(false, false)] - [InlineData(true, false)] - [InlineData(false, true)] - public async Task InternalHandleDiscoveryAsync_ShouldSendProperJson(bool isHttps, bool isValidNic) + [InlineData(true, true, 1234)] + [InlineData(false, false, 1234)] + [InlineData(true, false, 1234)] + [InlineData(false, true, 1234)] + [InlineData(true, true, null)] + [InlineData(false, true, null)] + public async Task InternalHandleDiscoveryAsync_ShouldSendProperJson(bool isHttps, bool isValidNic, int? port) { // arrange + const string host = "localhost"; InitializeConfigurationServiceMock(); // mocking localIpAddress var connectionInfoMock = new Mock(); - // this is going to select the network interface with most bytes received / sent - // this should correspond to the real ethernet/wifi interface on the machine - var firstNic = isValidNic ? NetworkInterface.GetAllNetworkInterfaces() - .OrderByDescending(x => x.GetIPv4Statistics().BytesReceived + x.GetIPv4Statistics().BytesSent) - .FirstOrDefault() - : null; - if (firstNic is not null) - { - var firstNicIp = firstNic.GetIPProperties().UnicastAddresses.First().Address; - connectionInfoMock.SetupGet(ci => ci.LocalIpAddress).Returns(firstNicIp); - this.httpContextMock.Setup(h => h.Connection).Returns(connectionInfoMock.Object); - } - else - { - connectionInfoMock.SetupGet(ci => ci.LocalIpAddress).Returns(new System.Net.IPAddress(new byte[] { 192, 168, 1, 10 })); - this.httpContextMock.Setup(h => h.Connection).Returns(connectionInfoMock.Object); - } + var nic = isValidNic ? DiscoveryServiceTests.GetMostUsedNic() : null; + var ip = isValidNic ? nic?.GetIPProperties().UnicastAddresses.First().Address : new IPAddress(new byte[] { 192, 168, 1, 10 }); + _ = connectionInfoMock.SetupGet(ci => ci.LocalIpAddress).Returns(ip); + this.httpContextMock.Setup(h => h.Connection).Returns(connectionInfoMock.Object); var mockHttpRequest = new Mock(); - mockHttpRequest.SetupGet(x => x.Host).Returns(new HostString("localhost", 1234)); + mockHttpRequest.SetupGet(x => x.Host).Returns(port is { } somePort ? new HostString(host, somePort) : new HostString(host)); mockHttpRequest.SetupGet(x => x.IsHttps).Returns(isHttps); + mockHttpRequest.SetupGet(x => x.Scheme).Returns("ws"); this.httpContextMock.Setup(h => h.Request).Returns(mockHttpRequest.Object); + var webSocketManager = new Mock(); + webSocketManager.Setup(wsm => wsm.AcceptWebSocketAsync()).ReturnsAsync(this.socketMock.Object); + webSocketManager.Setup(wsm => wsm.IsWebSocketRequest).Returns(true); + this.httpContextMock.Setup(h => h.WebSockets).Returns(webSocketManager.Object); SetupSocketReceiveAsync(@"{ router: 'b827:ebff:fee1:e39a' }"); @@ -224,11 +149,12 @@ public async Task InternalHandleDiscoveryAsync_ShouldSendProperJson(bool isHttps sentEnd = end; }); - var muxs = Id6.Format(firstNic?.GetPhysicalAddress().Convert48To64() ?? 0, Id6.FormatOptions.FixedWidth); - var expectedString = @$"{{""router"":""b827:ebff:fee1:e39a"",""muxs"":""{muxs}"",""uri"":""{(isHttps ? "wss" : "ws")}://localhost:1234{BasicsStationNetworkServer.DataEndpoint}/B827EBFFFEE1E39A""}}"; + var muxs = Id6.Format(nic?.GetPhysicalAddress().Convert48To64() ?? 0, Id6.FormatOptions.FixedWidth); + var portString = port is { } somePortPrime ? $":{somePortPrime}" : string.Empty; + var expectedString = @$"{{""router"":""b827:ebff:fee1:e39a"",""muxs"":""{muxs}"",""uri"":""{(isHttps ? "wss" : "ws")}://{host}{portString}{BasicsStationNetworkServer.DataEndpoint}/B827EBFFFEE1E39A""}}"; // act - await this.lnsMessageProcessorMock.InternalHandleDiscoveryAsync(this.httpContextMock.Object, this.socketMock.Object, CancellationToken.None); + await this.lnsMessageProcessorMock.HandleDiscoveryAsync(this.httpContextMock.Object, CancellationToken.None); // assert Assert.Equal(expectedString, sentString); @@ -298,7 +224,7 @@ private static RadioMetadata GetExpectedRadioMetadata() } [Fact] - public async Task InternalHandleDataAsync_ShouldProperlyCreateLoraPayloadForUpdfRequest() + public async Task HandleDataAsync_ShouldProperlyCreateLoraPayloadForUpdfRequest() { // arrange var message = JsonUtil.Strictify(@"{'msgtype':'updf','MHdr':128,'DevAddr':50244358,'FCtrl':0,'FCnt':1,'FOpts':'','FPort':8,'FRMPayload':'CB', @@ -310,6 +236,7 @@ public async Task InternalHandleDataAsync_ShouldProperlyCreateLoraPayloadForUpdf var expectedMic = Mic.Read(new byte[] { 100, 58, 178, 2 }); SetDataPathParameter(); SetupSocketReceiveAsync(message); + _ = SetupWebSocketConnection(); // intercepting messageDispatcher LoRaRequest loRaRequest = null; @@ -317,9 +244,7 @@ public async Task InternalHandleDataAsync_ShouldProperlyCreateLoraPayloadForUpdf .Callback((req) => loRaRequest = req); // act - await this.lnsMessageProcessorMock.InternalHandleDataAsync(this.httpContextMock.Object.Request.RouteValues, - this.socketMock.Object, - CancellationToken.None); + await this.lnsMessageProcessorMock.HandleDataAsync(this.httpContextMock.Object, CancellationToken.None); // assert Assert.NotNull(loRaRequest); @@ -333,7 +258,7 @@ public async Task InternalHandleDataAsync_ShouldProperlyCreateLoraPayloadForUpdf } [Fact] - public async Task InternalHandleDataAsync_ShouldProperlyCreateLoraPayloadForJoinRequest() + public async Task HandleDataAsync_ShouldProperlyCreateLoraPayloadForJoinRequest() { // arrange var message = JsonUtil.Strictify(@"{'msgtype':'jreq','MHdr':0,'JoinEui':'47-62-78-C8-E5-D2-C4-B5','DevEui':'85-27-C1-DF-EE-A4-16-9E', @@ -347,6 +272,7 @@ public async Task InternalHandleDataAsync_ShouldProperlyCreateLoraPayloadForJoin var expectedDevNonce = DevNonce.Read(new byte[] { 88, 212 }); SetDataPathParameter(); SetupSocketReceiveAsync(message); + _ = SetupWebSocketConnection(); // intercepting messageDispatcher LoRaRequest loRaRequest = null; @@ -354,9 +280,7 @@ public async Task InternalHandleDataAsync_ShouldProperlyCreateLoraPayloadForJoin .Callback((req) => loRaRequest = req); // act - await this.lnsMessageProcessorMock.InternalHandleDataAsync(this.httpContextMock.Object.Request.RouteValues, - this.socketMock.Object, - CancellationToken.None); + await this.lnsMessageProcessorMock.HandleDataAsync(this.httpContextMock.Object, CancellationToken.None); // assert Assert.NotNull(loRaRequest); @@ -387,6 +311,26 @@ public async Task InternalHandleDataAsync_ShouldThrow_OnNotExpectedMessageTypes( CancellationToken.None)); } + [Fact] + public async Task InternalHandleDataAsync_Starts_And_Stops_Message_Tracing() + { + // arrange + var disposableMock = new Mock(); + this.tracingMock.Setup(t => t.TrackDataMessage()).Returns(disposableMock.Object); + SetDataPathParameter(); + InitializeConfigurationServiceMock(); + SetupSocketReceiveAsync($@"{{ msgtype: '{LnsMessageType.Version.ToBasicStationString()}', station: 'stationName', package: '1.0.0' }}"); + + // act + await this.lnsMessageProcessorMock.InternalHandleDataAsync(this.httpContextMock.Object.Request.RouteValues, + this.socketMock.Object, + CancellationToken.None); + + // assert + this.tracingMock.Verify(t => t.TrackDataMessage(), Times.Once); + disposableMock.Verify(t => t.Dispose(), Times.Once); + } + [Fact] public async Task InternalHandleDataAsync_Should_Rethrow_If_Value_Not_Present() { diff --git a/Tests/Unit/NetworkServer/LnsRemoteCallHandlerTests.cs b/Tests/Unit/NetworkServer/LnsRemoteCallHandlerTests.cs new file mode 100644 index 0000000000..95485281c7 --- /dev/null +++ b/Tests/Unit/NetworkServer/LnsRemoteCallHandlerTests.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Unit.NetworkServer +{ + using System; + using System.Net; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + using Bogus; + using global::LoRaTools; + using LoRaWan.NetworkServer; + using LoRaWan.Tests.Common; + using Microsoft.Extensions.Logging; + using Moq; + using Xunit; + + public sealed class LnsRemoteCallHandlerTests + { + private readonly Faker faker = new(); + private readonly NetworkServerConfiguration networkServerConfiguration; + private readonly Mock classCMessageSender; + private readonly Mock loRaDeviceRegistry; + private readonly Mock> logger; + private readonly LnsRemoteCallHandler subject; + + public LnsRemoteCallHandlerTests() + { + this.networkServerConfiguration = new NetworkServerConfiguration(); + this.classCMessageSender = new Mock(); + this.loRaDeviceRegistry = new Mock(); + this.logger = new Mock>(); + this.subject = new LnsRemoteCallHandler(this.networkServerConfiguration, + this.classCMessageSender.Object, + this.loRaDeviceRegistry.Object, + this.logger.Object, + TestMeter.Instance); + } + + + [Fact] + public async Task CloseConnectionAsync_Should_Work_As_Expected() + { + // arrange + var devEui = new DevEui(0); + var mockedDevice = new Mock(null, devEui, null); + _ = this.loRaDeviceRegistry.Setup(x => x.GetDeviceByDevEUIAsync(devEui)).ReturnsAsync(mockedDevice.Object); + var c2d = JsonSerializer.Serialize(new + { + DevEui = devEui.ToString(), + Fport = 1, + MessageId = Guid.NewGuid(), + }); + + // act + _ = await CloseConnectionAsync(c2d); + + // assert + this.loRaDeviceRegistry.VerifyAll(); + mockedDevice.Verify(x => x.CloseConnectionAsync(It.IsAny(), true), Times.Once); + } + + [Fact] + public async Task ClearCache_When_Correct_Should_Work() + { + // arrange + this.loRaDeviceRegistry.Setup(x => x.ResetDeviceCacheAsync()).Returns(Task.CompletedTask); + this.networkServerConfiguration.IoTEdgeTimeout = 5; + + // act + await this.subject.ExecuteAsync(new LnsRemoteCall(RemoteCallKind.ClearCache, null), CancellationToken.None); + + // assert + this.loRaDeviceRegistry.VerifyAll(); + } + + public static TheoryData DropConnectionInvalidMessages => + TheoryDataFactory.From( + (string.Empty, "Unable to parse Json when attempting to close"), + ("null", "Missing payload when attempting to close the"), + (JsonSerializer.Serialize(new { DevEui = (string)null, Fport = 1 }), "DevEUI missing"), + (JsonSerializer.Serialize(new { DevEui = new DevEui(0).ToString(), Fport = 1, MessageId = 123 }), "Unable to parse Json")); + + [Theory] + [MemberData(nameof(DropConnectionInvalidMessages))] + public async Task CloseConnectionAsync_Should_Return_Bad_Request_When_Invalid_Message(string json, string expectedLogPattern) + { + // act + var response = await CloseConnectionAsync(json); + + // assert + Assert.Equal(HttpStatusCode.BadRequest, response); + var log = Assert.Single(this.logger.GetLogInvocations()); + Assert.Matches(expectedLogPattern, log.Message); + this.loRaDeviceRegistry.VerifyNoOtherCalls(); + } + + [Fact] + public async Task CloseConnectionAsync_Should_Return_NotFound_When_Device_Not_Found() + { + // arrange + var devEui = new DevEui(0); + var c2d = JsonSerializer.Serialize(new { DevEui = devEui.ToString(), Fport = 1 }); + + // act + var response = await CloseConnectionAsync(c2d); + + // assert + Assert.Equal(HttpStatusCode.NotFound, response); + this.loRaDeviceRegistry.Verify(x => x.GetDeviceByDevEUIAsync(devEui), Times.Once); + this.loRaDeviceRegistry.VerifyNoOtherCalls(); + } + + [Fact] + public async Task SendCloudToDeviceMessageAsync_When_Correct_Should_Work() + { + // arrange + this.classCMessageSender.Setup(x => x.SendAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); + var c2d = "{\"test\":\"asd\"}"; + + // act + var response = await SendCloudToDeviceMessageAsync(c2d); + + // assert + Assert.Equal(HttpStatusCode.OK, response); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public async Task SendCloudToDeviceMessageAsync_When_ClassC_Msg_Is_Null_Or_Empty_Should_Return_Not_Found(string json) + { + this.classCMessageSender.Setup(x => x.SendAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); + + var response = await SendCloudToDeviceMessageAsync(json); + Assert.Equal(HttpStatusCode.BadRequest, response); + } + + [Fact] + public async Task SendCloudToDeviceMessageAsync_When_ClassC_Msg_Is_Not_CorrectJson_Should_Return_Not_Found() + { + var response = await SendCloudToDeviceMessageAsync(this.faker.Random.String2(10)); + Assert.Equal(HttpStatusCode.BadRequest, response); + } + + private Task CloseConnectionAsync(string payload) => + this.subject.ExecuteAsync(new LnsRemoteCall(RemoteCallKind.CloseConnection, payload), CancellationToken.None); + + private Task SendCloudToDeviceMessageAsync(string payload) => + this.subject.ExecuteAsync(new LnsRemoteCall(RemoteCallKind.CloudToDeviceMessage, payload), CancellationToken.None); + } +} diff --git a/Tests/Unit/NetworkServer/LoRaApiHttpClientTests.cs b/Tests/Unit/NetworkServer/LoRaApiHttpClientTests.cs new file mode 100644 index 0000000000..2eb1a8acd4 --- /dev/null +++ b/Tests/Unit/NetworkServer/LoRaApiHttpClientTests.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Unit.NetworkServer +{ + using System.Net.Http; + using global::LoRaTools.CommonAPI; + using LoRaWan.NetworkServer; + using Microsoft.Extensions.DependencyInjection; + using Xunit; + + public sealed class LoRaApiHttpClientTests + { + [Fact] + public void AddApiClient_Allows_Creation_Of_Named_Client() + { + // arrange + var services = new ServiceCollection(); + + // act + var result = services.AddApiClient(new NetworkServerConfiguration(), ApiVersion.LatestVersion); + + // assert + var httpClientFactory = result.BuildServiceProvider().GetRequiredService(); + Assert.NotNull(httpClientFactory.CreateClient(LoRaApiHttpClient.Name)); + } + } +} diff --git a/Tests/Unit/NetworkServer/LoRaCloudToDeviceMessageWrapperTest.cs b/Tests/Unit/NetworkServer/LoRaCloudToDeviceMessageWrapperTest.cs index 0efc909486..7040702b54 100644 --- a/Tests/Unit/NetworkServer/LoRaCloudToDeviceMessageWrapperTest.cs +++ b/Tests/Unit/NetworkServer/LoRaCloudToDeviceMessageWrapperTest.cs @@ -6,6 +6,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer using System; using System.Linq; using System.Text; + using System.Threading.Tasks; using global::LoRaTools; using LoRaWan.NetworkServer; using LoRaWan.Tests.Common; @@ -13,7 +14,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer using Moq; using Xunit; - public sealed class LoRaCloudToDeviceMessageWrapperTest : IDisposable + public sealed class LoRaCloudToDeviceMessageWrapperTest : IAsyncDisposable { private readonly SingleDeviceConnectionManager connectionManager; private readonly LoRaDevice sampleDevice; @@ -97,10 +98,10 @@ public void When_Body_Has_MacCommand_With_Parameters_Should_Contain_It_List(stri Assert.Equal(2, dutyCycleCmd.DutyCyclePL); } - public void Dispose() + public async ValueTask DisposeAsync() { - this.sampleDevice.Dispose(); - this.connectionManager.Dispose(); + await this.sampleDevice.DisposeAsync(); + await this.connectionManager.DisposeAsync(); } } } diff --git a/Tests/Unit/NetworkServer/LoRaDeviceApiServiceTests.cs b/Tests/Unit/NetworkServer/LoRaDeviceApiServiceTests.cs index 607e34099e..d42707a5d8 100644 --- a/Tests/Unit/NetworkServer/LoRaDeviceApiServiceTests.cs +++ b/Tests/Unit/NetworkServer/LoRaDeviceApiServiceTests.cs @@ -4,12 +4,13 @@ namespace LoRaWan.Tests.Unit.NetworkServer { using System; - using System.IO; using System.Linq; using System.Net.Http; + using System.Net.Mime; using System.Text; using System.Threading; using System.Threading.Tasks; + using global::LoRaTools.CommonAPI; using LoRaWan.NetworkServer; using LoRaWan.Tests.Common; using Microsoft.Extensions.Logging.Abstractions; @@ -18,6 +19,8 @@ namespace LoRaWan.Tests.Unit.NetworkServer public class LoRaDeviceApiServiceTests { + private const string SearchByEuiAsyncValidJsonResponse = @"{""primaryKey"":""1234""}"; + [Theory] [InlineData("https://aka.ms", "api/A?code=B", "https://aka.ms/api/A?code=B")] [InlineData("https://aka.ms/", "api/A?code=B", "https://aka.ms/api/A?code=B")] @@ -60,15 +63,9 @@ public void BuildUri_Success(string basePath, string relativePath, string expect public async Task SearchByEuiAsync_StationEui_Is_Compatible_With_Contract() { // arrange - var facadeMock = new Mock(); - using var httpHandlerMock = new HttpMessageHandlerMock(); - httpHandlerMock.SetupHandler(r => new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(SearchByDevEuiContract.Response, Encoding.UTF8, "application/json"), - }); - using var httpClient = new HttpClient(httpHandlerMock); - facadeMock.Setup(f => f.GetHttpClient()).Returns(httpClient); - var subject = Setup(facadeMock.Object); + using var content = new StringContent(SearchByDevEuiContract.Response, Encoding.UTF8, "application/json"); + using var httpClientFactoryMock = SetupHttpClientFactoryMock(content); + var subject = Setup(httpClientFactoryMock.Value); // act var result = await subject.GetPrimaryKeyByEuiAsync(new StationEui(1)); @@ -81,15 +78,9 @@ public async Task SearchByEuiAsync_StationEui_Is_Compatible_With_Contract() public async Task SearchByEuiAsync_DevEui_Is_Compatible_With_Contract() { // arrange - var facadeMock = new Mock(); - using var httpHandlerMock = new HttpMessageHandlerMock(); - httpHandlerMock.SetupHandler(r => new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(SearchByDevEuiContract.Response, Encoding.UTF8, "application/json"), - }); - using var httpClient = new HttpClient(httpHandlerMock); - facadeMock.Setup(f => f.GetHttpClient()).Returns(httpClient); - var subject = Setup(facadeMock.Object); + using var content = new StringContent(SearchByDevEuiContract.Response, Encoding.UTF8, "application/json"); + using var httpClientFactoryMock = SetupHttpClientFactoryMock(content); + var subject = Setup(httpClientFactoryMock.Value); // act var result = await subject.GetPrimaryKeyByEuiAsync(new DevEui(1)); @@ -109,15 +100,9 @@ public async Task SearchByEuiAsync_DevEui_Is_Compatible_With_Contract() public async Task SearchByEuiAsync_DevEui_Parses_Json(string json, string expected) { // arrange - var facadeMock = new Mock(); - using var httpHandlerMock = new HttpMessageHandlerMock(); - httpHandlerMock.SetupHandler(r => new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(json, Encoding.UTF8, "application/json"), - }); - using var httpClient = new HttpClient(httpHandlerMock); - facadeMock.Setup(f => f.GetHttpClient()).Returns(httpClient); - var subject = Setup(facadeMock.Object); + using var content = new StringContent(json, Encoding.UTF8, "application/json"); + using var httpClientFactoryMock = SetupHttpClientFactoryMock(content); + var subject = Setup(httpClientFactoryMock.Value); // act var result = await subject.GetPrimaryKeyByEuiAsync(new DevEui(1)); @@ -129,17 +114,11 @@ public async Task SearchByEuiAsync_DevEui_Parses_Json(string json, string expect [Fact] public async Task FetchStationFirmwareAsync_Returns_Stream_And_Content_Length() { - var contentBytes = Encoding.UTF8.GetBytes("Test"); // arrange - var facadeMock = new Mock(); - using var httpHandlerMock = new HttpMessageHandlerMock(); - httpHandlerMock.SetupHandler(r => new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new ByteArrayContent(contentBytes) - }); - using var httpClient = new HttpClient(httpHandlerMock); - facadeMock.Setup(f => f.GetHttpClient()).Returns(httpClient); - var subject = Setup(facadeMock.Object); + var contentBytes = Encoding.UTF8.GetBytes("Test"); + using var byteArrayContent = new ByteArrayContent(contentBytes); + using var httpClientFactoryMock = SetupHttpClientFactoryMock(byteArrayContent); + var subject = Setup(httpClientFactoryMock.Value); // act var content = await subject.FetchStationFirmwareAsync(new StationEui(ulong.MaxValue), CancellationToken.None); @@ -151,15 +130,78 @@ public async Task FetchStationFirmwareAsync_Returns_Stream_And_Content_Length() Assert.Equal(expectedBytes, contentBytes); } + [Fact] + public Task NextFCntDownAsync_Disposes_HttpClient() => + ApiCall_Disposes_HttpClient(s => s.NextFCntDownAsync(new DevEui(1), 1, 1, "gateway1")); + + [Fact] + public Task ExecuteFunctionBundlerAsync_Disposes_HttpClient() => + ApiCall_Disposes_HttpClient(s => s.ExecuteFunctionBundlerAsync(new DevEui(1), new FunctionBundlerRequest()), "{}"); + + [Fact] + public Task ABPFcntCacheResetAsync_Disposes_HttpClient() => + ApiCall_Disposes_HttpClient(s => s.ABPFcntCacheResetAsync(new DevEui(1), 0, "gateway1")); + + [Fact] + public Task SearchAndLockForJoinAsync_Disposes_HttpClient() => + ApiCall_Disposes_HttpClient(s => s.SearchAndLockForJoinAsync("gateway1", new DevEui(1), new DevNonce(2)), "[]"); + + [Fact] + public Task SearchByDevAddr_Disposes_HttpClient() => + ApiCall_Disposes_HttpClient(s => s.SearchByDevAddrAsync(new DevAddr(1)), "[]"); + + [Fact] + public Task GetPrimaryKeyByEuiAsync_StationEui_Disposes_HttpClient() => + ApiCall_Disposes_HttpClient(s => s.GetPrimaryKeyByEuiAsync(new StationEui(1)), SearchByEuiAsyncValidJsonResponse); + + [Fact] + public Task GetPrimaryKeyByEuiAsync_DevEui_Disposes_HttpClient() => + ApiCall_Disposes_HttpClient(s => s.GetPrimaryKeyByEuiAsync(new DevEui(1)), SearchByEuiAsyncValidJsonResponse); + + [Fact] + public Task FetchStationCredentialsAsync_Disposes_HttpClient() => + ApiCall_Disposes_HttpClient(s => s.FetchStationCredentialsAsync(new StationEui(1), ConcentratorCredentialType.Lns, CancellationToken.None)); + + [Fact] + public Task FetchStationFirmwareAsync_Disposes_HttpClient() => + ApiCall_Disposes_HttpClient(s => s.FetchStationFirmwareAsync(new StationEui(1), CancellationToken.None)); + + private static async Task ApiCall_Disposes_HttpClient(Func callApiAsync, string jsonContent = null) + { + // arrange + using var content = jsonContent is { } someJsonContent ? new StringContent(someJsonContent, Encoding.UTF8, MediaTypeNames.Application.Json) : new StringContent("foo"); + using var httpClientFactoryMock = SetupHttpClientFactoryMock(content); + var subject = Setup(httpClientFactoryMock.Value); + + // act + await callApiAsync(subject); + + // assert + _ = Assert.Throws(() => httpClientFactoryMock.Value.HttpClient.CancelPendingRequests()); + } + + private static DisposableValue SetupHttpClientFactoryMock(HttpContent content) + { +#pragma warning disable CA2000 // Dispose objects before losing scope (disposal as part of DisposableValue) + var httpHandlerMock = new HttpMessageHandlerMock(); + var result = new MockHttpClientFactory(httpHandlerMock); +#pragma warning restore CA2000 // Dispose objects before losing scope + httpHandlerMock.SetupHandler(_ => new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = content + }); + return new DisposableValue(result, () => { httpHandlerMock.Dispose(); result.Dispose(); }); + } + private static LoRaDeviceAPIService Setup(string basePath) => new LoRaDeviceAPIService(new NetworkServerConfiguration { FacadeServerUrl = new Uri(basePath) }, - new Mock().Object, + new Mock().Object, NullLogger.Instance, TestMeter.Instance); - private static LoRaDeviceAPIService Setup(IServiceFacadeHttpClientProvider facadeHttpClientProvider) => + private static LoRaDeviceAPIService Setup(IHttpClientFactory httpClientFactory) => new LoRaDeviceAPIService(new NetworkServerConfiguration { FacadeServerUrl = new Uri("https://aka.ms/api/") }, - facadeHttpClientProvider, + httpClientFactory, NullLogger.Instance, TestMeter.Instance); } diff --git a/Tests/Unit/NetworkServer/LoRaDeviceCacheTest.cs b/Tests/Unit/NetworkServer/LoRaDeviceCacheTest.cs index d5dfdd5337..b5b4b7cd5d 100644 --- a/Tests/Unit/NetworkServer/LoRaDeviceCacheTest.cs +++ b/Tests/Unit/NetworkServer/LoRaDeviceCacheTest.cs @@ -17,21 +17,20 @@ namespace LoRaWan.Tests.Unit.NetworkServer public class LoRaDeviceCacheTest { - [Fact] public async Task When_Device_Expires_It_Is_Refreshed() { var moqCallback = new Mock>(); - using var cache = new TestDeviceCache(moqCallback.Object, this.quickRefreshOptions, true); + await using var cache = new TestDeviceCache(moqCallback.Object, this.quickRefreshOptions, true); var deviceMock = CreateMockDevice(); - var disposableMock = new Mock(); + var disposableMock = new Mock(); deviceMock.Setup(x => x.BeginDeviceClientConnectionActivity()) .Returns(disposableMock.Object); var device = deviceMock.Object; cache.Register(device); await cache.WaitForRefreshAsync(CancellationToken.None); moqCallback.Verify(x => x.Invoke(device)); - disposableMock.Verify(x => x.Dispose(), Times.Once); + disposableMock.Verify(x => x.DisposeAsync(), Times.Once); } [Fact] @@ -42,7 +41,7 @@ public async Task When_Cache_Is_Disposed_While_Waiting_For_Refresh_Refresh_Stops RefreshInterval = TimeSpan.FromSeconds(250) }; var cache = new TestDeviceCache(options); - cache.Dispose(); + await cache.DisposeAsync(); var count = cache.RefreshOperationsCount; await Task.Delay(700); @@ -52,19 +51,19 @@ public async Task When_Cache_Is_Disposed_While_Waiting_For_Refresh_Refresh_Stops [Fact] public async Task When_Device_Is_Fresh_No_Refresh_Is_Triggered() { - using var cache = new TestDeviceCache(this.quickRefreshOptions); - using var device = CreateTestDevice(); + await using var cache = new TestDeviceCache(this.quickRefreshOptions); + await using var device = CreateTestDevice(); device.LastUpdate = DateTime.UtcNow + TimeSpan.FromMinutes(1); cache.Register(device); - using var cts = new CancellationTokenSource(this.quickRefreshOptions.ValidationInterval * 2); + using var cts = this.quickRefreshOptions.ValidationIntervalCancellationToken(); await Assert.ThrowsAsync(() => cache.WaitForRefreshAsync(cts.Token)); } [Fact] public async Task When_Disposed_While_Refreshing_We_Shutdown_Gracefully() { - using var cache = new TestDeviceCache(this.quickRefreshOptions, true); + await using var cache = new TestDeviceCache(this.quickRefreshOptions, true); var deviceMock = CreateMockDevice(); deviceMock.Setup(x => x.InitializeAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((NetworkServerConfiguration config, CancellationToken token) => @@ -81,16 +80,16 @@ public async Task When_Disposed_While_Refreshing_We_Shutdown_Gracefully() await Task.Delay(5); } - cache.Dispose(); + await cache.DisposeAsync(); var count = cache.DeviceRefreshCount; - await Task.Delay(this.quickRefreshOptions.ValidationInterval * 2); + await Task.Delay(this.quickRefreshOptions.ValidationIntervalDelay()); Assert.Equal(count, cache.DeviceRefreshCount); } [Fact] public async Task When_Refresh_Fails_It_Is_Retried() { - using var cache = new TestDeviceCache(this.quickRefreshOptions, true); + await using var cache = new TestDeviceCache(this.quickRefreshOptions, true); var deviceMock = CreateMockDevice(); deviceMock.SetupSequence(x => x.InitializeAsync(It.IsAny(), It.IsAny())) .ThrowsAsync(new LoRaProcessingException("Refresh failed.", LoRaProcessingErrorCode.DeviceInitializationFailed)) @@ -111,59 +110,59 @@ public async Task When_Device_Inactive_It_Is_Removed() { MaxUnobservedLifetime = TimeSpan.FromMilliseconds(1) }; - using var cache = new TestDeviceCache(options); + await using var cache = new TestDeviceCache(options); var connectionManager = new Mock(); - using var device = new LoRaDevice(new DevAddr(0xabc), new DevEui(0x123), connectionManager.Object) { LastSeen = DateTime.UtcNow }; + await using var device = new LoRaDevice(new DevAddr(0xabc), new DevEui(0x123), connectionManager.Object) { LastSeen = DateTime.UtcNow }; cache.Register(device); - using var cts = new CancellationTokenSource(this.quickRefreshOptions.ValidationInterval * 2); + using var cts = this.quickRefreshOptions.ValidationIntervalCancellationToken(); await cache.WaitForRemoveAsync(cts.Token); Assert.False(cache.TryGetByDevEui(device.DevEUI, out _)); - connectionManager.Verify(x => x.Release(device), Times.Once); + connectionManager.Verify(x => x.ReleaseAsync(device), Times.Once); } [Fact] - public void Trying_To_Remove_Device_That_Was_Not_Registered_Fails() + public async Task Trying_To_Remove_Device_That_Was_Not_Registered_Fails() { - using var cache = CreateNoRefreshCache(); - using var device = CreateTestDevice(); - Assert.False(cache.Remove(device)); + await using var cache = CreateNoRefreshCache(); + await using var device = CreateTestDevice(); + Assert.False(await cache.RemoveAsync(device)); } [Fact] - public void Remove_Registered_Device_Succeeds() + public async Task Remove_Registered_Device_Succeeds() { - using var cache = CreateNoRefreshCache(); - using var device = CreateTestDevice(); + await using var cache = CreateNoRefreshCache(); + await using var device = CreateTestDevice(); cache.Register(device); - Assert.True(cache.Remove(device)); + Assert.True(await cache.RemoveAsync(device)); Assert.False(cache.TryGetByDevEui(device.DevEUI, out _)); } [Fact] - public void When_Last_Device_Is_Removed_DevAddr_Registry_Is_Cleared() + public async Task When_Last_Device_Is_Removed_DevAddr_Registry_Is_Cleared() { - using var cache = CreateNoRefreshCache(); - using var device = CreateTestDevice(); + await using var cache = CreateNoRefreshCache(); + await using var device = CreateTestDevice(); cache.Register(device); - Assert.True(cache.Remove(device)); + Assert.True(await cache.RemoveAsync(device)); Assert.False(cache.HasRegistrations(device.DevAddr.Value)); } [Fact] - public void Adding_And_Removing_Join_Device_Succeeds() + public async Task Adding_And_Removing_Join_Device_Succeeds() { - using var cache = CreateNoRefreshCache(); - using var device = CreateTestDevice(); + await using var cache = CreateNoRefreshCache(); + await using var device = CreateTestDevice(); device.DevAddr = null; cache.Register(device); Assert.True(cache.TryGetByDevEui(device.DevEUI, out _)); ValidateStats(1, 0); - Assert.True(cache.Remove(device)); + Assert.True(await cache.RemoveAsync(device)); Assert.False(cache.TryGetByDevEui(device.DevEUI, out _)); ValidateStats(1, 1); @@ -175,27 +174,25 @@ void ValidateStats(int hit, int miss) } } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void When_Removing_Device_Is_Disposed_On_Request(bool dispose) + [Fact] + public async Task When_Removing_Device_Is_Disposed() { - using var cache = CreateNoRefreshCache(); + await using var cache = CreateNoRefreshCache(); var deviceMock = CreateMockDevice(); var device = deviceMock.Object; cache.Register(device); - Assert.True(cache.Remove(device, dispose)); + Assert.True(await cache.RemoveAsync(device)); Assert.False(cache.TryGetByDevEui(device.DevEUI, out _)); - deviceMock.Protected().Verify(nameof(device.Dispose), dispose ? Times.Once() : Times.Never(), true, true); + deviceMock.Protected().Verify("DisposeAsyncCore", Times.Once()); } [Fact] - public void Registering_And_Unregistering_Multiple_Devices_With_Matching_DevAddr_Succeeds() + public async Task Registering_And_Unregistering_Multiple_Devices_With_Matching_DevAddr_Succeeds() { - using var cache = CreateNoRefreshCache(); - using var device1 = CreateTestDevice(); - using var device2 = CreateTestDevice(); + await using var cache = CreateNoRefreshCache(); + await using var device1 = CreateTestDevice(); + await using var device2 = CreateTestDevice(); device2.DevEUI = new DevEui(0xaaa); Assert.Equal(device1.DevAddr, device2.DevAddr); @@ -208,11 +205,11 @@ public void Registering_And_Unregistering_Multiple_Devices_With_Matching_DevAddr Assert.True(cache.HasRegistrations(devAddr)); Assert.Equal(2, cache.RegistrationCount(devAddr)); - Assert.True(cache.Remove(device1)); + Assert.True(await cache.RemoveAsync(device1)); Assert.True(cache.HasRegistrations(devAddr)); Assert.Equal(1, cache.RegistrationCount(devAddr)); - Assert.True(cache.Remove(device2)); + Assert.True(await cache.RemoveAsync(device2)); Assert.False(cache.HasRegistrations(devAddr)); Assert.Equal(0, cache.RegistrationCount(devAddr)); } @@ -220,10 +217,10 @@ public void Registering_And_Unregistering_Multiple_Devices_With_Matching_DevAddr [Theory] [InlineData(true)] [InlineData(false)] - public void Mic_Validation_Is_Used_To_Validate_Items(bool isValid) + public async Task Mic_Validation_Is_Used_To_Validate_Items(bool isValid) { - using var device = CreateTestDevice(); - using var cache = new TestDeviceCache(this.noRefreshOptions, (_, _) => isValid); + await using var device = CreateTestDevice(); + await using var cache = new TestDeviceCache(this.noRefreshOptions, (_, _) => isValid); cache.Register(device); @@ -240,10 +237,10 @@ public void Mic_Validation_Is_Used_To_Validate_Items(bool isValid) } [Fact] - public void Device_LastSeen_Is_Updated_When_Registered() + public async Task Device_LastSeen_Is_Updated_When_Registered() { - using var device = CreateTestDevice(); - using var cache = CreateNoRefreshCache(); + await using var device = CreateTestDevice(); + await using var cache = CreateNoRefreshCache(); var initialLastSeen = device.LastSeen = DateTime.UtcNow - TimeSpan.FromMinutes(1); cache.Register(device); @@ -253,10 +250,10 @@ public void Device_LastSeen_Is_Updated_When_Registered() [Theory] [InlineData(true)] [InlineData(false)] - public void Last_Seen_Is_Updated_Depending_On_Cache_Hit(bool cacheHit) + public async Task Last_Seen_Is_Updated_Depending_On_Cache_Hit(bool cacheHit) { - using var device = CreateTestDevice(); - using var cache = new TestDeviceCache(this.noRefreshOptions, (_, _) => cacheHit); + await using var device = CreateTestDevice(); + await using var cache = new TestDeviceCache(this.noRefreshOptions, (_, _) => cacheHit); cache.Register(device); @@ -274,29 +271,29 @@ public void Last_Seen_Is_Updated_Depending_On_Cache_Hit(bool cacheHit) } [Fact] - public void When_Trying_To_Cleanup_Same_DevAddress_Fails() + public async Task When_Trying_To_Cleanup_Same_DevAddress_Fails() { - using var cache = CreateNoRefreshCache(); - using var device = CreateTestDevice(); + await using var cache = CreateNoRefreshCache(); + await using var device = CreateTestDevice(); Assert.Throws(() => cache.CleanupOldDevAddrForDevice(device, device.DevAddr.Value)); } [Fact] - public void When_Trying_To_Cleanup_Non_Existing_Old_Address_Fails() + public async Task When_Trying_To_Cleanup_Non_Existing_Old_Address_Fails() { - using var cache = CreateNoRefreshCache(); - using var device = CreateTestDevice(); + await using var cache = CreateNoRefreshCache(); + await using var device = CreateTestDevice(); Assert.Throws(() => cache.CleanupOldDevAddrForDevice(device, new DevAddr(0x00ffffff))); } [Fact] - public void When_Trying_To_Cleanup_OldDevAddr_With_Different_Device_Instance_Fails() + public async Task When_Trying_To_Cleanup_OldDevAddr_With_Different_Device_Instance_Fails() { - using var cache = CreateNoRefreshCache(); - using var device1 = CreateTestDevice(); - using var device2 = CreateTestDevice(); + await using var cache = CreateNoRefreshCache(); + await using var device1 = CreateTestDevice(); + await using var device2 = CreateTestDevice(); device2.DevAddr = new DevAddr(0x00ffffff); cache.Register(device2); @@ -305,10 +302,10 @@ public void When_Trying_To_Cleanup_OldDevAddr_With_Different_Device_Instance_Fai } [Fact] - public void When_Cleaning_Up_Old_DevAddr_Entry_Is_Removed() + public async Task When_Cleaning_Up_Old_DevAddr_Entry_Is_Removed() { - using var cache = CreateNoRefreshCache(); - using var device = CreateTestDevice(); + await using var cache = CreateNoRefreshCache(); + await using var device = CreateTestDevice(); cache.Register(device); @@ -320,22 +317,22 @@ public void When_Cleaning_Up_Old_DevAddr_Entry_Is_Removed() } [Fact] - public void When_Registering_After_Join_With_Different_Device_Throws() + public async Task When_Registering_After_Join_With_Different_Device_Throws() { - using var cache = CreateNoRefreshCache(); - using var device = CreateTestDevice(); + await using var cache = CreateNoRefreshCache(); + await using var device = CreateTestDevice(); device.DevAddr = null; cache.Register(device); - using var device2 = CreateTestDevice(); + await using var device2 = CreateTestDevice(); Assert.Throws(() => cache.Register(device2)); } [Fact] - public void When_Resetting_Cache_All_Connections_Are_Released() + public async Task When_Resetting_Cache_All_Connections_Are_Released() { - using var cache = CreateNoRefreshCache(); + await using var cache = CreateNoRefreshCache(); var items = Enumerable.Range(1, 2).Select(x => { var connectionMgr = new Mock(); @@ -344,11 +341,11 @@ public void When_Resetting_Cache_All_Connections_Are_Released() return (device, connectionMgr); }).ToArray(); - cache.Reset(); + await cache.ResetAsync(); foreach (var (device, connectionMgr) in items) { - connectionMgr.Verify(x => x.Release(device), Times.Once); + connectionMgr.Verify(x => x.ReleaseAsync(device), Times.Once); } } private static Mock CreateMockDevice() @@ -450,13 +447,11 @@ protected override void OnRefresh() RefreshOperationsCount++; } - public override bool Remove(LoRaDevice loRaDevice, bool dispose = true) + public override async Task RemoveAsync(LoRaDevice device) { + var ret = await base.RemoveAsync(device); if (this.removeTick.CurrentCount == 0) this.removeTick.Release(); - - var ret = base.Remove(loRaDevice, dispose); - return ret; } @@ -465,15 +460,22 @@ protected override bool ValidateMic(LoRaDevice loRaDevice, LoRaPayload loRaPaylo return this.validateMic == null || this.validateMic(loRaDevice, loRaPayload); } - protected override void Dispose(bool dispose) + protected override ValueTask DisposeAsync(bool dispose) { if (dispose) { this.refreshTick.Dispose(); this.removeTick.Dispose(); } - base.Dispose(dispose); + return base.DisposeAsync(dispose); } } } + internal static class LoRaDeviceCacheOptionsExtensions + { + public static TimeSpan ValidationIntervalDelay(this LoRaDeviceCacheOptions options) + => options.ValidationInterval * 3; + + public static CancellationTokenSource ValidationIntervalCancellationToken(this LoRaDeviceCacheOptions options) => new CancellationTokenSource(options.ValidationIntervalDelay()); + } } diff --git a/Tests/Unit/NetworkServer/LoRaDeviceClientConnectionManagerTests.cs b/Tests/Unit/NetworkServer/LoRaDeviceClientConnectionManagerTests.cs index c4b8057479..a5dca0f8d7 100644 --- a/Tests/Unit/NetworkServer/LoRaDeviceClientConnectionManagerTests.cs +++ b/Tests/Unit/NetworkServer/LoRaDeviceClientConnectionManagerTests.cs @@ -1,60 +1,431 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +#nullable enable + namespace LoRaWan.Tests.Unit.NetworkServer { using System; + using System.Collections.Generic; + using System.IO; using System.Linq; + using System.Threading; + using System.Threading.Tasks; using LoRaWan.NetworkServer; - using LoRaWan.Tests.Common; + using Common; + using Microsoft.Azure.Devices.Client; + using Microsoft.Azure.Devices.Shared; using Microsoft.Extensions.Caching.Memory; - using Microsoft.Extensions.Logging.Abstractions; + using Microsoft.Extensions.Logging; using Moq; using Xunit; + using Xunit.Abstractions; - public class LoRaDeviceClientConnectionManagerTests + public sealed class LoRaDeviceClientConnectionManagerTests : IAsyncDisposable { + private readonly Mock cacheMock; + private readonly LoRaDeviceClientConnectionManager subject; + private readonly TestOutputLoggerFactory loggerFactory; + private readonly ILogger logger; + private LoRaDevice? testDevice; + + public LoRaDeviceClientConnectionManagerTests(ITestOutputHelper testOutputHelper) + { + this.cacheMock = new Mock(); + var cache = this.cacheMock.Object; + this.loggerFactory = new TestOutputLoggerFactory(testOutputHelper); + this.logger = new TestOutputLogger(testOutputHelper); + this.subject = new LoRaDeviceClientConnectionManager(cache, this.loggerFactory, this.logger); + } + + public async ValueTask DisposeAsync() + { + await this.subject.DisposeAsync(); + if (this.testDevice is { } someTestDevice) + await someTestDevice.DisposeAsync(); + this.loggerFactory.Dispose(); + } + + private LoRaDevice TestDevice => this.testDevice ??= new(null, new DevEui(42), this.subject); + + private (Mock, ILoRaDeviceClient) RegisterTestDevice(TimeSpan? keepAliveTimeout = null) + { + var device = TestDevice; + device.KeepAliveTimeout = (int)(keepAliveTimeout ?? TimeSpan.Zero).TotalSeconds; + var mock = new Mock(); + this.subject.Register(device, mock.Object); + mock.Setup(x => x.EnsureConnected()).Returns(true); + return (mock, this.subject.GetClient(device)); + } + [Theory] [InlineData(0)] [InlineData(1)] [InlineData(5)] - public void When_Disposing_Should_Dispose_All_Managed_Connections(int numberOfDevices) + public async Task When_Disposing_Should_Dispose_All_Managed_Connections(int numberOfDevices) { // arrange - using var cache = new MemoryCache(new MemoryCacheOptions()); - using var connectionManager = new LoRaDeviceClientConnectionManager(cache, NullLogger.Instance); var deviceRegistrations = Enumerable.Range(0, numberOfDevices) - .Select(i => TestUtils.CreateFromSimulatedDevice(new SimulatedDevice(TestDeviceInfo.CreateABPDevice((uint)i)), connectionManager)) + .Select(i => TestUtils.CreateFromSimulatedDevice(new SimulatedDevice(TestDeviceInfo.CreateABPDevice((uint)i)), this.subject)) .Select(d => (d, new Mock())) .ToList(); foreach (var (d, c) in deviceRegistrations) { - connectionManager.Register(d, c.Object); + this.subject.Register(d, c.Object); } // act - connectionManager.Dispose(); + + await this.subject.DisposeAsync(); // assert + foreach (var (_, c) in deviceRegistrations) { - c.Verify(client => client.Dispose(), Times.Exactly(2)); + c.Verify(client => client.DisposeAsync(), Times.Exactly(1)); } } [Fact] public void When_Registering_Existing_Connection_Throws() + { + var device = TestDevice; + this.subject.Register(device, new Mock().Object); + Assert.Throws(() => this.subject.Register(device, new Mock().Object)); + } + + [Fact] + public void GetClient_Returns_Registered_Client_Of_Device() + { + var device = TestDevice; + this.subject.Register(device, new Mock().Object); + Assert.NotNull(this.subject.GetClient(device)); + } + + [Fact] + public void GetClient_Returns_Client_That_Invokes_Underlying_Client() + { + // arrange + + var (clientMock, client) = RegisterTestDevice(); + + // act + + client.GetTwinAsync(CancellationToken.None); + + var telemetry = new LoRaDeviceTelemetry(); + var properties = new Dictionary(); + client.SendEventAsync(telemetry, properties); + + var reportedProperties = new TwinCollection(); + client.UpdateReportedPropertiesAsync(reportedProperties, CancellationToken.None); + + var timeout = TimeSpan.FromSeconds(5); + client.ReceiveAsync(timeout); + + using var message = new Message(Stream.Null); + client.CompleteAsync(message); + client.AbandonAsync(message); + client.RejectAsync(message); + + client.EnsureConnected(); + + // assert + + clientMock.Verify(x => x.GetTwinAsync(CancellationToken.None), Times.Once); + clientMock.Verify(x => x.SendEventAsync(telemetry, properties), Times.Once); + clientMock.Verify(x => x.UpdateReportedPropertiesAsync(reportedProperties, CancellationToken.None), Times.Once); + clientMock.Verify(x => x.ReceiveAsync(timeout), Times.Once); + clientMock.Verify(x => x.CompleteAsync(message), Times.Once); + clientMock.Verify(x => x.AbandonAsync(message), Times.Once); + clientMock.Verify(x => x.RejectAsync(message), Times.Once); + clientMock.Verify(x => x.EnsureConnected(), Times.Exactly(8)); + } + + [Fact] + public async Task Client_DisconnectAsync_Does_Not_Invoke_EnsureConnected() + { + // arrange + + var (clientMock, client) = RegisterTestDevice(); + + // act + + await client.DisconnectAsync(CancellationToken.None); + + // assert + + clientMock.Verify(x => x.DisconnectAsync(CancellationToken.None), Times.Once); + clientMock.Verify(x => x.EnsureConnected(), Times.Never); + } + + private Mock CreateCacheEntryMock(IList? postEvictionCallbackRegistrationList = null) + { + var cacheEntryMock = new Mock(); + this.cacheMock.Setup(x => x.CreateEntry(It.IsAny())).Returns(cacheEntryMock.Object); + cacheEntryMock.SetupAllProperties(); + if (postEvictionCallbackRegistrationList is { } somePostEvictionCallbackRegistrationList) + cacheEntryMock.Setup(x => x.PostEvictionCallbacks).Returns(somePostEvictionCallbackRegistrationList); + return cacheEntryMock; + } + + [Fact] + public async Task Client_With_Zero_KeepAliveTimeout_Is_Never_Cached_For_Disconnection() + { + // arrange + + var postEvictionCallbackRegistrationList = new List(); + _ = CreateCacheEntryMock(postEvictionCallbackRegistrationList); + + var (clientMock, client) = RegisterTestDevice(keepAliveTimeout: TimeSpan.Zero); + clientMock.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(new Twin()); + + // 1st act + + await client.GetTwinAsync(CancellationToken.None); + + // assert + + clientMock.Verify(x => x.EnsureConnected(), Times.Once); + clientMock.Verify(x => x.DisconnectAsync(CancellationToken.None), Times.Never); + Assert.Empty(postEvictionCallbackRegistrationList); + } + + [Fact] + public async Task Client_With_Non_Zero_KeepAliveTimeout_Is_Disconnected_When_Evicted_From_Cache() + { + // arrange + + var postEvictionCallbackRegistrationList = new List(); + var cacheEntryMock = CreateCacheEntryMock(postEvictionCallbackRegistrationList); + + var (clientMock, client) = RegisterTestDevice(keepAliveTimeout: TimeSpan.FromSeconds(1)); + clientMock.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(new Twin()); + + // 1st act + + await client.GetTwinAsync(CancellationToken.None); + + // assert + + clientMock.Verify(x => x.EnsureConnected(), Times.Once); + var registration = Assert.Single(postEvictionCallbackRegistrationList); + + // 2nd act + + registration.EvictionCallback(null, cacheEntryMock.Object.Value, EvictionReason.Expired, registration.State); + + // assert + + clientMock.Verify(x => x.DisconnectAsync(CancellationToken.None), Times.Once); + } + + [Fact] + public async Task Client_Disconnection_Is_Deferred_When_An_Activity_Is_Outstanding() + { + // arrange + + _ = CreateCacheEntryMock(new List()); + + var (clientMock, client) = RegisterTestDevice(keepAliveTimeout: TimeSpan.FromSeconds(1)); + clientMock.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(new Twin()); + + // 1st act + + await using (TestDevice.BeginDeviceClientConnectionActivity()) + { + await client.GetTwinAsync(CancellationToken.None); + + // assert + + clientMock.Verify(x => x.EnsureConnected(), Times.Once); + + // 2nd act + + await client.DisconnectAsync(CancellationToken.None); + + // assert + + clientMock.Verify(x => x.DisconnectAsync(CancellationToken.None), Times.Never); + + // 3rd act (activity disposed) + } + + // assert + + clientMock.Verify(x => x.DisconnectAsync(CancellationToken.None), Times.Once); + } + + [Fact] + public async Task Client_Disconnection_Is_Deferred_When_Several_Activities_Are_Outstanding() + { + // arrange + + _ = CreateCacheEntryMock(new List()); + + var (clientMock, client) = RegisterTestDevice(keepAliveTimeout: TimeSpan.FromSeconds(1)); + clientMock.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(new Twin()); + + // act + + await using (TestDevice.BeginDeviceClientConnectionActivity()) + await using (TestDevice.BeginDeviceClientConnectionActivity()) + await using (TestDevice.BeginDeviceClientConnectionActivity()) + { + await client.GetTwinAsync(CancellationToken.None); + await client.DisconnectAsync(CancellationToken.None); + + // assert + + clientMock.Verify(x => x.DisconnectAsync(CancellationToken.None), Times.Never); + } + + clientMock.Verify(x => x.DisconnectAsync(CancellationToken.None), Times.Once); + } + + [Fact] + public async Task Client_Not_Disconnected_If_No_Disconnection_Deferred_During_Activities() { // arrange - using var cache = new MemoryCache(new MemoryCacheOptions()); - using var connectionManager = new LoRaDeviceClientConnectionManager(cache, NullLogger.Instance); - using var loraDevice = new LoRaDevice(null, new DevEui(0), null); - connectionManager.Register(loraDevice, new Mock().Object); - Assert.Throws(() => connectionManager.Register(loraDevice, new Mock().Object)); + _ = CreateCacheEntryMock(new List()); + + var (clientMock, client) = RegisterTestDevice(keepAliveTimeout: TimeSpan.FromSeconds(1)); + clientMock.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(new Twin()); + + // act + + await using (TestDevice.BeginDeviceClientConnectionActivity()) + await using (TestDevice.BeginDeviceClientConnectionActivity()) + await using (TestDevice.BeginDeviceClientConnectionActivity()) + { + await client.GetTwinAsync(CancellationToken.None); + } + + // assert + + clientMock.Verify(x => x.DisconnectAsync(CancellationToken.None), Times.Never); + } + + [Fact] + public async Task Client_Disconnection_Via_KeepAliveTimeout_Expiry_Is_Deferred_When_An_Activity_Is_Outstanding() + { + // arrange + + var postEvictionCallbackRegistrationList = new List(); + var cacheEntryMock = CreateCacheEntryMock(postEvictionCallbackRegistrationList); + + var (clientMock, client) = RegisterTestDevice(keepAliveTimeout: TimeSpan.FromSeconds(1)); + clientMock.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(new Twin()); + + // 1st act + + await using (TestDevice.BeginDeviceClientConnectionActivity()) + { + await client.GetTwinAsync(CancellationToken.None); + + // assert + + var registration = Assert.Single(postEvictionCallbackRegistrationList); + + // 2nd act + + registration.EvictionCallback(null, cacheEntryMock.Object.Value, EvictionReason.Expired, registration.State); + + // assert + + clientMock.Verify(x => x.EnsureConnected(), Times.Once); + clientMock.Verify(x => x.DisconnectAsync(CancellationToken.None), Times.Never); + + // 3rd act (activity disposed) + } + + // assert + + clientMock.Verify(x => x.DisconnectAsync(CancellationToken.None), Times.Once); + } + + [Fact] + public async Task Client_Operations_Are_Synchronized() + { + var (clientMock, client) = RegisterTestDevice(); + + // Monitor operations on the client. + + var queuedOperations = new List<(int Id, string Name)>(); + var completedOperations = new List<(int Id, string Name)>(); + + var eventSource = (ILoRaDeviceClientSynchronizedOperationEventSource)client; + eventSource.Queued += (_, args) => queuedOperations.Add((args.Id, args.Name)); + eventSource.Processed += (_, args) => completedOperations.Add((args.Id, args.Name)); + + // Setup the client mock. + + var tcs = new + { + GetTwinAsync = new TaskCompletionSource(), + UpdateReportedPropertiesAsync = new TaskCompletionSource(), + DisconnectAsync = new TaskCompletionSource(), + }; + + clientMock.Setup(x => x.GetTwinAsync(CancellationToken.None)).Returns(tcs.GetTwinAsync.Task); + + var reportedProperties = new TwinCollection(); + clientMock.Setup(x => x.UpdateReportedPropertiesAsync(reportedProperties, CancellationToken.None)).Returns(tcs.UpdateReportedPropertiesAsync.Task); + + clientMock.Setup(x => x.DisconnectAsync(CancellationToken.None)).Returns(tcs.DisconnectAsync.Task); + + // Issue the first operation and... + + var getTwinTask = client.GetTwinAsync(CancellationToken.None); + + // ...assert state of each operations list is the expected: + + Assert.Single(queuedOperations); + Assert.Empty(completedOperations); + + // Issue the second operations and... + + var updateReportedPropertiesTask = client.UpdateReportedPropertiesAsync(reportedProperties, CancellationToken.None); + + // ...assert state of each operations list is the expected: + + Assert.Equal(2, queuedOperations.Count); + Assert.Empty(completedOperations); + + // Issue the third operation and... + + var disconnectTask = client.DisconnectAsync(CancellationToken.None); + + // ...assert state of each operations list is the expected: + + Assert.Equal(3, queuedOperations.Count); + Assert.Empty(completedOperations); + + // Assert that the queued operations are the expected ones and in the expected order. + + var expectedQueuedOperations = new[] + { + (1, nameof(client.GetTwinAsync)), + (2, nameof(client.UpdateReportedPropertiesAsync)), + (3, nameof(client.DisconnectAsync)) + }; + Assert.Equal(expectedQueuedOperations, queuedOperations); + + // Now complete all operations. + + tcs.GetTwinAsync.SetResult(new Twin()); + tcs.UpdateReportedPropertiesAsync.SetResult(true); + tcs.DisconnectAsync.SetResult(); + + await Task.WhenAll(getTwinTask, updateReportedPropertiesTask, disconnectTask); + + // Assert the the queued operations completed in the same order. + + Assert.Equal(completedOperations, queuedOperations); } } } diff --git a/Tests/Unit/NetworkServer/LoRaDeviceClientResiliencyTests.cs b/Tests/Unit/NetworkServer/LoRaDeviceClientResiliencyTests.cs new file mode 100644 index 0000000000..94193e1649 --- /dev/null +++ b/Tests/Unit/NetworkServer/LoRaDeviceClientResiliencyTests.cs @@ -0,0 +1,383 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Unit.NetworkServer +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Common; + using LoRaWan.NetworkServer; + using Microsoft.Azure.Devices.Client; + using Microsoft.Azure.Devices.Shared; + using Microsoft.Extensions.Logging; + using Moq; + using Moq.Language; + using Xunit; + + /// + /// Tests the client returned by . + /// + public sealed class LoRaDeviceClientResiliencyTests : IAsyncDisposable + { + private readonly Mock originalMock; + private readonly Mock loggerMock = new(); + private readonly ILoRaDeviceClient subject; + private readonly CancellationTokenSource cancellationTokenSource = new(); + + private record struct LogEntry(string Message, Exception? Exception); + + public LoRaDeviceClientResiliencyTests() + { + this.originalMock = new Mock(); + var loggerFactoryMock = new Mock(); + loggerFactoryMock.Setup(x => x.CreateLogger(It.IsAny())).Returns(this.loggerMock.Object); + this.subject = this.originalMock.Object.AddResiliency(loggerFactoryMock.Object); + } + + private IEnumerable LogEntries => + from e in this.loggerMock.GetLogInvocations() + where e.LogLevel is LogLevel.Debug + select new LogEntry(e.Message, e.Exception); + + public async ValueTask DisposeAsync() + { + this.cancellationTokenSource.Dispose(); + await this.subject.DisposeAsync(); + } + + private CancellationToken CancellationToken => this.cancellationTokenSource.Token; + + [Fact] + public void AddResiliency_Returns_Same_For_Successive_Call() + { + var result = this.subject.AddResiliency(null); + + Assert.Same(this.subject, result); + } + + [Fact] + public void Client_Provides_Identity() + { + var identityProvider = Assert.IsAssignableFrom>(this.subject); + + Assert.Same(this.originalMock.Object, identityProvider.Identity); + } + + [Fact] + public void EnsureConnected_Invokes_Original_Client() + { + this.originalMock.Setup(x => x.EnsureConnected()).Returns(true); + + var result = this.subject.EnsureConnected(); + + Assert.True(result); + } + + [Theory] + [MemberData(nameof(GetRetriedExceptionsTestData))] + public void EnsureConnected_Is_Not_Resilient(Exception exception) + { + this.originalMock.Setup(x => x.EnsureConnected()).Throws(exception); + + var ex = Assert.Throws(exception.GetType(), () => _ = this.subject.EnsureConnected()); + + Assert.Same(exception, ex); + this.originalMock.Verify(x => x.EnsureConnected(), Times.Once); + } + + [Fact] + public async Task DisconnectAsync_Invokes_Original_Client() + { + await this.subject.DisconnectAsync(CancellationToken); + + this.originalMock.Verify(x => x.DisconnectAsync(CancellationToken), Times.Once); + } + + private static IEnumerable GetRetriedExceptions() => new[] + { + new InvalidOperationException("This operation is only allowed using a successfully authenticated context. " + "" + + "This sentence in the error message shouldn't matter."), + new ObjectDisposedException("") + }; + + public static TheoryData GetRetriedExceptionsTestData() => TheoryDataFactory.From(GetRetriedExceptions()); + + [Theory] + [MemberData(nameof(GetRetriedExceptionsTestData))] + public async Task DisconnectAsync_Is_Not_Resilient(Exception exception) + { + this.originalMock.Setup(x => x.DisconnectAsync(CancellationToken)).Throws(exception); + + var ex = await Assert.ThrowsAsync(exception.GetType(), () => this.subject.DisconnectAsync(CancellationToken)); + + Assert.Same(exception, ex); + this.originalMock.Verify(x => x.DisconnectAsync(CancellationToken), Times.Once); + } + + [Fact] + public async Task DisposeAsync_Invokes_Original_Client() + { + await this.subject.DisposeAsync(); + + this.originalMock.Verify(x => x.DisposeAsync(), Times.Once); + } + + [Theory] + [MemberData(nameof(GetRetriedExceptionsTestData))] + public async Task DisposeAsync_Is_Not_Resilient(Exception exception) + { + this.originalMock.Setup(x => x.DisposeAsync()).Throws(exception); + + var ex = await Assert.ThrowsAsync(exception.GetType(), () => this.subject.DisposeAsync().AsTask()); + + Assert.Same(exception, ex); + this.originalMock.Verify(x => x.DisposeAsync(), Times.Once); + } + + public interface IOperationTestHelper + { + string Name { get; } + IOperationSequentialResultSetup SetupSequence(Mock mock); + Task InvokeAsync(ILoRaDeviceClient subject); + void Verify(Mock mock, Times times); + } + + public interface IOperationSequentialResultSetup + { + IOperationSequentialResultSetup Succeed(); + IOperationSequentialResultSetup Fail(Exception exception); + } + + private abstract class OperationTestHelper : IOperationTestHelper, IOperationSequentialResultSetup, IDisposable + { + private readonly T result; + private readonly CancellationTokenSource cancellationTokenSource = new(); + private ISetupSequentialResult>? setupSequentialResult; + + protected OperationTestHelper(T result) => this.result = result; + + public virtual string Name => GetType().Name[..^"TestHelper".Length]; + + protected CancellationToken CancellationToken => this.cancellationTokenSource.Token; + + private ISetupSequentialResult> SetupSequentialResult => this.setupSequentialResult ?? throw new InvalidOperationException(); + + protected abstract ISetupSequentialResult> SetupSequenceCore(Mock mock); + + public virtual IOperationSequentialResultSetup SetupSequence(Mock mock) + { + this.setupSequentialResult = SetupSequenceCore(mock); + return this; + } + + public abstract Task InvokeAsync(ILoRaDeviceClient subject); + public abstract void Verify(Mock mock, Times times); + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + this.cancellationTokenSource.Dispose(); + if (this.result is IDisposable disposable) + disposable.Dispose(); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + IOperationSequentialResultSetup IOperationSequentialResultSetup.Succeed() + { + SetupSequentialResult.ReturnsAsync(this.result); + return this; + } + + IOperationSequentialResultSetup IOperationSequentialResultSetup.Fail(Exception exception) + { + SetupSequentialResult.Throws(exception); + return this; + } + } + + private sealed class GetTwinAsyncTestHelper : OperationTestHelper + { + public GetTwinAsyncTestHelper(Twin result) : base(result) { } + + protected override ISetupSequentialResult> SetupSequenceCore(Mock mock) => mock.SetupSequence(x => x.GetTwinAsync(CancellationToken)); + public override Task InvokeAsync(ILoRaDeviceClient subject) => subject.GetTwinAsync(CancellationToken); + public override void Verify(Mock mock, Times times) => mock.Verify(x => x.GetTwinAsync(CancellationToken), times); + } + + private sealed class SendEventAsyncTestHelper : OperationTestHelper + { + private readonly LoRaDeviceTelemetry telemetry; + private readonly Dictionary properties; + + public SendEventAsyncTestHelper(LoRaDeviceTelemetry telemetry, Dictionary properties, bool result) : base(result) + { + this.telemetry = telemetry; + this.properties = properties; + } + + protected override ISetupSequentialResult> SetupSequenceCore(Mock mock) => mock.SetupSequence(x => x.SendEventAsync(this.telemetry, this.properties)); + public override Task InvokeAsync(ILoRaDeviceClient subject) => subject.SendEventAsync(this.telemetry, this.properties); + public override void Verify(Mock mock, Times times) => mock.Verify(x => x.SendEventAsync(this.telemetry, this.properties), times); + } + + private sealed class UpdateReportedPropertiesAsyncTestHelper : OperationTestHelper + { + private readonly TwinCollection twinCollection; + + public UpdateReportedPropertiesAsyncTestHelper(TwinCollection twinCollection, bool result) : base(result) => + this.twinCollection = twinCollection; + + protected override ISetupSequentialResult> SetupSequenceCore(Mock mock) => mock.SetupSequence(x => x.UpdateReportedPropertiesAsync(this.twinCollection, CancellationToken)); + public override Task InvokeAsync(ILoRaDeviceClient subject) => subject.UpdateReportedPropertiesAsync(this.twinCollection, CancellationToken); + public override void Verify(Mock mock, Times times) => mock.Verify(x => x.UpdateReportedPropertiesAsync(this.twinCollection, CancellationToken), times); + } + + private sealed class ReceiveAsyncTestHelper : OperationTestHelper + { + private readonly TimeSpan timeout; + + public ReceiveAsyncTestHelper(TimeSpan timeout, Message result) : base(result) => + this.timeout = timeout; + + protected override ISetupSequentialResult> SetupSequenceCore(Mock mock) => mock.SetupSequence(x => x.ReceiveAsync(this.timeout)); + public override Task InvokeAsync(ILoRaDeviceClient subject) => subject.ReceiveAsync(this.timeout); + public override void Verify(Mock mock, Times times) => mock.Verify(x => x.ReceiveAsync(this.timeout), times); + } + + private sealed class CompleteAsyncTestHelper : OperationTestHelper + { + private readonly Message message; + + public CompleteAsyncTestHelper(Message message, bool result) : base(result) => this.message = message; + + protected override ISetupSequentialResult> SetupSequenceCore(Mock mock) => mock.SetupSequence(x => x.CompleteAsync(this.message)); + public override Task InvokeAsync(ILoRaDeviceClient subject) => subject.CompleteAsync(this.message); + public override void Verify(Mock mock, Times times) => mock.Verify(x => x.CompleteAsync(this.message), times); + } + + private sealed class AbandonAsyncTestHelper : OperationTestHelper + { + private readonly Message message; + + public AbandonAsyncTestHelper(Message message, bool result) : base(result) => this.message = message; + + protected override ISetupSequentialResult> SetupSequenceCore(Mock mock) => mock.SetupSequence(x => x.AbandonAsync(this.message)); + public override Task InvokeAsync(ILoRaDeviceClient subject) => subject.AbandonAsync(this.message); + public override void Verify(Mock mock, Times times) => mock.Verify(x => x.AbandonAsync(this.message), times); + } + + private sealed class RejectAsyncTestHelper : OperationTestHelper + { + private readonly Message message; + + public RejectAsyncTestHelper(Message message, bool result) : base(result) => this.message = message; + + protected override ISetupSequentialResult> SetupSequenceCore(Mock mock) => mock.SetupSequence(x => x.RejectAsync(this.message)); + public override Task InvokeAsync(ILoRaDeviceClient subject) => subject.RejectAsync(this.message); + public override void Verify(Mock mock, Times times) => mock.Verify(x => x.RejectAsync(this.message), times); + } + + private static IEnumerable GetOperationTestHelpers() + { + using var message = new Message(); + yield return new GetTwinAsyncTestHelper(new Twin()); + yield return new SendEventAsyncTestHelper(new LoRaDeviceTelemetry(), new Dictionary(), true); + yield return new UpdateReportedPropertiesAsyncTestHelper(new TwinCollection(), true); + yield return new ReceiveAsyncTestHelper(TimeSpan.FromSeconds(5), message); + yield return new CompleteAsyncTestHelper(message, true); + yield return new AbandonAsyncTestHelper(message, true); + yield return new RejectAsyncTestHelper(message, true); + } + + public static TheoryData GetOperationsTestData() => + TheoryDataFactory.From(GetOperationTestHelpers()); + + public static TheoryData GetResiliencyTestData() => + TheoryDataFactory.From(from tc in GetOperationTestHelpers() + from re in GetRetriedExceptions() + select (tc, re)); + + [Theory] + [MemberData(nameof(GetOperationsTestData))] + public async Task Operation_Is_Not_Retried_When_Successful(IOperationTestHelper operation) + { + operation.SetupSequence(this.originalMock).Succeed(); + + await operation.InvokeAsync(this.subject); + + operation.Verify(this.originalMock, Times.Exactly(1)); + this.originalMock.Verify(x => x.EnsureConnected(), Times.Exactly(1)); + this.originalMock.Verify(x => x.DisconnectAsync(It.IsAny()), Times.Never); + Assert.Empty(LogEntries); + } + + [Theory] + [MemberData(nameof(GetResiliencyTestData))] + public async Task Operation_Is_Retried_On_Expected_Errors_Until_Retry_Limit_Is_Exhausted(IOperationTestHelper operation, Exception exception) + { + operation.SetupSequence(this.originalMock) + .Fail(exception) + .Fail(exception) + .Fail(exception); + + var ex = await Assert.ThrowsAsync(exception.GetType(), () => operation.InvokeAsync(this.subject)); + + Assert.Same(exception, ex); + operation.Verify(this.originalMock, Times.Exactly(3)); + this.originalMock.Verify(x => x.EnsureConnected(), Times.Exactly(3)); + this.originalMock.Verify(x => x.DisconnectAsync(CancellationToken.None), Times.Exactly(3)); + Assert.Equal(CreateExpectedLog(3, operation.Name, exception), LogEntries); + } + + [Theory] + [MemberData(nameof(GetOperationsTestData))] + public async Task Operation_Is_Not_Retried_On_Unexpected_Errors(IOperationTestHelper operation) + { + var exception = new InvalidOperationException(); + operation.SetupSequence(this.originalMock).Fail(exception); + + var ex = await Assert.ThrowsAsync(() => operation.InvokeAsync(this.subject)); + + Assert.Same(exception, ex); + operation.Verify(this.originalMock, Times.Exactly(1)); + this.originalMock.Verify(x => x.EnsureConnected(), Times.Exactly(1)); + this.originalMock.Verify(x => x.DisconnectAsync(It.IsAny()), Times.Never); + Assert.Empty(LogEntries); + } + + [Theory] + [MemberData(nameof(GetResiliencyTestData))] + public async Task Operation_Succeeds_Eventually_If_Retried_Within_Retry_Limit(IOperationTestHelper operation, Exception exception) + { + operation.SetupSequence(this.originalMock) + .Fail(exception) + .Fail(exception) + .Succeed(); + + await operation.InvokeAsync(this.subject); + + operation.Verify(this.originalMock, Times.Exactly(3)); + this.originalMock.Verify(x => x.EnsureConnected(), Times.Exactly(3)); + this.originalMock.Verify(x => x.DisconnectAsync(CancellationToken.None), Times.Exactly(2)); + Assert.Equal(CreateExpectedLog(2, operation.Name, exception), LogEntries); + } + + private static IEnumerable CreateExpectedLog(int count, string name, Exception exception) => + from a in Enumerable.Range(1, count) + select a.ToString(CultureInfo.InvariantCulture) into a + select new LogEntry($@"Device client operation ""{name}"" (attempt {a}/3) failed due to error: {exception.Message}", exception); + } +} diff --git a/Tests/Unit/NetworkServer/LoRaDeviceFactoryTest.cs b/Tests/Unit/NetworkServer/LoRaDeviceFactoryTest.cs index 6f14a59272..d08a6010a1 100644 --- a/Tests/Unit/NetworkServer/LoRaDeviceFactoryTest.cs +++ b/Tests/Unit/NetworkServer/LoRaDeviceFactoryTest.cs @@ -22,76 +22,78 @@ public sealed class LoRaDeviceFactoryTest public void Throws_When_Missing_DeviceInfo() { var factory = new TestDeviceFactory(); - var deviceInfo = DefaultDeviceInfo; + var deviceInfo = defaultDeviceInfo; deviceInfo.PrimaryKey = null; Assert.ThrowsAsync(() => factory.CreateAndRegisterAsync(deviceInfo, CancellationToken.None)); - deviceInfo = DefaultDeviceInfo; + deviceInfo = defaultDeviceInfo; deviceInfo.DevEUI = new DevEui(0); Assert.ThrowsAsync(() => factory.CreateAndRegisterAsync(deviceInfo, this.cancellationToken)); } [Fact] - public void Throws_When_Device_Is_Already_Registered() + public async Task Throws_When_Device_Is_Already_Registered() { - var deviceInfo = DefaultDeviceInfo; - using var cache = CreateDefaultCache(); + var deviceInfo = defaultDeviceInfo; + await using var cache = CreateDefaultCache(); var factory = new TestDeviceFactory(loRaDeviceCache: cache); - using var device = new LoRaDevice(deviceInfo.DevAddr, deviceInfo.DevEUI, null); + await using var device = new LoRaDevice(deviceInfo.DevAddr, deviceInfo.DevEUI, null); cache.Register(device); - Assert.ThrowsAsync(() => factory.CreateAndRegisterAsync(deviceInfo, this.cancellationToken)); + await Assert.ThrowsAsync(() => factory.CreateAndRegisterAsync(deviceInfo, this.cancellationToken)); } [Fact] public async Task When_Created_Successfully_It_Is_Cached() { var connectionManager = new Mock(); - using var cache = CreateDefaultCache(); - var factory = new TestDeviceFactory(DefaultConfiguration, connectionManager.Object, cache, meter: TestMeter.Instance); + await using var cache = CreateDefaultCache(); + var factory = new TestDeviceFactory(defaultConfiguration, connectionManager.Object, cache, meter: TestMeter.Instance); - var device = await factory.CreateAndRegisterAsync(DefaultDeviceInfo, this.cancellationToken); + var device = await factory.CreateAndRegisterAsync(defaultDeviceInfo, this.cancellationToken); - Assert.True(cache.TryGetByDevEui(this.DefaultDeviceInfo.DevEUI, out var cachedDevice)); + Assert.True(cache.TryGetByDevEui(this.defaultDeviceInfo.DevEUI, out var cachedDevice)); Assert.Equal(device, cachedDevice); - factory.LastDeviceMock.Verify(x => x.InitializeAsync(DefaultConfiguration, this.cancellationToken), Times.Once); + factory.LastDeviceMock.Verify(x => x.InitializeAsync(defaultConfiguration, this.cancellationToken), Times.Once); connectionManager.VerifySuccess(device); + factory.LastDeviceClientMock.Verify(x => x.DisposeAsync(), Times.Never()); } [Fact] public async Task When_Created_But_Not_Our_Device_It_Is_Not_Initialized_But_Connection_Is_Registered() { var connectionManager = new Mock(); - using var cache = CreateDefaultCache(); - var factory = new TestDeviceFactory(DefaultConfiguration, connectionManager.Object, cache, x => x.Object.GatewayID = "OtherGw", TestMeter.Instance); + await using var cache = CreateDefaultCache(); + var factory = new TestDeviceFactory(defaultConfiguration, connectionManager.Object, cache, x => x.Object.GatewayID = "OtherGw", TestMeter.Instance); - var device = await factory.CreateAndRegisterAsync(DefaultDeviceInfo, this.cancellationToken); + var device = await factory.CreateAndRegisterAsync(defaultDeviceInfo, this.cancellationToken); - factory.LastDeviceMock.Verify(x => x.InitializeAsync(DefaultConfiguration, this.cancellationToken), Times.Never); + factory.LastDeviceMock.Verify(x => x.InitializeAsync(defaultConfiguration, this.cancellationToken), Times.Never); connectionManager.VerifySuccess(device); + factory.LastDeviceClientMock.Verify(x => x.DisposeAsync(), Times.Never()); } [Fact] public async Task When_Init_Fails_Cleaned_Up() { var connectionManager = new Mock(); - using var cache = CreateDefaultCache(); - var factory = new TestDeviceFactory(DefaultConfiguration, connectionManager.Object, cache, x => x.Setup(y => y.InitializeAsync(DefaultConfiguration, this.cancellationToken)).ReturnsAsync(false), TestMeter.Instance); - await Assert.ThrowsAsync(() => factory.CreateAndRegisterAsync(DefaultDeviceInfo, this.cancellationToken)); + await using var cache = CreateDefaultCache(); + var factory = new TestDeviceFactory(defaultConfiguration, connectionManager.Object, cache, x => x.Setup(y => y.InitializeAsync(defaultConfiguration, this.cancellationToken)).ReturnsAsync(false), TestMeter.Instance); + await Assert.ThrowsAsync(() => factory.CreateAndRegisterAsync(defaultDeviceInfo, this.cancellationToken)); - Assert.False(cache.TryGetByDevEui(this.DefaultDeviceInfo.DevEUI, out _)); - connectionManager.VerifyFailure(factory.LastDeviceMock.Object); - factory.LastDeviceMock.Protected().Verify(nameof(LoRaDevice.Dispose), Times.Once(), true, true); + Assert.False(cache.TryGetByDevEui(this.defaultDeviceInfo.DevEUI, out _)); + factory.LastDeviceClientMock.Verify(x => x.DisposeAsync(), Times.Once()); + factory.LastDeviceMock.Protected().Verify("DisposeAsyncCore", Times.Once()); } - private readonly NetworkServerConfiguration DefaultConfiguration = new NetworkServerConfiguration { EnableGateway = true, IoTHubHostName = "TestHub", GatewayHostName = "testGw" }; + private readonly NetworkServerConfiguration defaultConfiguration = new NetworkServerConfiguration { EnableGateway = true, IoTHubHostName = "TestHub", GatewayHostName = "testGw" }; private static LoRaDeviceCache CreateDefaultCache() => LoRaDeviceCacheDefault.CreateDefault(); - private readonly IoTHubDeviceInfo DefaultDeviceInfo = new IoTHubDeviceInfo + private readonly IoTHubDeviceInfo defaultDeviceInfo = new IoTHubDeviceInfo { DevEUI = new DevEui(1), PrimaryKey = "AAAA", @@ -113,13 +115,16 @@ private class TestDeviceFactory : LoRaDeviceFactory loRaDeviceCache, NullLoggerFactory.Instance, NullLogger.Instance, - meter) + meter, + new NoopTracing()) { this.deviceSetup = deviceSetup; } internal Mock LastDeviceMock { get; private set; } + internal Mock LastDeviceClientMock { get; private set; } + protected override LoRaDevice CreateDevice(IoTHubDeviceInfo deviceInfo) { var connectionManager = new Mock(); @@ -135,6 +140,12 @@ protected override LoRaDevice CreateDevice(IoTHubDeviceInfo deviceInfo) LastDeviceMock = device; return device.Object; } + + public override ILoRaDeviceClient CreateDeviceClient(string deviceId, string primaryKey) + { + LastDeviceClientMock = new Mock(); + return LastDeviceClientMock.Object; + } } } @@ -143,13 +154,13 @@ internal static class TestExtensions internal static void VerifySuccess(this Mock connectionManager, LoRaDevice device) { connectionManager.Verify(x => x.Register(device, It.IsAny()), Times.Once); - connectionManager.Verify(x => x.Release(device), Times.Never); + connectionManager.Verify(x => x.ReleaseAsync(device), Times.Never); } internal static void VerifyFailure(this Mock connectionManager, LoRaDevice device) { connectionManager.Verify(x => x.Register(device, It.IsAny()), Times.Once); - connectionManager.Verify(x => x.Release(device), Times.Once); + connectionManager.Verify(x => x.ReleaseAsync(device), Times.Once); } } } diff --git a/Tests/Unit/NetworkServer/LoRaDeviceRegistryTest.cs b/Tests/Unit/NetworkServer/LoRaDeviceRegistryTest.cs index 580eef6479..4fb3753276 100644 --- a/Tests/Unit/NetworkServer/LoRaDeviceRegistryTest.cs +++ b/Tests/Unit/NetworkServer/LoRaDeviceRegistryTest.cs @@ -36,7 +36,7 @@ public async Task GetDeviceForJoinRequestAsync_When_Device_Api_Throws_Error_Shou var apiService = new Mock(); apiService.Setup(x => x.SearchAndLockForJoinAsync(ServerConfiguration.GatewayID, devEui, devNonce)) .Throws(new InvalidOperationException()); - using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, this.loraDeviceFactoryMock.Object, DeviceCache); + await using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, this.loraDeviceFactoryMock.Object, DeviceCache); Task Act() => target.GetDeviceForJoinRequestAsync(devEui, devNonce); _ = await Assert.ThrowsAsync(Act); @@ -45,15 +45,12 @@ public async Task GetDeviceForJoinRequestAsync_When_Device_Api_Throws_Error_Shou apiService.VerifyAll(); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task GetDeviceForJoinRequestAsync_When_Join_Handled_By_Other_Cache_Is_Updated(bool joinedDevice) + [Fact] + public async Task GetDeviceForJoinRequestAsync_When_Join_Handled_By_Other_Cache_Is_Cleared() { var devNonce = new DevNonce(1); var apiService = new Mock(); var otaaDevice = TestDeviceInfo.CreateOTAADevice(1); - if (joinedDevice) otaaDevice.AppSKey = new AppSessionKey(); var simulatedDevice = new SimulatedDevice(otaaDevice); @@ -61,10 +58,10 @@ public async Task GetDeviceForJoinRequestAsync_When_Join_Handled_By_Other_Cache_ .ReturnsAsync(new SearchDevicesResult() { IsDevNonceAlreadyUsed = true }); DeviceCache.Register(CreateLoRaDevice(simulatedDevice)); - using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, this.loraDeviceFactoryMock.Object, DeviceCache); + await using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, this.loraDeviceFactoryMock.Object, DeviceCache); Assert.Null(await target.GetDeviceForJoinRequestAsync(simulatedDevice.DevEUI, devNonce)); - Assert.Equal(joinedDevice, !DeviceCache.TryGetByDevEui(simulatedDevice.DevEUI, out _)); + Assert.False(DeviceCache.TryGetByDevEui(simulatedDevice.DevEUI, out _)); } [Theory] @@ -82,7 +79,7 @@ public async Task When_Device_Is_Not_In_Cache_And_Found_In_Api_Should_Cache_And_ // device will be initialized LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(simulatedDevice.CreateABPTwin()); + .ReturnsAsync(simulatedDevice.GetDefaultAbpTwin()); using var request = WaitableLoRaRequest.Create(payload); var requestHandler = new Mock(MockBehavior.Strict); @@ -91,7 +88,7 @@ public async Task When_Device_Is_Not_In_Cache_And_Found_In_Api_Should_Cache_And_ var deviceFactory = new TestLoRaDeviceFactory(LoRaDeviceClient.Object, requestHandler.Object, DeviceCache, ConnectionManager); - using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, deviceFactory, DeviceCache); + await using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, deviceFactory, DeviceCache); target.GetLoRaRequestQueue(request).Queue(request); Assert.True(await request.WaitCompleteAsync()); @@ -112,7 +109,7 @@ public async Task When_Device_Is_Not_In_Cache_And_Found_In_Api_Should_Cache_And_ [InlineData(null)] public async Task When_ABP_Device_Is_Created_Should_Call_Initializers(string deviceGatewayID) { - LoRaDeviceClient.Setup(ldc => ldc.Dispose()); + LoRaDeviceClient.Setup(ldc => ldc.DisposeAsync()); var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, gatewayID: deviceGatewayID)); var payload = simulatedDevice.CreateUnconfirmedDataUpMessage("1234"); @@ -121,16 +118,16 @@ public async Task When_ABP_Device_Is_Created_Should_Call_Initializers(string dev apiService.Setup(x => x.SearchByDevAddrAsync(It.IsAny())) .ReturnsAsync(new SearchDevicesResult(iotHubDeviceInfo.AsList())); - using var connectionManager = new SingleDeviceConnectionManager(LoRaDeviceClient.Object); - using var createdLoraDevice = TestUtils.CreateFromSimulatedDevice(simulatedDevice, connectionManager); + await using var connectionManager = new SingleDeviceConnectionManager(LoRaDeviceClient.Object); + await using var createdLoraDevice = TestUtils.CreateFromSimulatedDevice(simulatedDevice, connectionManager); this.loraDeviceFactoryMock.Setup(x => x.CreateAndRegisterAsync(iotHubDeviceInfo, It.IsAny())) .ReturnsAsync(createdLoraDevice); // device will be initialized LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(simulatedDevice.CreateABPTwin()); + .ReturnsAsync(simulatedDevice.GetDefaultAbpTwin()); - using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, this.loraDeviceFactoryMock.Object, DeviceCache); + await using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, this.loraDeviceFactoryMock.Object, DeviceCache); var initializer = new Mock(); initializer.Setup(x => x.Initialize(createdLoraDevice)); @@ -155,7 +152,7 @@ public async Task When_ABP_Device_Is_Created_Should_Call_Initializers(string dev public async Task When_Devices_From_Another_Gateway_Is_Cached_Return_Null() { var simulatedDevice1 = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, gatewayID: "another-gateway")); - using var loraDevice1 = TestUtils.CreateFromSimulatedDevice(simulatedDevice1, ConnectionManager); + await using var loraDevice1 = TestUtils.CreateFromSimulatedDevice(simulatedDevice1, ConnectionManager); loraDevice1.IsOurDevice = false; DeviceCache.Register(loraDevice1); @@ -164,7 +161,7 @@ public async Task When_Devices_From_Another_Gateway_Is_Cached_Return_Null() var apiService = new Mock(MockBehavior.Strict); - using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, this.loraDeviceFactoryMock.Object, DeviceCache); + await using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, this.loraDeviceFactoryMock.Object, DeviceCache); using var request = WaitableLoRaRequest.Create(payload); var queue = target.GetLoRaRequestQueue(request); queue.Queue(request); @@ -188,12 +185,12 @@ public async Task When_Multiple_Devices_With_Same_DevAddr_Are_Cached_Should_Find var connectionManager = new Mock(); var simulatedDevice1 = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, gatewayID: deviceGatewayID)); - using var loraDevice1 = TestUtils.CreateFromSimulatedDevice(simulatedDevice1, connectionManager.Object); + await using var loraDevice1 = TestUtils.CreateFromSimulatedDevice(simulatedDevice1, connectionManager.Object); var simulatedDevice2 = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, gatewayID: ServerConfiguration.GatewayID)); simulatedDevice2.LoRaDevice.DeviceID = new DevEui(2).ToString(); simulatedDevice2.LoRaDevice.NwkSKey = TestKeys.CreateNetworkSessionKey(0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF); - using var loraDevice2 = TestUtils.CreateFromSimulatedDevice(simulatedDevice2, connectionManager.Object); + await using var loraDevice2 = TestUtils.CreateFromSimulatedDevice(simulatedDevice2, connectionManager.Object); DeviceCache.Register(loraDevice1); DeviceCache.Register(loraDevice2); @@ -208,7 +205,7 @@ public async Task When_Multiple_Devices_With_Same_DevAddr_Are_Cached_Should_Find .ReturnsAsync(new LoRaDeviceRequestProcessResult(loraDevice1, request)); loraDevice1.SetRequestHandler(requestHandler.Object); - using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, this.loraDeviceFactoryMock.Object, DeviceCache); + await using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, this.loraDeviceFactoryMock.Object, DeviceCache); target.GetLoRaRequestQueue(request).Queue(request); Assert.True(await request.WaitCompleteAsync()); Assert.True(request.ProcessingSucceeded); @@ -233,10 +230,10 @@ public async Task When_Queueing_To_Multiple_Devices_With_Same_DevAddr_Should_Que var loRaDeviceClient1 = new Mock(MockBehavior.Loose); loRaDeviceClient1.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(simulatedDevice1.CreateABPTwin()); + .ReturnsAsync(simulatedDevice1.GetDefaultAbpTwin()); - using var connectionManager1 = new SingleDeviceConnectionManager(loRaDeviceClient1.Object); - using var loraDevice1 = TestUtils.CreateFromSimulatedDevice(simulatedDevice1, connectionManager1); + await using var connectionManager1 = new SingleDeviceConnectionManager(loRaDeviceClient1.Object); + await using var loraDevice1 = TestUtils.CreateFromSimulatedDevice(simulatedDevice1, connectionManager1); var devAddr = loraDevice1.DevAddr.Value; var reqHandler1 = new Mock(MockBehavior.Strict); @@ -249,9 +246,9 @@ public async Task When_Queueing_To_Multiple_Devices_With_Same_DevAddr_Should_Que simulatedDevice2.LoRaDevice.NwkSKey = TestKeys.CreateNetworkSessionKey(0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF); var loRaDeviceClient2 = new Mock(MockBehavior.Loose); loRaDeviceClient2.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(simulatedDevice2.CreateABPTwin()); - using var connectionManager2 = new SingleDeviceConnectionManager(loRaDeviceClient2.Object); - using var loraDevice2 = TestUtils.CreateFromSimulatedDevice(simulatedDevice2, connectionManager2); + .ReturnsAsync(simulatedDevice2.GetDefaultAbpTwin()); + await using var connectionManager2 = new SingleDeviceConnectionManager(loRaDeviceClient2.Object); + await using var loraDevice2 = TestUtils.CreateFromSimulatedDevice(simulatedDevice2, connectionManager2); // Api service: search devices async var iotHubDeviceInfo1 = new IoTHubDeviceInfo(devAddr, loraDevice1.DevEUI, string.Empty); @@ -274,7 +271,7 @@ public async Task When_Queueing_To_Multiple_Devices_With_Same_DevAddr_Should_Que return loraDevice2; }); - using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, this.loraDeviceFactoryMock.Object, DeviceCache); + await using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, this.loraDeviceFactoryMock.Object, DeviceCache); using var request = WaitableLoRaRequest.Create(payload); target.GetLoRaRequestQueue(request).Queue(request); Assert.True(await request.WaitCompleteAsync()); @@ -318,7 +315,7 @@ public async Task When_Device_Is_Assigned_To_Another_Gateway_Cache_Locally_And_R var deviceFactory = new TestLoRaDeviceFactory(LoRaDeviceClient.Object, DeviceCache, ConnectionManager); - using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, deviceFactory, DeviceCache); + await using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, deviceFactory, DeviceCache); // request #1 var payload1 = simulatedDevice.CreateUnconfirmedDataUpMessage("1", fcnt: 11); @@ -343,7 +340,7 @@ public async Task When_Device_Is_Assigned_To_Another_Gateway_Cache_Locally_And_R // Device should not be connected LoRaDeviceClient.VerifyAll(); LoRaDeviceClient.Verify(x => x.GetTwinAsync(CancellationToken.None), Times.Never()); - LoRaDeviceClient.Verify(x => x.Disconnect(), Times.Never()); + LoRaDeviceClient.Verify(x => x.DisconnectAsync(CancellationToken.None), Times.Once()); // device is in cache Assert.True(DeviceCache.TryGetForPayload(request1.Payload, out var loRaDevice)); @@ -363,7 +360,7 @@ public async Task When_Device_Is_Assigned_To_Another_Gateway_After_No_Connection var deviceFactory = new TestLoRaDeviceFactory(LoRaDeviceClient.Object, DeviceCache, ConnectionManager); - using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, deviceFactory, DeviceCache); + await using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, deviceFactory, DeviceCache); // setup 2 requests - ensure the cache is validated before fetching from the function var requests = Enumerable.Range(1, 2).Select((n) => @@ -396,23 +393,21 @@ public async Task When_Device_Is_Assigned_To_Another_Gateway_After_No_Connection [Theory] [InlineData(ServerGatewayID)] [InlineData(null)] - public void When_Cache_Clear_Is_Called_Should_Removed_Cached_Devices(string deviceGatewayID) + public async Task When_Cache_Clear_Is_Called_Should_Removed_Cached_Devices(string deviceGatewayID) { - LoRaDeviceClient.Setup(ldc => ldc.Dispose()); + LoRaDeviceClient.Setup(ldc => ldc.DisposeAsync()); const int deviceCount = 10; var deviceList = new HashSet(); var apiService = new Mock(); var deviceFactory = new TestLoRaDeviceFactory(LoRaDeviceClient.Object, DeviceCache); - using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, deviceFactory, DeviceCache); - using var connectionManager = new SingleDeviceConnectionManager(LoRaDeviceClient.Object); + await using var target = new LoRaDeviceRegistry(ServerConfiguration, this.cache, apiService.Object, deviceFactory, DeviceCache); + await using var connectionManager = new SingleDeviceConnectionManager(LoRaDeviceClient.Object); for (var deviceID = 1; deviceID <= deviceCount; ++deviceID) { var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice((uint)deviceID, gatewayID: deviceGatewayID)); -#pragma warning disable CA2000 // Dispose objects before losing scope - transfer ownership var device = TestUtils.CreateFromSimulatedDevice(simulatedDevice, connectionManager); -#pragma warning restore CA2000 // Dispose objects before losing scope DeviceCache.Register(device); deviceList.Add(device); } @@ -428,7 +423,7 @@ public void When_Cache_Clear_Is_Called_Should_Removed_Cached_Devices(string devi // ensure all devices are in cache Assert.Equal(deviceCount, deviceList.Count(x => DeviceCache.TryGetByDevEui(x.DevEUI, out _))); - target.ResetDeviceCache(); + await target.ResetDeviceCacheAsync(); Assert.False(deviceList.Any(x => DeviceCache.TryGetByDevEui(x.DevEUI, out _)), "Should not find devices again"); } @@ -443,12 +438,12 @@ public async Task When_Loading_Device_By_DevAddr_Should_Be_Able_To_Load_By_DevEU var deviceClient = new Mock(MockBehavior.Loose); deviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(simDevice.CreateABPTwin()); + .ReturnsAsync(simDevice.GetDefaultAbpTwin()); var handlerImplementation = new Mock(MockBehavior.Strict); var deviceFactory = new TestLoRaDeviceFactory(deviceClient.Object, handlerImplementation.Object, DeviceCache, ConnectionManager); - using var deviceRegistry = new LoRaDeviceRegistry( + await using var deviceRegistry = new LoRaDeviceRegistry( ServerConfiguration, this.cache, deviceApi.Object, @@ -480,7 +475,7 @@ public async Task When_Loading_Device_By_DevEUI_Should_Be_Able_To_Load_By_DevAdd var deviceClient = new Mock(MockBehavior.Loose); deviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(simDevice.CreateABPTwin()); + .ReturnsAsync(simDevice.GetDefaultAbpTwin()); var handlerImplementation = new Mock(MockBehavior.Strict); handlerImplementation.Setup(x => x.ProcessRequestAsync(It.IsNotNull(), It.IsNotNull())) @@ -491,7 +486,7 @@ public async Task When_Loading_Device_By_DevEUI_Should_Be_Able_To_Load_By_DevAdd var deviceFactory = new TestLoRaDeviceFactory(deviceClient.Object, handlerImplementation.Object, DeviceCache, ConnectionManager); - using var deviceRegistry = new LoRaDeviceRegistry( + await using var deviceRegistry = new LoRaDeviceRegistry( ServerConfiguration, this.cache, deviceApi.Object, @@ -522,7 +517,7 @@ public async Task GetDeviceByDevEUIAsync_When_Api_Returns_Null_Should_Return_Nul var deviceFactory = new TestLoRaDeviceFactory(deviceClient.Object, DeviceCache); - using var deviceRegistry = new LoRaDeviceRegistry( + await using var deviceRegistry = new LoRaDeviceRegistry( ServerConfiguration, this.cache, deviceApi.Object, @@ -547,7 +542,7 @@ public async Task GetDeviceByDevEUIAsync_When_Api_Returns_Empty_Should_Return_Nu var deviceFactory = new TestLoRaDeviceFactory(deviceClient.Object, DeviceCache); - using var deviceRegistry = new LoRaDeviceRegistry( + await using var deviceRegistry = new LoRaDeviceRegistry( ServerConfiguration, this.cache, deviceApi.Object, diff --git a/Tests/Unit/NetworkServer/LoRaDeviceTest.cs b/Tests/Unit/NetworkServer/LoRaDeviceTest.cs index 668d8e04ad..ae726a0c65 100644 --- a/Tests/Unit/NetworkServer/LoRaDeviceTest.cs +++ b/Tests/Unit/NetworkServer/LoRaDeviceTest.cs @@ -5,6 +5,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer { using System; using System.Collections.Generic; + using System.Security.Cryptography; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -14,7 +15,6 @@ namespace LoRaWan.Tests.Unit.NetworkServer using LoRaWan.Tests.Common; using Microsoft.Azure.Devices.Client.Exceptions; using Microsoft.Azure.Devices.Shared; - using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; @@ -28,27 +28,72 @@ namespace LoRaWan.Tests.Unit.NetworkServer /// public class LoRaDeviceTest { + private static readonly NetworkServerConfiguration Configuration = new() { GatewayID = "test-gateway" }; + private static readonly LoRaDesiredTwinProperties OtaaDesiredTwinProperties = new() + { + JoinEui = new JoinEui((ulong)RandomNumberGenerator.GetInt32(0, int.MaxValue)), + AppKey = TestKeys.CreateAppKey(), + GatewayId = Configuration.GatewayID, + SensorDecoder = "DecoderValueSensor", + Version = 1, + }; + private static readonly LoRaReportedTwinProperties OtaaReportedTwinProperties = new() + { + Version = 1, + NetworkSessionKey = TestKeys.CreateNetworkSessionKey(), + AppSessionKey = TestKeys.CreateAppSessionKey(), + DevNonce = new DevNonce((ushort)RandomNumberGenerator.GetInt32(0, ushort.MaxValue)), + DevAddr = new DevAddr((uint)RandomNumberGenerator.GetInt32(0, int.MaxValue)), + }; + private static readonly LoRaDesiredTwinProperties AbpDesiredTwinProperties = new() + { + NetworkSessionKey = TestKeys.CreateNetworkSessionKey(), + AppSessionKey = TestKeys.CreateAppSessionKey(), + DevAddr = new DevAddr((uint)RandomNumberGenerator.GetInt32(0, int.MaxValue)), + GatewayId = Configuration.GatewayID, + SensorDecoder = "DecoderValueSensor", + Version = 1, + }; + private static readonly LoRaReportedTwinProperties AbpReportedTwinProperties = new() + { + Version = 1, + NetworkSessionKey = TestKeys.CreateNetworkSessionKey(), + AppSessionKey = TestKeys.CreateAppSessionKey(), + DevAddr = AbpDesiredTwinProperties.DevAddr, + }; + private readonly Mock loRaDeviceClient; - private readonly NetworkServerConfiguration configuration; public LoRaDeviceTest() { this.loRaDeviceClient = new Mock(MockBehavior.Strict); - this.loRaDeviceClient.Setup(ldc => ldc.Dispose()); - this.configuration = new NetworkServerConfiguration { GatewayID = "test-gateway" }; + this.loRaDeviceClient.Setup(ldc => ldc.DisposeAsync()).Returns(ValueTask.CompletedTask); + } + + [Fact] + public async Task When_Disposing_Device_ConnectionManager_Should_Release_It() + { + var connectionManager = new Mock(); + var target = CreateDefaultDevice(connectionManager.Object); + + // act + await target.DisposeAsync(); + + // assert + connectionManager.Verify(x => x.ReleaseAsync(target), Times.Once()); } [Fact] public async Task When_No_Changes_Were_Made_Should_Not_Save_Frame_Counter() { - using var target = CreateDefaultDevice(); + await using var target = CreateDefaultDevice(); await target.SaveChangesAsync(); } [Fact] public async Task When_Incrementing_FcntDown_Should_Save_Frame_Counter() { - using var target = CreateDefaultDevice(); + await using var target = CreateDefaultDevice(); this.loRaDeviceClient.Setup(x => x.UpdateReportedPropertiesAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(true); @@ -62,7 +107,7 @@ public async Task When_Incrementing_FcntDown_Should_Save_Frame_Counter() [Fact] public async Task When_Setting_FcntDown_Should_Save_Frame_Counter() { - using var target = CreateDefaultDevice(); + await using var target = CreateDefaultDevice(); this.loRaDeviceClient.Setup(x => x.UpdateReportedPropertiesAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(true); @@ -77,7 +122,7 @@ public async Task When_Setting_FcntDown_Should_Save_Frame_Counter() [Fact] public async Task When_Setting_FcntUp_Should_Save_Frame_Counter() { - using var target = CreateDefaultDevice(); + await using var target = CreateDefaultDevice(); this.loRaDeviceClient.Setup(x => x.UpdateReportedPropertiesAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(true); @@ -92,7 +137,7 @@ public async Task When_Setting_FcntUp_Should_Save_Frame_Counter() [Fact] public async Task After_Saving_Frame_Counter_Changes_Should_Not_Have_Pending_Changes() { - using var target = CreateDefaultDevice(); + await using var target = CreateDefaultDevice(); this.loRaDeviceClient.Setup(x => x.UpdateReportedPropertiesAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(true); @@ -107,33 +152,18 @@ public async Task After_Saving_Frame_Counter_Changes_Should_Not_Have_Pending_Cha [Fact] public async Task When_Initialized_New_OTAA_Device_Should_Have_All_Properties() { - var appKey = TestKeys.CreateAppKey(0xABC0200000000000, 0x09); - var joinEui = new JoinEui(0xABC0200000000009); - - var twin = TestUtils.CreateTwin( - desired: new Dictionary - { - { "AppEUI", joinEui.ToString() }, - { "AppKey", appKey.ToString() }, - { "GatewayID", "mygateway" }, - { "SensorDecoder", "DecoderValueSensor" }, - { "$version", 1 }, - }, - reported: new Dictionary - { - { "$version", 1 }, - }); + var twin = LoRaDeviceTwin.Create(OtaaDesiredTwinProperties, new LoRaReportedTwinProperties { Version = 1 }); this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); - using var connectionManager = new SingleDeviceConnectionManager(this.loRaDeviceClient.Object); - using var loRaDevice = new LoRaDevice(null, new DevEui(0xabc0200000000009), connectionManager); - await loRaDevice.InitializeAsync(this.configuration); - Assert.Equal(joinEui, loRaDevice.AppEui); - Assert.Equal(appKey, loRaDevice.AppKey); - Assert.Equal("mygateway", loRaDevice.GatewayID); - Assert.Equal("DecoderValueSensor", loRaDevice.SensorDecoder); + await using var connectionManager = new SingleDeviceConnectionManager(this.loRaDeviceClient.Object); + await using var loRaDevice = new LoRaDevice(null, new DevEui(0xabc0200000000009), connectionManager); + await loRaDevice.InitializeAsync(Configuration); + Assert.Equal(OtaaDesiredTwinProperties.JoinEui, loRaDevice.AppEui); + Assert.Equal(OtaaDesiredTwinProperties.AppKey, loRaDevice.AppKey); + Assert.Equal(OtaaDesiredTwinProperties.GatewayId, loRaDevice.GatewayID); + Assert.Equal(OtaaDesiredTwinProperties.SensorDecoder, loRaDevice.SensorDecoder); Assert.Equal(0U, loRaDevice.FCntDown); Assert.Equal(0U, loRaDevice.LastSavedFCntDown); Assert.Equal(0U, loRaDevice.FCntUp); @@ -145,89 +175,74 @@ public async Task When_Initialized_New_OTAA_Device_Should_Have_All_Properties() Assert.Null(loRaDevice.DevNonce); Assert.Null(loRaDevice.NetId); Assert.False(loRaDevice.IsABP); - Assert.False(loRaDevice.IsOurDevice); + Assert.True(loRaDevice.IsOurDevice); Assert.Null(loRaDevice.ReportedDwellTimeSetting); } [Fact] - public async Task When_Initialized_Joined_OTAA_Device_Should_Have_All_Properties() + public Task Initializing_Otaa_Device_Should_Determine_Device_Ownership() => + Initializing_Device_Should_Determine_Device_Ownership(OtaaDesiredTwinProperties); + + [Fact] + public Task Initializing_Abp_Device_Should_Determine_Device_Ownership() => + Initializing_Device_Should_Determine_Device_Ownership(AbpDesiredTwinProperties); + + private async Task Initializing_Device_Should_Determine_Device_Ownership(LoRaDesiredTwinProperties desiredProperties) { - var appKey = TestKeys.CreateAppKey(0xABC0200000000000, 0x09); - var networkSessionKey = TestKeys.CreateNetworkSessionKey(0xABC0200000000000, 0x09); - var appSessionKey = TestKeys.CreateAppSessionKey(0xABCD200000000000, 0x09); - var joinEui = new JoinEui(0xABC0200000000009); + // arrange + const string gateway = "mygateway"; + Assert.NotEqual(gateway, desiredProperties.GatewayId); + this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) + .ReturnsAsync(LoRaDeviceTwin.Create(desiredProperties with { GatewayId = gateway })); + await using var loRaDevice = CreateDefaultDevice(); - var twin = TestUtils.CreateTwin( - desired: new Dictionary - { - { "AppEUI", joinEui.ToString() }, - { "AppKey", appKey.ToString() }, - { "GatewayID", "mygateway" }, - { "SensorDecoder", "DecoderValueSensor" }, - { "$version", 1 }, - }, - reported: new Dictionary - { - { "$version", 1 }, - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevNonce", "0123" }, - { "DevAddr", "0000AABB" }, - }); + // act + _ = await loRaDevice.InitializeAsync(Configuration); + + // assert + Assert.False(loRaDevice.IsOurDevice); + Assert.Equal(gateway, loRaDevice.GatewayID); + } + + [Fact] + public async Task When_Initialized_Joined_OTAA_Device_Should_Have_All_Properties() + { + var twin = LoRaDeviceTwin.Create(OtaaDesiredTwinProperties, OtaaReportedTwinProperties); this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); - using var loRaDevice = CreateDefaultDevice(); + await using var loRaDevice = CreateDefaultDevice(); - await loRaDevice.InitializeAsync(this.configuration); - Assert.Equal(joinEui, loRaDevice.AppEui); - Assert.Equal(appKey, loRaDevice.AppKey); - Assert.Equal("mygateway", loRaDevice.GatewayID); - Assert.Equal("DecoderValueSensor", loRaDevice.SensorDecoder); + await loRaDevice.InitializeAsync(Configuration); + Assert.Equal(OtaaDesiredTwinProperties.JoinEui, loRaDevice.AppEui); + Assert.Equal(OtaaDesiredTwinProperties.AppKey, loRaDevice.AppKey); + Assert.Equal(OtaaDesiredTwinProperties.GatewayId, loRaDevice.GatewayID); + Assert.Equal(OtaaDesiredTwinProperties.SensorDecoder, loRaDevice.SensorDecoder); Assert.False(loRaDevice.IsABP); - Assert.False(loRaDevice.IsOurDevice); - Assert.Equal(networkSessionKey, loRaDevice.NwkSKey); - Assert.Equal(appSessionKey, loRaDevice.AppSKey); - Assert.Equal(new DevNonce(123), loRaDevice.DevNonce); - Assert.Equal(new DevAddr(0x0000aabb), loRaDevice.DevAddr); + Assert.True(loRaDevice.IsOurDevice); + Assert.Equal(OtaaReportedTwinProperties.NetworkSessionKey, loRaDevice.NwkSKey); + Assert.Equal(OtaaReportedTwinProperties.AppSessionKey, loRaDevice.AppSKey); + Assert.Equal(OtaaReportedTwinProperties.DevNonce, loRaDevice.DevNonce); + Assert.Equal(OtaaReportedTwinProperties.DevAddr, loRaDevice.DevAddr); Assert.Null(loRaDevice.ReportedDwellTimeSetting); } [Fact] public async Task When_Initialized_ABP_Device_Should_Have_All_Properties() { - var networkSessionKey = TestKeys.CreateNetworkSessionKey(0xABC0200000000000, 0x09); - var appSessionKey = TestKeys.CreateAppSessionKey(0xABCD200000000000, 0x09); - - var twin = TestUtils.CreateTwin( - desired: new Dictionary - { - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - { "GatewayID", this.configuration.GatewayID }, - { "SensorDecoder", "DecoderValueSensor" }, - { "$version", 1 }, - }, - reported: new Dictionary - { - { "$version", 1 }, - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - }); + var twin = LoRaDeviceTwin.Create(AbpDesiredTwinProperties, AbpReportedTwinProperties); this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); - using var loRaDevice = CreateDefaultDevice(); + await using var loRaDevice = CreateDefaultDevice(); - await loRaDevice.InitializeAsync(this.configuration); + await loRaDevice.InitializeAsync(Configuration); Assert.Null(loRaDevice.AppEui); Assert.Null(loRaDevice.AppKey); - Assert.Equal(this.configuration.GatewayID, loRaDevice.GatewayID); - Assert.Equal("DecoderValueSensor", loRaDevice.SensorDecoder); + Assert.Equal(Configuration.GatewayID, loRaDevice.GatewayID); + Assert.Equal(AbpDesiredTwinProperties.SensorDecoder, loRaDevice.SensorDecoder); Assert.True(loRaDevice.IsABP); Assert.True(loRaDevice.IsOurDevice); Assert.Equal(0U, loRaDevice.FCntDown); @@ -235,117 +250,46 @@ public async Task When_Initialized_ABP_Device_Should_Have_All_Properties() Assert.Equal(0U, loRaDevice.FCntUp); Assert.Equal(0U, loRaDevice.LastSavedFCntUp); Assert.False(loRaDevice.HasFrameCountChanges); - Assert.Equal(networkSessionKey, loRaDevice.NwkSKey); - Assert.Equal(appSessionKey, loRaDevice.AppSKey); + Assert.Equal(AbpDesiredTwinProperties.NetworkSessionKey, loRaDevice.NwkSKey); + Assert.Equal(AbpDesiredTwinProperties.AppSessionKey, loRaDevice.AppSKey); Assert.Null(loRaDevice.DevNonce); - Assert.Equal(new DevAddr(0x0000aabb), loRaDevice.DevAddr); + Assert.Equal(AbpDesiredTwinProperties.DevAddr, loRaDevice.DevAddr); Assert.Null(loRaDevice.ReportedDwellTimeSetting); } [Theory] - [InlineData("false")] - [InlineData("FALSE")] - [InlineData(0)] - [InlineData(false)] - public async Task When_Downlink_Is_Disabled_In_Twin_Should_Have_DownlinkEnabled_Equals_False(object downlinkEnabledTwinValue) - { - var networkSessionKey = TestKeys.CreateNetworkSessionKey(0xABC0200000000000, 0x09); - var appSessionKey = TestKeys.CreateAppSessionKey(0xABCD200000000000, 0x09); - - var twin = TestUtils.CreateTwin( - desired: new Dictionary - { - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - { "GatewayID", "mygateway" }, - { "SensorDecoder", "DecoderValueSensor" }, - { TwinProperty.DownlinkEnabled, downlinkEnabledTwinValue }, - { "$version", 1 }, - }, - reported: new Dictionary - { - { "$version", 1 }, - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - }); + [InlineData("false", false)] + [InlineData("FALSE", false)] + [InlineData(0, false)] + [InlineData(false, false)] + [InlineData("true", true)] + [InlineData("TRUE", true)] + [InlineData("1", true)] + [InlineData(1, true)] + [InlineData(true, true)] + public async Task Downlink_Should_Be_Deserialized_Correctly(object downlinkEnabledTwinValue, bool expectedDownlink) + { + var twin = LoRaDeviceTwin.Create(AbpDesiredTwinProperties, AbpReportedTwinProperties); + twin.Properties.Desired["Downlink"] = downlinkEnabledTwinValue; this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); - using var loRaDevice = CreateDefaultDevice(); - await loRaDevice.InitializeAsync(this.configuration); - Assert.False(loRaDevice.DownlinkEnabled); - } - - [Theory] - [InlineData("true")] - [InlineData("TRUE")] - [InlineData("1")] - [InlineData(1)] - [InlineData(true)] - public async Task When_Downlink_Is_Enabled_In_Twin_Should_Have_DownlinkEnabled_Equals_True(object downlinkTwinPropertyValue) - { - var networkSessionKey = TestKeys.CreateNetworkSessionKey(0xABC0200000000000, 0x09); - var appSessionKey = TestKeys.CreateAppSessionKey(0xABCD200000000000, 0x09); - - var twin = TestUtils.CreateTwin( - desired: new Dictionary - { - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - { "GatewayID", "mygateway" }, - { "SensorDecoder", "DecoderValueSensor" }, - { TwinProperty.DownlinkEnabled, downlinkTwinPropertyValue }, - { "$version", 1 }, - }, - reported: new Dictionary - { - { "$version", 1 }, - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - }); - - this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(twin); - - using var loRaDevice = CreateDefaultDevice(); - await loRaDevice.InitializeAsync(this.configuration); - Assert.True(loRaDevice.DownlinkEnabled); + await using var loRaDevice = CreateDefaultDevice(); + await loRaDevice.InitializeAsync(Configuration); + Assert.Equal(expectedDownlink, loRaDevice.DownlinkEnabled); } [Fact] public async Task When_Downlink_Is_Not_Defined_In_Twin_Should_Have_DownlinkEnabled_Equals_True() { - var networkSessionKey = TestKeys.CreateNetworkSessionKey(0xABC0200000000000, 0x09); - var appSessionKey = TestKeys.CreateAppSessionKey(0xABCD200000000000, 0x09); - - var twin = TestUtils.CreateTwin( - desired: new Dictionary - { - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - { "GatewayID", "mygateway" }, - { "SensorDecoder", "DecoderValueSensor" }, - { "$version", 1 }, - }, - reported: new Dictionary - { - { "$version", 1 }, - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - }); + var twin = LoRaDeviceTwin.Create(AbpDesiredTwinProperties, AbpReportedTwinProperties); this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); - using var loRaDevice = CreateDefaultDevice(); - await loRaDevice.InitializeAsync(this.configuration); + await using var loRaDevice = CreateDefaultDevice(); + await loRaDevice.InitializeAsync(Configuration); Assert.True(loRaDevice.DownlinkEnabled); } @@ -357,33 +301,14 @@ public async Task When_Downlink_Is_Not_Defined_In_Twin_Should_Have_DownlinkEnabl [InlineData(true)] public async Task When_PreferredWindow_Is_Not_2_In_Twin_Should_Have_Window1_As_Preferred(object preferredWindowTwinValue) { - var networkSessionKey = TestKeys.CreateNetworkSessionKey(0xABC0200000000000, 0x09); - var appSessionKey = TestKeys.CreateAppSessionKey(0xABCD200000000000, 0x09); - - var twin = TestUtils.CreateTwin( - desired: new Dictionary - { - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - { "GatewayID", "mygateway" }, - { "SensorDecoder", "DecoderValueSensor" }, - { TwinProperty.PreferredWindow, preferredWindowTwinValue }, - { "$version", 1 }, - }, - reported: new Dictionary - { - { "$version", 1 }, - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - }); + var twin = LoRaDeviceTwin.Create(AbpDesiredTwinProperties, AbpReportedTwinProperties); + twin.Properties.Desired[TwinProperty.PreferredWindow] = preferredWindowTwinValue; this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); - using var loRaDevice = CreateDefaultDevice(); - await loRaDevice.InitializeAsync(this.configuration); + await using var loRaDevice = CreateDefaultDevice(); + await loRaDevice.InitializeAsync(Configuration); Assert.Equal(ReceiveWindow1, loRaDevice.PreferredWindow); } @@ -393,116 +318,63 @@ public async Task When_PreferredWindow_Is_Not_2_In_Twin_Should_Have_Window1_As_P [InlineData(2.0)] public async Task When_PreferredWindow_Is_2_In_Twin_Should_Have_Window2_As_Preferred(object preferredWindowTwinProperty) { - var networkSessionKey = TestKeys.CreateNetworkSessionKey(0xABC0200000000000, 0x09); - var appSessionKey = TestKeys.CreateAppSessionKey(0xABCD200000000000, 0x09); - - var twin = TestUtils.CreateTwin( - desired: new Dictionary - { - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - { "GatewayID", "mygateway" }, - { "SensorDecoder", "DecoderValueSensor" }, - { TwinProperty.PreferredWindow, preferredWindowTwinProperty }, - { "$version", 1 }, - }, - reported: new Dictionary - { - { "$version", 1 }, - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - }); + var twin = LoRaDeviceTwin.Create(AbpDesiredTwinProperties, AbpReportedTwinProperties); + twin.Properties.Desired["PreferredWindow"] = preferredWindowTwinProperty; this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); - using var loRaDevice = CreateDefaultDevice(); - await loRaDevice.InitializeAsync(this.configuration); + await using var loRaDevice = CreateDefaultDevice(); + await loRaDevice.InitializeAsync(Configuration); Assert.Equal(ReceiveWindow2, loRaDevice.PreferredWindow); } [Fact] public async Task When_PreferredWindow_Is_Not_Define_In_Twin_Should_Have_Window1_As_Preferred() { - var networkSessionKey = TestKeys.CreateNetworkSessionKey(0xABC0200000000000, 0x09); - var appSessionKey = TestKeys.CreateAppSessionKey(0xABCD200000000000, 0x09); - - var twin = TestUtils.CreateTwin( - desired: new Dictionary - { - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - { "GatewayID", "mygateway" }, - { "SensorDecoder", "DecoderValueSensor" }, - { "$version", 1 }, - }, - reported: new Dictionary - { - { "$version", 1 }, - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - }); + var twin = LoRaDeviceTwin.Create(AbpDesiredTwinProperties, AbpReportedTwinProperties); this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); - using var loRaDevice = CreateDefaultDevice(); - await loRaDevice.InitializeAsync(this.configuration); + await using var loRaDevice = CreateDefaultDevice(); + await loRaDevice.InitializeAsync(Configuration); Assert.Equal(ReceiveWindow1, loRaDevice.PreferredWindow); } [Fact] public async Task When_CN470JoinChannel_Is_In_Twin_Should_Have_JoinChannel_Set() { - var networkSessionKey = TestKeys.CreateNetworkSessionKey(0xABC0200000000000, 0x09); - var appSessionKey = TestKeys.CreateAppSessionKey(0xABCD200000000000, 0x09); - - var twin = TestUtils.CreateTwin( - desired: new Dictionary - { - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - { "CN470JoinChannel", 10 } - }, - reported: new Dictionary - { - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - { "CN470JoinChannel", 2 } - }); + var twin = LoRaDeviceTwin.Create(AbpDesiredTwinProperties, AbpReportedTwinProperties); + twin.Properties.Desired["CN470JoinChannel"] = 10; + twin.Properties.Reported["CN470JoinChannel"] = 2; this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); - using var loRaDevice = CreateDefaultDevice(); - await loRaDevice.InitializeAsync(this.configuration); + await using var loRaDevice = CreateDefaultDevice(); + await loRaDevice.InitializeAsync(Configuration); Assert.Equal(2, loRaDevice.ReportedCN470JoinChannel); // check that reported property is prioritized } [Fact] - public void New_LoRaDevice_Should_Have_C2D_Enabled() + public async Task New_LoRaDevice_Should_Have_C2D_Enabled() { - using var loRaDevice = CreateDefaultDevice(); + await using var loRaDevice = CreateDefaultDevice(); Assert.True(loRaDevice.DownlinkEnabled); } [Fact] - public void New_LoRaDevice_Should_Have_PreferredWindow_As_1() + public async Task New_LoRaDevice_Should_Have_PreferredWindow_As_1() { - using var loRaDevice = CreateDefaultDevice(); + await using var loRaDevice = CreateDefaultDevice(); Assert.Equal(ReceiveWindow1, loRaDevice.PreferredWindow); } [Fact] - public void After_3_Resubmits_Should_Not_Be_Valid_To_Resend_Ack() + public async Task After_3_Resubmits_Should_Not_Be_Valid_To_Resend_Ack() { - using var target = CreateDefaultDevice(); + await using var target = CreateDefaultDevice(); // 1st time target.SetFcntUp(12); @@ -536,9 +408,9 @@ public void After_3_Resubmits_Should_Not_Be_Valid_To_Resend_Ack() } [Fact] - public void When_ResetFcnt_In_New_Instance_Should_Have_HasFrameCountChanges_False() + public async Task When_ResetFcnt_In_New_Instance_Should_Have_HasFrameCountChanges_False() { - using var target = CreateDefaultDevice(); + await using var target = CreateDefaultDevice(); // Setting from 0 to 0 should not trigger changes target.ResetFcnt(); @@ -550,14 +422,14 @@ public void When_ResetFcnt_In_New_Instance_Should_Have_HasFrameCountChanges_Fals [Fact] public async Task When_Updating_LastUpdate_Is_Updated() { - var twin = TestUtils.CreateTwin(desired: GetEssentialDesiredProperties()); + var twin = LoRaDeviceTwin.Create(OtaaDesiredTwinProperties); this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); - using var loRaDevice = CreateDefaultDevice(); + await using var loRaDevice = CreateDefaultDevice(); var lastUpdate = loRaDevice.LastUpdate = DateTime.UtcNow - TimeSpan.FromDays(1); - await loRaDevice.InitializeAsync(this.configuration); + await loRaDevice.InitializeAsync(Configuration); Assert.True(loRaDevice.LastUpdate > lastUpdate); } @@ -567,19 +439,19 @@ public async Task When_Update_Fails_LastUpdate_Is_Not_Changed() this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ThrowsAsync(new IotHubException()); - using var loRaDevice = CreateDefaultDevice(); + await using var loRaDevice = CreateDefaultDevice(); var lastUpdate = loRaDevice.LastUpdate = DateTime.UtcNow - TimeSpan.FromDays(1); - await Assert.ThrowsAsync(async () => await loRaDevice.InitializeAsync(this.configuration)); + await Assert.ThrowsAsync(async () => await loRaDevice.InitializeAsync(Configuration)); Assert.Equal(lastUpdate, loRaDevice.LastUpdate); } [Fact] - public void When_ResetFcnt_In_Device_With_Pending_Changes_Should_Have_HasFrameCountChanges_True() + public async Task When_ResetFcnt_In_Device_With_Pending_Changes_Should_Have_HasFrameCountChanges_True() { var devAddr = new DevAddr(0x1231); // Non zero fcnt up - using var target = CreateDefaultDevice(); + await using var target = CreateDefaultDevice(); target.SetFcntUp(1); target.AcceptFrameCountChanges(); target.ResetFcnt(); @@ -588,8 +460,8 @@ public void When_ResetFcnt_In_Device_With_Pending_Changes_Should_Have_HasFrameCo Assert.True(target.HasFrameCountChanges); // Non zero fcnt down - using var secondConnectionManager = new SingleDeviceConnectionManager(this.loRaDeviceClient.Object); - using var secondTarget = new LoRaDevice(devAddr, new DevEui(0x12312), secondConnectionManager); + await using var secondConnectionManager = new SingleDeviceConnectionManager(this.loRaDeviceClient.Object); + await using var secondTarget = new LoRaDevice(devAddr, new DevEui(0x12312), secondConnectionManager); secondTarget.SetFcntDown(1); secondTarget.AcceptFrameCountChanges(); secondTarget.ResetFcnt(); @@ -598,8 +470,8 @@ public void When_ResetFcnt_In_Device_With_Pending_Changes_Should_Have_HasFrameCo Assert.True(secondTarget.HasFrameCountChanges); // Non zero fcnt down and up - using var thirdConnectionManager = new SingleDeviceConnectionManager(this.loRaDeviceClient.Object); - using var thirdTarget = new LoRaDevice(devAddr, new DevEui(0x12312), thirdConnectionManager); + await using var thirdConnectionManager = new SingleDeviceConnectionManager(this.loRaDeviceClient.Object); + await using var thirdTarget = new LoRaDevice(devAddr, new DevEui(0x12312), thirdConnectionManager); thirdTarget.SetFcntDown(1); thirdTarget.SetFcntDown(2); thirdTarget.AcceptFrameCountChanges(); @@ -610,12 +482,12 @@ public void When_ResetFcnt_In_Device_With_Pending_Changes_Should_Have_HasFrameCo } [Fact] - public void When_ResetFcnt_In_NonZero_FcntUp_Or_FcntDown_Should_Have_HasFrameCountChanges_True() + public async Task When_ResetFcnt_In_NonZero_FcntUp_Or_FcntDown_Should_Have_HasFrameCountChanges_True() { var devAddr = new DevAddr(0x1231); // Non zero fcnt up - using var target = CreateDefaultDevice(); + await using var target = CreateDefaultDevice(); target.SetFcntUp(1); target.AcceptFrameCountChanges(); target.ResetFcnt(); @@ -624,8 +496,8 @@ public void When_ResetFcnt_In_NonZero_FcntUp_Or_FcntDown_Should_Have_HasFrameCou Assert.True(target.HasFrameCountChanges); // Non zero fcnt down - using var secondConnectionManager = new SingleDeviceConnectionManager(this.loRaDeviceClient.Object); - using var secondTarget = new LoRaDevice(devAddr, new DevEui(0x12312), secondConnectionManager); + await using var secondConnectionManager = new SingleDeviceConnectionManager(this.loRaDeviceClient.Object); + await using var secondTarget = new LoRaDevice(devAddr, new DevEui(0x12312), secondConnectionManager); secondTarget.SetFcntDown(1); secondTarget.AcceptFrameCountChanges(); secondTarget.ResetFcnt(); @@ -636,8 +508,8 @@ public void When_ResetFcnt_In_NonZero_FcntUp_Or_FcntDown_Should_Have_HasFrameCou Assert.True(secondTarget.HasFrameCountChanges); // Non zero fcnt down and up - using var thirdConnectionManager = new SingleDeviceConnectionManager(this.loRaDeviceClient.Object); - using var thirdTarget = new LoRaDevice(devAddr, new DevEui(0x12312), thirdConnectionManager); + await using var thirdConnectionManager = new SingleDeviceConnectionManager(this.loRaDeviceClient.Object); + await using var thirdTarget = new LoRaDevice(devAddr, new DevEui(0x12312), thirdConnectionManager); thirdTarget.SetFcntDown(1); thirdTarget.SetFcntDown(2); thirdTarget.AcceptFrameCountChanges(); @@ -655,35 +527,16 @@ public void When_ResetFcnt_In_NonZero_FcntUp_Or_FcntDown_Should_Have_HasFrameCou [Fact] public async Task When_Initialized_ABP_Device_Has_Fcnt_Should_Have_Non_Zero_Fcnt_Values() { - var networkSessionKey = TestKeys.CreateNetworkSessionKey(0xABC0200000000000, 0x09); - var appSessionKey = TestKeys.CreateAppSessionKey(0xABCD200000000000, 0x09); - - var twin = TestUtils.CreateTwin( - desired: new Dictionary - { - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - { "GatewayID", this.configuration.GatewayID }, - { "SensorDecoder", "DecoderValueSensor" }, - { "$version", 1 }, - }, - reported: new Dictionary - { - { "$version", 1 }, - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - { "FCntDown", 10 }, - { "FCntUp", 20 }, - }); + var twin = LoRaDeviceTwin.Create(AbpDesiredTwinProperties, AbpReportedTwinProperties); + twin.Properties.Reported["FCntDown"] = 10; + twin.Properties.Reported["FCntUp"] = 20; this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); - using var loRaDevice = CreateDefaultDevice(); + await using var loRaDevice = CreateDefaultDevice(); - await loRaDevice.InitializeAsync(this.configuration); + await loRaDevice.InitializeAsync(Configuration); Assert.True(loRaDevice.IsOurDevice); Assert.Equal(10U, loRaDevice.FCntDown); Assert.Equal(10U, loRaDevice.LastSavedFCntDown); @@ -699,36 +552,22 @@ public async Task When_Initialized_ABP_Device_Has_Fcnt_Should_Have_Non_Zero_Fcnt public async Task When_Initialized_With_PreferredGateway_And_Region_Should_Get_Properties( [CombinatorialValues("EU868", "3132", "eu868", "US915", "us915", "eu")] string regionValue) { - var networkSessionKey = TestKeys.CreateNetworkSessionKey(0xABC0200000000000, 0x09); - var appSessionKey = TestKeys.CreateAppSessionKey(0xABCD200000000000, 0x09); - - var twin = TestUtils.CreateTwin( - desired: new Dictionary - { - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - { "GatewayID", "mygateway" }, - { "SensorDecoder", "DecoderValueSensor" }, - { "$version", 1 }, - }, - reported: new Dictionary + var twin = LoRaDeviceTwin.Create( + AbpDesiredTwinProperties, + AbpReportedTwinProperties with { - { "$version", 1 }, - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - { "FCntDown", 10 }, - { "FCntUp", 20 }, - { "PreferredGatewayID", "gateway1" }, - { "Region", regionValue } + FCntDown = 10, + FCntUp = 20, + PreferredGatewayId = "gateway1", }); + twin.Properties.Reported["Region"] = regionValue; + this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); - using var loRaDevice = CreateDefaultDevice(); - await loRaDevice.InitializeAsync(this.configuration); + await using var loRaDevice = CreateDefaultDevice(); + await loRaDevice.InitializeAsync(Configuration); if (string.Equals(LoRaRegionType.EU868.ToString(), regionValue, StringComparison.OrdinalIgnoreCase)) Assert.Equal(LoRaRegionType.EU868, loRaDevice.LoRaRegion); @@ -746,174 +585,48 @@ public async Task When_Initialized_ABP_Device_Has_Fcnt_Should_Have_Non_Zero_Fcnt [InlineData(120, 120)] public async Task When_Initialized_With_Keep_Alive_Should_Read_Value_From_Twin(object keepAliveTimeoutValue, int expectedKeepAliveTimeout) { - var networkSessionKey = TestKeys.CreateNetworkSessionKey(0xABC0200000000000, 0x09); - var appSessionKey = TestKeys.CreateAppSessionKey(0xABCD200000000000, 0x09); - - var twin = TestUtils.CreateTwin( - desired: new Dictionary - { - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - { "GatewayID", "mygateway" }, - { "SensorDecoder", "DecoderValueSensor" }, - { "KeepAliveTimeout", keepAliveTimeoutValue }, - { "$version", 1 }, - }, - reported: new Dictionary - { - { "$version", 1 }, - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" } - }); + const string gatewayId = "mygateway"; + var twin = LoRaDeviceTwin.Create(AbpDesiredTwinProperties with { GatewayId = gatewayId }, AbpReportedTwinProperties); + twin.Properties.Desired["KeepAliveTimeout"] = keepAliveTimeoutValue; this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); - using var loRaDevice = CreateDefaultDevice(); - await loRaDevice.InitializeAsync(this.configuration); + await using var loRaDevice = CreateDefaultDevice(); + await loRaDevice.InitializeAsync(Configuration); Assert.Equal(expectedKeepAliveTimeout, loRaDevice.KeepAliveTimeout); } - [Fact] - public void When_Device_Has_No_Connection_Timeout_Should_Disconnect() - { - var deviceClient = new Mock(MockBehavior.Strict); - deviceClient.Setup(dc => dc.Dispose()); - using var cache = new MemoryCache(new MemoryCacheOptions()); - using var manager = new LoRaDeviceClientConnectionManager(cache, NullLogger.Instance); - using var device = new LoRaDevice(DevAddr.Private0(0), new DevEui(0x0123456789), manager); - manager.Register(device, deviceClient.Object); - - var activity = device.BeginDeviceClientConnectionActivity(); - Assert.NotNull(activity); - - deviceClient.Setup(x => x.Disconnect()) - .Returns(true); - - Assert.True(device.TryDisconnect()); - - deviceClient.Verify(x => x.Disconnect(), Times.Once()); - } - - [Fact] - public void When_Device_Connection_Not_In_Use_Should_Disconnect() - { - var deviceClient = new Mock(MockBehavior.Strict); - deviceClient.Setup(dc => dc.Dispose()); - using var cache = new MemoryCache(new MemoryCacheOptions()); - using var manager = new LoRaDeviceClientConnectionManager(cache, NullLogger.Instance); - using var device = new LoRaDevice(DevAddr.Private0(0), new DevEui(0x0123456789), manager); - device.KeepAliveTimeout = 60; - manager.Register(device, deviceClient.Object); - - deviceClient.Setup(x => x.EnsureConnected()) - .Returns(true); - - var activity1 = device.BeginDeviceClientConnectionActivity(); - Assert.NotNull(activity1); - - Assert.False(device.TryDisconnect()); - - var activity2 = device.BeginDeviceClientConnectionActivity(); - Assert.NotNull(activity2); - - Assert.False(device.TryDisconnect()); - activity1.Dispose(); - Assert.False(device.TryDisconnect()); - - activity2.Dispose(); - deviceClient.Setup(x => x.Disconnect()) - .Returns(true); - - Assert.True(device.TryDisconnect()); - - deviceClient.Verify(x => x.EnsureConnected(), Times.Exactly(2)); - } - - [Fact] - public void When_Needed_Should_Reconnect_Client() - { - var deviceClient = new Mock(MockBehavior.Strict); - deviceClient.Setup(dc => dc.Dispose()); - using var cache = new MemoryCache(new MemoryCacheOptions()); - using var manager = new LoRaDeviceClientConnectionManager(cache, NullLogger.Instance); - using var device = new LoRaDevice(DevAddr.Private0(0), new DevEui(0x0123456789), manager); - device.KeepAliveTimeout = 60; - manager.Register(device, deviceClient.Object); - - deviceClient.Setup(x => x.EnsureConnected()) - .Returns(true); - - deviceClient.Setup(x => x.Disconnect()) - .Returns(true); - - using (var activity1 = device.BeginDeviceClientConnectionActivity()) - { - Assert.NotNull(activity1); - } - - Assert.True(device.TryDisconnect()); - - using (var activity2 = device.BeginDeviceClientConnectionActivity()) - { - Assert.NotNull(activity2); - - Assert.False(device.TryDisconnect()); - } - - Assert.True(device.TryDisconnect()); - - deviceClient.Verify(x => x.EnsureConnected(), Times.Exactly(2)); - deviceClient.Verify(x => x.Disconnect(), Times.Exactly(2)); - } - [Fact] public async Task When_Initialized_With_Class_C_And_Custom_RX2DR_Should_Have_Correct_Properties() { - var networkSessionKey = TestKeys.CreateNetworkSessionKey(0xABC0200000000000, 0x09); - var appSessionKey = TestKeys.CreateAppSessionKey(0xABCD200000000000, 0x09); - - var twin = TestUtils.CreateTwin( - desired: new Dictionary - { - { "AppEUI", new JoinEui(0xABCD1234).ToString() }, - { "AppKey", "ABCD2000000000000000000000000009" }, - { "ClassType", "C" }, - { "GatewayID", "mygateway" }, - { "SensorDecoder", "http://mydecoder" }, - { "RX2DataRate", "10" }, - { "$version", 1 }, - }, - reported: new Dictionary + var twin = LoRaDeviceTwin.Create( + OtaaDesiredTwinProperties, + OtaaReportedTwinProperties with { - { "$version", 1 }, - { "NwkSKey", networkSessionKey.ToString() }, - { "AppSKey", appSessionKey.ToString() }, - { "DevAddr", "0000AABB" }, - { "FCntDown", 9 }, - { "FCntUp", 100 }, - { "DevEUI", "ABC0200000000009" }, - { "NetId", "010000" }, - { "DevNonce", "C872" }, - { "RX2DataRate", 10 }, - { "Region", "US915" }, + FCntDown = 9, + FCntUp = 100, + NetId = new NetId(1231), + Region = LoRaRegionType.US915 }); + twin.Properties.Desired["RX2DataRate"] = "10"; + twin.Properties.Desired["ClassType"] = "C"; + twin.Properties.Reported["RX2DataRate"] = 10; + this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); - using var loRaDevice = CreateDefaultDevice(); - await loRaDevice.InitializeAsync(this.configuration); + await using var loRaDevice = CreateDefaultDevice(); + await loRaDevice.InitializeAsync(Configuration); Assert.Equal(LoRaDeviceClassType.C, loRaDevice.ClassType); - Assert.Equal("mygateway", loRaDevice.GatewayID); + Assert.Equal(OtaaDesiredTwinProperties.GatewayId, loRaDevice.GatewayID); Assert.Equal(9u, loRaDevice.FCntDown); Assert.Equal(100u, loRaDevice.FCntUp); Assert.Equal(DR10, loRaDevice.ReportedRX2DataRate.Value); Assert.Equal(DR10, loRaDevice.DesiredRX2DataRate.Value); - Assert.Equal(appSessionKey, loRaDevice.AppSKey); - Assert.Equal(networkSessionKey, loRaDevice.NwkSKey); + Assert.Equal(OtaaReportedTwinProperties.AppSessionKey, loRaDevice.AppSKey); + Assert.Equal(OtaaReportedTwinProperties.NetworkSessionKey, loRaDevice.NwkSKey); Assert.Equal(LoRaRegionType.US915, loRaDevice.LoRaRegion); Assert.False(loRaDevice.IsABP); } @@ -921,11 +634,11 @@ public async Task When_Initialized_With_Class_C_And_Custom_RX2DR_Should_Have_Cor [Theory] [InlineData(false)] [InlineData(true)] - public void When_Updating_Dwell_Time_Settings_Should_Update(bool acceptChanges) + public async Task When_Updating_Dwell_Time_Settings_Should_Update(bool acceptChanges) { // arrange var dwellTimeSetting = new DwellTimeSetting(true, false, 3); - using var loRaDevice = CreateDefaultDevice(); + await using var loRaDevice = CreateDefaultDevice(); // act loRaDevice.UpdateDwellTimeSetting(dwellTimeSetting, acceptChanges); @@ -941,7 +654,7 @@ public async Task When_Updating_Dwell_Time_Settings_Save_Success(bool acceptChan { // arrange var dwellTimeSetting = new DwellTimeSetting(true, false, 3); - using var loRaDevice = CreateDefaultDevice(); + await using var loRaDevice = CreateDefaultDevice(); TwinCollection actualReportedProperties = null; this.loRaDeviceClient.Setup(x => x.UpdateReportedPropertiesAsync(It.IsAny(), It.IsAny())) .Callback((TwinCollection t, CancellationToken _) => actualReportedProperties = t) @@ -969,29 +682,23 @@ public async Task When_Updating_Dwell_Time_Settings_Save_Success(bool acceptChan public async Task InitializeAsync_Should_Initialize_TxParams() { // arrange - using var loRaDevice = CreateDefaultDevice(); + await using var loRaDevice = CreateDefaultDevice(); var dwellTimeSetting = new DwellTimeSetting(true, false, 4); - var twin = TestUtils.CreateTwin(GetEssentialDesiredProperties(), - new Dictionary { [TwinProperty.TxParam] = JsonSerializer.Serialize(dwellTimeSetting) }); + var twin = LoRaDeviceTwin.Create(OtaaDesiredTwinProperties); + twin.Properties.Reported["TxParam"] = JsonSerializer.Serialize(dwellTimeSetting); this.loRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) .ReturnsAsync(twin); // act - _ = await loRaDevice.InitializeAsync(this.configuration); + _ = await loRaDevice.InitializeAsync(Configuration); // assert Assert.Equal(dwellTimeSetting, loRaDevice.ReportedDwellTimeSetting); } - private static Dictionary GetEssentialDesiredProperties() => - new Dictionary - { - ["AppEUI"] = new JoinEui(0xABCD1234).ToString(), - ["AppKey"] = TestKeys.CreateAppKey(1).ToString() - }; - #pragma warning disable CA2000 // Dispose objects before losing scope - private LoRaDevice CreateDefaultDevice() => new LoRaDevice(new DevAddr(0xffffffff), new DevEui(0), new SingleDeviceConnectionManager(this.loRaDeviceClient.Object)); + private LoRaDevice CreateDefaultDevice(ILoRaDeviceClientConnectionManager connectionManager = null) => + new(new DevAddr(0xffffffff), new DevEui(0), connectionManager ?? new SingleDeviceConnectionManager(this.loRaDeviceClient.Object)); #pragma warning restore CA2000 // Dispose objects before losing scope public class FrameCounterInitTests @@ -1004,17 +711,18 @@ public FrameCounterInitTests() } [Fact] - public void If_No_Reset_FcntUpDown_Initialized() + public async Task If_No_Reset_FcntUpDown_Initialized() { - using var device = CreateDefault(); + await using var device = CreateDefault(); const uint fcntUp = 10; const uint fcntDown = 2; - var twin = TestUtils.CreateTwin(reported: new Dictionary - { - [TwinProperty.FCntUp] = fcntUp, - [TwinProperty.FCntDown] = fcntDown - }); + + var twin = LoRaDeviceTwin.Create(reportedProperties: new LoRaReportedTwinProperties + { + FCntUp = fcntUp, + FCntDown = fcntDown, + }); device.ExecuteInitializeFrameCounters(twin); AssertFcntUp(fcntUp, device); @@ -1028,26 +736,28 @@ public void If_No_Reset_FcntUpDown_Initialized() [InlineData(1, 1, 10, 10, 10, 10, false)] // all up to date - no update - expect set to last reported [InlineData(2, 1, 10, 10, 10, 10, true)] // reset counter higher - expect to set to start counter and saved [InlineData(2, 3, 10, 10, 10, 10, false)] // reset counter smaller - expect set to last reported - public void When_Start_Specified_Initialized_Correctly(uint fcntResetDesired, uint fcntResetReported, uint startDesiredUp, uint startDesiredDown, uint startReportedUp, uint startReportedDown, bool expectStart) + public async Task When_Start_Specified_Initialized_Correctly(uint fcntResetDesired, uint fcntResetReported, uint startDesiredUp, uint startDesiredDown, uint startReportedUp, uint startReportedDown, bool expectStart) { - using var device = CreateDefault(); + await using var device = CreateDefault(); const uint fcntUp = 10; const uint fcntDown = 2; - var twin = TestUtils.CreateTwin(desired: new Dictionary - { - [TwinProperty.FCntUpStart] = startDesiredUp, - [TwinProperty.FCntDownStart] = startDesiredDown, - [TwinProperty.FCntResetCounter] = fcntResetDesired - }, - reported: new Dictionary - { - [TwinProperty.FCntUpStart] = startReportedUp, - [TwinProperty.FCntDownStart] = startReportedDown, - [TwinProperty.FCntUp] = fcntUp, - [TwinProperty.FCntDown] = fcntDown, - [TwinProperty.FCntResetCounter] = fcntResetReported - }); + + var twin = LoRaDeviceTwin.Create( + new LoRaDesiredTwinProperties + { + FCntUpStart = startDesiredUp, + FCntDownStart = startDesiredDown, + FCntResetCounter = fcntResetDesired + }, + new LoRaReportedTwinProperties + { + FCntUpStart = startReportedUp, + FCntDownStart = startReportedDown, + FCntUp = fcntUp, + FCntDown = fcntDown, + FCntResetCounter = fcntResetReported + }); device.ExecuteInitializeFrameCounters(twin); @@ -1068,24 +778,25 @@ public void When_Start_Specified_Initialized_Correctly(uint fcntResetDesired, ui [Theory] [InlineData(10, 10, 0, 0, true)] [InlineData(10, 10, 10, 10, false)] - public void When_Reset_Specified_Initialized_Correctly(uint startDesiredUp, uint startDesiredDown, uint startReportedUp, uint startReportedDown, bool expectStart) + public async Task When_Reset_Specified_Initialized_Correctly(uint startDesiredUp, uint startDesiredDown, uint startReportedUp, uint startReportedDown, bool expectStart) { - using var device = CreateDefault(); + await using var device = CreateDefault(); const uint fcntUp = 10; const uint fcntDown = 2; - var twin = TestUtils.CreateTwin(desired: new Dictionary - { - [TwinProperty.FCntUpStart] = startDesiredUp, - [TwinProperty.FCntDownStart] = startDesiredDown - }, - reported: new Dictionary - { - [TwinProperty.FCntUpStart] = startReportedUp, - [TwinProperty.FCntDownStart] = startReportedDown, - [TwinProperty.FCntUp] = fcntUp, - [TwinProperty.FCntDown] = fcntDown - }); + var twin = LoRaDeviceTwin.Create( + new LoRaDesiredTwinProperties + { + FCntUpStart = startDesiredUp, + FCntDownStart = startDesiredDown, + }, + new LoRaReportedTwinProperties + { + FCntUpStart = startReportedUp, + FCntDownStart = startReportedDown, + FCntUp = fcntUp, + FCntDown = fcntDown + }); device.ExecuteInitializeFrameCounters(twin); @@ -1132,5 +843,23 @@ public void ExecuteInitializeFrameCounters(Twin twin) new TwinCollectionReader(twin.Properties.Reported, this.logger)); } } + + [Theory] + [InlineData(0)] + [InlineData(1)] + public async Task BeginDeviceClientConnectionActivity_Delegates_To_Connection_Manager_When_Device_Has_KeepAliveTimeout(int timeoutSeconds) + { + // arrange + var connectionManagerMock = new Mock(); + await using var target = CreateDefaultDevice(connectionManagerMock.Object); + target.KeepAliveTimeout = timeoutSeconds; + + // act + target.BeginDeviceClientConnectionActivity(); + + // assert + connectionManagerMock.Verify(x => x.BeginDeviceClientConnectionActivity(target), + Times.Exactly(timeoutSeconds > 0 ? 1 : 0)); + } } } diff --git a/Tests/Unit/NetworkServer/LoRaOperationTimeWatcherTest.cs b/Tests/Unit/NetworkServer/LoRaOperationTimeWatcherTest.cs index 37d22c32ea..2402b1638e 100644 --- a/Tests/Unit/NetworkServer/LoRaOperationTimeWatcherTest.cs +++ b/Tests/Unit/NetworkServer/LoRaOperationTimeWatcherTest.cs @@ -4,6 +4,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer { using System; + using System.Threading.Tasks; using global::LoRaTools.Regions; using LoRaWan.NetworkServer; using Moq; @@ -56,10 +57,10 @@ public void After_6_Seconds_Should_Not_Be_In_Time_For_Join() [InlineData(0)] [InlineData(950)] [InlineData(1390)] - public void When_In_Time_For_First_Window_But_Device_Preferes_Seconds_Should_Resolve_Window_2(int delayInMs) + public async Task When_In_Time_For_First_Window_But_Device_Preferes_Seconds_Should_Resolve_Window_2(int delayInMs) { var target = new LoRaOperationTimeWatcher(RegionManager.EU868, DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMilliseconds(delayInMs))); - using var loRaDevice = new LoRaDevice(new DevAddr(0x31312), new DevEui(0x312321321), ConnectionManager) + await using var loRaDevice = new LoRaDevice(new DevAddr(0x31312), new DevEui(0x312321321), ConnectionManager) { PreferredWindow = ReceiveWindow2, }; @@ -70,10 +71,10 @@ public void When_In_Time_For_First_Window_But_Device_Preferes_Seconds_Should_Res [Theory] [InlineData(0)] [InlineData(490)] - public void When_In_Time_For_First_Window_Should_Resolve_Window_1(int delayInMs) + public async Task When_In_Time_For_First_Window_Should_Resolve_Window_1(int delayInMs) { var target = new LoRaOperationTimeWatcher(RegionManager.EU868, DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMilliseconds(delayInMs))); - using var loRaDevice = new LoRaDevice(new DevAddr(0x31312), new DevEui(0x312321321), ConnectionManager); + await using var loRaDevice = new LoRaDevice(new DevAddr(0x31312), new DevEui(0x312321321), ConnectionManager); Assert.Equal(ReceiveWindow1, target.ResolveReceiveWindowToUse(loRaDevice)); } @@ -82,10 +83,10 @@ public void When_In_Time_For_First_Window_Should_Resolve_Window_1(int delayInMs) [InlineData(1000)] [InlineData(1001)] [InlineData(1490)] - public void When_In_Time_For_Second_Window_Should_Resolve_Window_2(int delayInMs) + public async Task When_In_Time_For_Second_Window_Should_Resolve_Window_2(int delayInMs) { var target = new LoRaOperationTimeWatcher(RegionManager.EU868, DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMilliseconds(delayInMs))); - using var loRaDevice = new LoRaDevice(new DevAddr(0x31312), new DevEui(0x312321321), ConnectionManager); + await using var loRaDevice = new LoRaDevice(new DevAddr(0x31312), new DevEui(0x312321321), ConnectionManager); Assert.Equal(ReceiveWindow2, target.ResolveReceiveWindowToUse(loRaDevice)); } @@ -94,10 +95,10 @@ public void When_In_Time_For_Second_Window_Should_Resolve_Window_2(int delayInMs [InlineData(1801)] [InlineData(2000)] [InlineData(4000)] - public void When_Missed_Both_Windows_Should_Resolve_Window_0(int delayInMs) + public async Task When_Missed_Both_Windows_Should_Resolve_Window_0(int delayInMs) { var target = new LoRaOperationTimeWatcher(RegionManager.EU868, DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMilliseconds(delayInMs))); - using var loRaDevice = new LoRaDevice(new DevAddr(0x31312), new DevEui(0x312321321), ConnectionManager); + await using var loRaDevice = new LoRaDevice(new DevAddr(0x31312), new DevEui(0x312321321), ConnectionManager); Assert.Null(target.ResolveReceiveWindowToUse(loRaDevice)); } @@ -109,10 +110,10 @@ public void When_Missed_Both_Windows_Should_Resolve_Window_0(int delayInMs) [InlineData(3000)] [InlineData(4000)] [InlineData(4490)] - public void When_In_Time_For_Join_Accept_First_Window_Should_Resolve_Window_1(int delayInMs) + public async Task When_In_Time_For_Join_Accept_First_Window_Should_Resolve_Window_1(int delayInMs) { var target = new LoRaOperationTimeWatcher(RegionManager.EU868, DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMilliseconds(delayInMs))); - using var loRaDevice = new LoRaDevice(new DevAddr(0x31312), new DevEui(0x312321321), ConnectionManager); + await using var loRaDevice = new LoRaDevice(new DevAddr(0x31312), new DevEui(0x312321321), ConnectionManager); Assert.Equal(ReceiveWindow1, target.ResolveJoinAcceptWindowToUse()); } @@ -121,10 +122,10 @@ public void When_In_Time_For_Join_Accept_First_Window_Should_Resolve_Window_1(in [InlineData(4900)] [InlineData(5000)] [InlineData(5490)] - public void When_In_Time_For_Join_Accept_Second_Window_Should_Resolve_Window_2(int delayInMs) + public async Task When_In_Time_For_Join_Accept_Second_Window_Should_Resolve_Window_2(int delayInMs) { var target = new LoRaOperationTimeWatcher(RegionManager.EU868, DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMilliseconds(delayInMs))); - using var loRaDevice = new LoRaDevice(new DevAddr(0x31312), new DevEui(0x312321321), ConnectionManager); + await using var loRaDevice = new LoRaDevice(new DevAddr(0x31312), new DevEui(0x312321321), ConnectionManager); Assert.Equal(ReceiveWindow2, target.ResolveJoinAcceptWindowToUse()); } @@ -133,10 +134,10 @@ public void When_In_Time_For_Join_Accept_Second_Window_Should_Resolve_Window_2(i [InlineData(6000)] [InlineData(7000)] [InlineData(8000)] - public void When_Out_Of_Time_For_Join_Accept_Second_Window_Should_Resolve_Window_0(int delayInMs) + public async Task When_Out_Of_Time_For_Join_Accept_Second_Window_Should_Resolve_Window_0(int delayInMs) { var target = new LoRaOperationTimeWatcher(RegionManager.EU868, DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMilliseconds(delayInMs))); - using var loRaDevice = new LoRaDevice(new DevAddr(0x31312), new DevEui(0x312321321), ConnectionManager); + await using var loRaDevice = new LoRaDevice(new DevAddr(0x31312), new DevEui(0x312321321), ConnectionManager); Assert.Null(target.ResolveJoinAcceptWindowToUse()); } @@ -153,10 +154,10 @@ public void When_Out_Of_Time_For_Join_Accept_Second_Window_Should_Resolve_Window [InlineData(1601, 0, 0)] [InlineData(1700, 0, 0)] [InlineData(3000, 0, 0)] - public void When_Device_PreferredWindow1_In_Time_For_First_Window_Should_Get_Check_C2D_Avaible_Time_Correctly(int delayInMs, int expectedMinMs, int expectedMaxMs) + public async Task When_Device_PreferredWindow1_In_Time_For_First_Window_Should_Get_Check_C2D_Avaible_Time_Correctly(int delayInMs, int expectedMinMs, int expectedMaxMs) { var target = new LoRaOperationTimeWatcher(RegionManager.EU868, DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMilliseconds(delayInMs))); - using var loRaDevice = new LoRaDevice(new DevAddr(0x1111), new DevEui(0x2222), ConnectionManager); + await using var loRaDevice = new LoRaDevice(new DevAddr(0x1111), new DevEui(0x2222), ConnectionManager); // Will be around 1000 - delay - 400 Assert.InRange(target.GetAvailableTimeToCheckCloudToDeviceMessage(loRaDevice), TimeSpan.FromMilliseconds(expectedMinMs), TimeSpan.FromMilliseconds(expectedMaxMs)); @@ -173,10 +174,10 @@ public void When_Device_PreferredWindow1_In_Time_For_First_Window_Should_Get_Che [InlineData(1601, 0, 0)] [InlineData(1700, 0, 0)] [InlineData(3000, 0, 0)] - public void When_Device_PreferredWindow2_In_Time_For_First_Window_Should_Get_Check_C2D_Avaible_Time_Correctly(int delayInMs, int expectedMinMs, int expectedMaxMs) + public async Task When_Device_PreferredWindow2_In_Time_For_First_Window_Should_Get_Check_C2D_Avaible_Time_Correctly(int delayInMs, int expectedMinMs, int expectedMaxMs) { var target = new LoRaOperationTimeWatcher(RegionManager.EU868, DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMilliseconds(delayInMs))); - using var loRaDevice = new LoRaDevice(new DevAddr(0x1111), new DevEui(0x2222), ConnectionManager) + await using var loRaDevice = new LoRaDevice(new DevAddr(0x1111), new DevEui(0x2222), ConnectionManager) { PreferredWindow = ReceiveWindow2, }; @@ -194,10 +195,10 @@ public void When_Device_PreferredWindow2_In_Time_For_First_Window_Should_Get_Che [InlineData(1581, ReceiveWindow2)] [InlineData(1600, ReceiveWindow2)] [InlineData(2000, ReceiveWindow2)] - public void When_Device_Out_Of_Time_For_C2D_Receive_Should_Return_TimeSpan_Zero(int delayInMs, ReceiveWindowNumber devicePreferredReceiveWindow) + public async Task When_Device_Out_Of_Time_For_C2D_Receive_Should_Return_TimeSpan_Zero(int delayInMs, ReceiveWindowNumber devicePreferredReceiveWindow) { var target = new LoRaOperationTimeWatcher(RegionManager.EU868, DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMilliseconds(delayInMs))); - using var loRaDevice = new LoRaDevice(new DevAddr(0x1111), new DevEui(0x2222), ConnectionManager) + await using var loRaDevice = new LoRaDevice(new DevAddr(0x1111), new DevEui(0x2222), ConnectionManager) { PreferredWindow = devicePreferredReceiveWindow, }; diff --git a/Tests/Unit/NetworkServer/LoRaPayloadDecoderTest.cs b/Tests/Unit/NetworkServer/LoRaPayloadDecoderTest.cs index 88864984a6..6182cca08c 100644 --- a/Tests/Unit/NetworkServer/LoRaPayloadDecoderTest.cs +++ b/Tests/Unit/NetworkServer/LoRaPayloadDecoderTest.cs @@ -10,6 +10,8 @@ namespace LoRaWan.Tests.Unit.NetworkServer using System.Threading.Tasks; using LoRaWan.NetworkServer; using LoRaWan.Tests.Common; + using Microsoft.Azure.Devices.Common.Extensions; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json; using Xunit; @@ -58,9 +60,9 @@ public void When_Value_Is_NAN_Json_Should_Be_Quoted(object value, string expecte [InlineData("0xAE0198", "{\"value\":\"0xAE0198\"}")] public async Task When_Value_From_String_Is_Passed_Should_Try_To_Validate_As_Number(string value, string expectedJson) { - var target = new LoRaPayloadDecoder(NullLogger.Instance); + using var target = SetupLoRaPayloadDecoder(); - var result = await target.DecodeMessageAsync(new DevEui(0x12), Encoding.UTF8.GetBytes(value), FramePorts.App1, "DecoderValueSensor"); + var result = await target.Value.DecodeMessageAsync(new DevEui(0x12), Encoding.UTF8.GetBytes(value), FramePorts.App1, "DecoderValueSensor"); var json = JsonConvert.SerializeObject(result.GetDecodedPayload()); Assert.Equal(expectedJson, json); } @@ -68,9 +70,9 @@ public async Task When_Value_From_String_Is_Passed_Should_Try_To_Validate_As_Num [Fact] public async Task When_Payload_Is_Null_DecoderValueSensor_Should_Return_Empty() { - var target = new LoRaPayloadDecoder(NullLogger.Instance); + using var target = SetupLoRaPayloadDecoder(); - var result = await target.DecodeMessageAsync(new DevEui(0x12), null, FramePorts.App1, "DecoderValueSensor"); + var result = await target.Value.DecodeMessageAsync(new DevEui(0x12), null, FramePorts.App1, "DecoderValueSensor"); var json = JsonConvert.SerializeObject(result.GetDecodedPayload()); Assert.Equal("{\"value\":\"\"}", json); } @@ -78,9 +80,9 @@ public async Task When_Payload_Is_Null_DecoderValueSensor_Should_Return_Empty() [Fact] public async Task When_Payload_Is_Empty_DecoderValueSensor_Should_Return_Empty() { - var target = new LoRaPayloadDecoder(NullLogger.Instance); + using var target = SetupLoRaPayloadDecoder(); - var result = await target.DecodeMessageAsync(new DevEui(0x12), Array.Empty(), FramePorts.App1, "DecoderValueSensor"); + var result = await target.Value.DecodeMessageAsync(new DevEui(0x12), Array.Empty(), FramePorts.App1, "DecoderValueSensor"); var json = JsonConvert.SerializeObject(result.GetDecodedPayload()); Assert.Equal("{\"value\":\"\"}", json); } @@ -88,9 +90,9 @@ public async Task When_Payload_Is_Empty_DecoderValueSensor_Should_Return_Empty() [Fact] public async Task When_Payload_Is_Null_DecoderHexSensor_Should_Return_Empty() { - var target = new LoRaPayloadDecoder(NullLogger.Instance); + using var target = SetupLoRaPayloadDecoder(); - var result = await target.DecodeMessageAsync(new DevEui(0x12), null, FramePorts.App1, "DecoderHexSensor"); + var result = await target.Value.DecodeMessageAsync(new DevEui(0x12), null, FramePorts.App1, "DecoderHexSensor"); var json = JsonConvert.SerializeObject(result.GetDecodedPayload()); Assert.Equal("{\"value\":\"\"}", json); } @@ -98,9 +100,9 @@ public async Task When_Payload_Is_Null_DecoderHexSensor_Should_Return_Empty() [Fact] public async Task When_Payload_Is_Empty_DecoderHexSensor_Should_Return_Empty() { - var target = new LoRaPayloadDecoder(NullLogger.Instance); + using var target = SetupLoRaPayloadDecoder(); - var result = await target.DecodeMessageAsync(new DevEui(0x12), Array.Empty(), FramePorts.App1, "DecoderHexSensor"); + var result = await target.Value.DecodeMessageAsync(new DevEui(0x12), Array.Empty(), FramePorts.App1, "DecoderHexSensor"); var json = JsonConvert.SerializeObject(result.GetDecodedPayload()); Assert.Equal("{\"value\":\"\"}", json); } @@ -125,8 +127,8 @@ public async Task When_Payload_Is_Null_ExternalDecoder_Should_Be_Called_With_Emp }; }); - using var httpClient = new HttpClient(httpMessageHandler); - var target = new LoRaPayloadDecoder(httpClient); + using var httpClientFactory = new MockHttpClientFactory(httpMessageHandler); + var target = SetupLoRaPayloadDecoder(httpClientFactory); var result = await target.DecodeMessageAsync(devEui, null, fport, "http://test/decoder"); var json = JsonConvert.SerializeObject(result.GetDecodedPayload()); Assert.Equal(decodedValue, json); @@ -152,8 +154,8 @@ public async Task When_Payload_Is_Empty_ExternalDecoder_Should_Be_Called_With_Em }; }); - using var httpClient = new HttpClient(httpMessageHandler); - var target = new LoRaPayloadDecoder(httpClient); + using var httpClientFactory = new MockHttpClientFactory(httpMessageHandler); + var target = SetupLoRaPayloadDecoder(httpClientFactory); var result = await target.DecodeMessageAsync(devEui, Array.Empty(), fport, "http://test/decoder"); var json = JsonConvert.SerializeObject(result.GetDecodedPayload()); Assert.Equal(decodedValue, json); @@ -174,8 +176,8 @@ public async Task When_Decoder_Is_DecoderValueSensor_Return_In_Value(string deco { var payload = Encoding.UTF8.GetBytes(payloadString); - var target = new LoRaPayloadDecoder(NullLogger.Instance); - var actual = await target.DecodeMessageAsync(new DevEui(0x12), payload, fport, decoder); + using var target = SetupLoRaPayloadDecoder(); + var actual = await target.Value.DecodeMessageAsync(new DevEui(0x12), payload, fport, decoder); Assert.IsType(actual.Value); var decodedPayloadValue = (DecodedPayloadValue)actual.Value; Assert.Equal(expectedValue, decodedPayloadValue.Value); @@ -196,8 +198,8 @@ public async Task When_Decoder_Is_DecoderHexSensor_Return_In_Value(string decode { var payload = Encoding.UTF8.GetBytes(payloadString); - var target = new LoRaPayloadDecoder(NullLogger.Instance); - var actual = await target.DecodeMessageAsync(new DevEui(0x12), payload, fport, decoder); + using var target = SetupLoRaPayloadDecoder(); + var actual = await target.Value.DecodeMessageAsync(new DevEui(0x12), payload, fport, decoder); Assert.IsType(actual.Value); var decodedPayloadValue = (DecodedPayloadValue)actual.Value; Assert.Equal(expectedValue, decodedPayloadValue.Value); @@ -220,9 +222,35 @@ public async Task When_Decoder_Is_Undefined_Return_In_Value(string decoder, stri { var payload = Encoding.UTF8.GetBytes(payloadString); - var target = new LoRaPayloadDecoder(NullLogger.Instance); - var actual = await target.DecodeMessageAsync(new DevEui(0x12), payload, fport, decoder); + using var target = SetupLoRaPayloadDecoder(); + var actual = await target.Value.DecodeMessageAsync(new DevEui(0x12), payload, fport, decoder); Assert.NotNull(actual.Error); } + + [Fact] + public void AddPayloadDecoderHttpClient_Sets_Default_Headers() + { + // arrange + var services = new ServiceCollection(); + + // act + var result = services.AddPayloadDecoderHttpClient(); + + // assert + var client = result.BuildServiceProvider().GetRequiredService().CreateClient(PayloadDecoderHttpClient.ClientName); + Assert.Equal("Keep-Alive", client.DefaultRequestHeaders.GetFirstValueOrNull("Connection")); + Assert.Equal("timeout=86400", client.DefaultRequestHeaders.GetFirstValueOrNull("Keep-Alive")); + } + + private static DisposableValue SetupLoRaPayloadDecoder() + { +#pragma warning disable CA2000 // Dispose objects before losing scope (disposed as part of DisposableValue) + var httpClientFactory = new MockHttpClientFactory(); +#pragma warning restore CA2000 // Dispose objects before losing scope + return new DisposableValue(SetupLoRaPayloadDecoder(httpClientFactory), httpClientFactory); + } + + private static LoRaPayloadDecoder SetupLoRaPayloadDecoder(IHttpClientFactory httpClientFactory) => + new LoRaPayloadDecoder(httpClientFactory, NullLogger.Instance); } } diff --git a/Tests/Unit/Logger/IoTHubLoggerTests.cs b/Tests/Unit/NetworkServer/Logger/IoTHubLoggerTests.cs similarity index 78% rename from Tests/Unit/Logger/IoTHubLoggerTests.cs rename to Tests/Unit/NetworkServer/Logger/IoTHubLoggerTests.cs index 50634d49ef..3d61484911 100644 --- a/Tests/Unit/Logger/IoTHubLoggerTests.cs +++ b/Tests/Unit/NetworkServer/Logger/IoTHubLoggerTests.cs @@ -3,13 +3,14 @@ #nullable enable -namespace LoRaWan.Tests.Unit.Logger +namespace LoRaWan.Tests.Unit.NetworkServer.Logger { using System; using System.Threading; using System.Threading.Tasks; - using global::Logger; + using global::LoRaTools; using LoRaWan.NetworkServer; + using LoRaWan.NetworkServer.Logger; using LoRaWan.Tests.Common; using Microsoft.Azure.Devices.Client; using Microsoft.Extensions.Logging; @@ -116,19 +117,51 @@ public async Task Error_During_Module_Client_Initialization_Disables_Logger() Assert.False(logger.IsEnabled(logLevel)); } + [Fact] + public async Task Traces_Iot_Hub_Send_Events() + { + // arrange + var tracing = new Mock(); + const LogLevel logLevel = LogLevel.Information; + using var testableLogger = SetupProviderAndLogger(new LoRaLoggerConfiguration { LogLevel = logLevel }, tracing: tracing.Object); + + // act + testableLogger.Value.Object.Log(logLevel, "foo"); + + // assert + await tracing.RetryVerifyAsync(t => t.TrackIotHubDependency("SDK SendEvent", "log"), Times.Once); + } + + [Fact] + public void Does_Not_Trace_If_Log_Level_Disabled() + { + // arrange + var tracing = new Mock(); + using var testableLogger = SetupProviderAndLogger(new LoRaLoggerConfiguration { LogLevel = LogLevel.Error }, tracing: tracing.Object); + + // act + testableLogger.Value.Object.Log(LogLevel.Information, "foo"); + + // assert + tracing.Verify(t => t.TrackIotHubDependency(It.IsAny(), It.IsAny()), Times.Never); + } + private static DisposableValue> SetupProviderAndLogger(Lazy>? moduleClientFactory) => SetupProviderAndLogger(new LoRaLoggerConfiguration { LogLevel = LogLevel.Trace, UseScopes = false }, moduleClientFactory); private static DisposableValue> SetupProviderAndLogger() => SetupProviderAndLogger(null); - private static DisposableValue> SetupProviderAndLogger(LoRaLoggerConfiguration configuration, Lazy>? moduleClientFactory = null) + private static DisposableValue> SetupProviderAndLogger(LoRaLoggerConfiguration configuration, + Lazy>? moduleClientFactory = null, + ITracing? tracing = null) { var optionsMonitor = new Mock>(); optionsMonitor.Setup(om => om.CurrentValue).Returns(configuration); - optionsMonitor.Setup(om => om.OnChange(It.IsAny>())).Returns(NullDisposable.Instance); + optionsMonitor.Setup(om => om.OnChange(It.IsAny>())).Returns(NoopDisposable.Instance); var mcf = moduleClientFactory ?? new Lazy>(Task.FromResult((ModuleClient)null!)); - var provider = new IotHubLoggerProvider(optionsMonitor.Object, mcf); - return new DisposableValue>(new Mock(provider, mcf), provider); + tracing ??= new NoopTracing(); + var provider = new IotHubLoggerProvider(optionsMonitor.Object, mcf, tracing); + return new DisposableValue>(new Mock(provider, mcf, tracing), provider); } private static async Task VerifyMessageAsync(Mock logger, string message) diff --git a/Tests/Unit/Logger/LoRaConsoleLoggerTests.cs b/Tests/Unit/NetworkServer/Logger/LoRaConsoleLoggerTests.cs similarity index 98% rename from Tests/Unit/Logger/LoRaConsoleLoggerTests.cs rename to Tests/Unit/NetworkServer/Logger/LoRaConsoleLoggerTests.cs index 520009452d..b910f86258 100644 --- a/Tests/Unit/Logger/LoRaConsoleLoggerTests.cs +++ b/Tests/Unit/NetworkServer/Logger/LoRaConsoleLoggerTests.cs @@ -1,11 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace LoRaWan.Tests.Unit.Logger +namespace LoRaWan.Tests.Unit.NetworkServer.Logger { using System; - using global::Logger; + using global::LoRaTools; using LoRaWan; + using LoRaWan.NetworkServer.Logger; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; diff --git a/Tests/Unit/Logger/LoggerConfigurationMonitorTests.cs b/Tests/Unit/NetworkServer/Logger/LoggerConfigurationMonitorTests.cs similarity index 93% rename from Tests/Unit/Logger/LoggerConfigurationMonitorTests.cs rename to Tests/Unit/NetworkServer/Logger/LoggerConfigurationMonitorTests.cs index 57e9916f7e..657f4f6f77 100644 --- a/Tests/Unit/Logger/LoggerConfigurationMonitorTests.cs +++ b/Tests/Unit/NetworkServer/Logger/LoggerConfigurationMonitorTests.cs @@ -3,11 +3,11 @@ #nullable enable -namespace LoRaWan.Tests.Unit.Logger +namespace LoRaWan.Tests.Unit.NetworkServer.Logger { using System; - using global::Logger; - using LoRaWan.NetworkServer; + using global::LoRaTools; + using LoRaWan.NetworkServer.Logger; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; @@ -41,7 +41,7 @@ public void Monitor_Update_Synchronizes_Configuration() optionsMonitorMock.Setup(om => om.CurrentValue).Returns(configuration); optionsMonitorMock.Setup(om => om.OnChange(It.IsAny>())) .Callback((Action c) => actualOnChangeCallback = c) - .Returns(NullDisposable.Instance); + .Returns(NoopDisposable.Instance); // act using var loggerMonitor = new LoggerConfigurationMonitor(optionsMonitorMock.Object); @@ -73,7 +73,7 @@ private static IOptionsMonitor CreateOptionsMonitor(LoR { var optionsMonitorMock = new Mock>(); optionsMonitorMock.Setup(om => om.CurrentValue).Returns(configuration); - optionsMonitorMock.Setup(om => om.OnChange(It.IsAny>())).Returns(onChangeToken ?? NullDisposable.Instance); + optionsMonitorMock.Setup(om => om.OnChange(It.IsAny>())).Returns(onChangeToken ?? NoopDisposable.Instance); return optionsMonitorMock.Object; } } diff --git a/Tests/Unit/NetworkServer/MessageProcessorJoinTest.cs b/Tests/Unit/NetworkServer/MessageProcessorJoinTest.cs index dd872c542c..1b953eb3d5 100644 --- a/Tests/Unit/NetworkServer/MessageProcessorJoinTest.cs +++ b/Tests/Unit/NetworkServer/MessageProcessorJoinTest.cs @@ -39,7 +39,9 @@ public async Task When_Device_Is_Not_Found_In_Api_Should_Return_Null() .ReturnsAsync(() => null); // Send to message processor - using var messageProcessor = new MessageDispatcher( + using var cache = NewMemoryCache(); + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, loRaDeviceRegistryMock.Object, FrameCounterUpdateStrategyProvider); @@ -51,8 +53,8 @@ public async Task When_Device_Is_Not_Found_In_Api_Should_Return_Null() loRaDeviceRegistryMock.VerifyAll(); - loRaDeviceRegistryMock.Setup(dr => dr.Dispose()); - LoRaDeviceClient.Setup(ldc => ldc.Dispose()); + loRaDeviceRegistryMock.Setup(dr => dr.DisposeAsync()).Returns(ValueTask.CompletedTask); + LoRaDeviceClient.Setup(ldc => ldc.DisposeAsync()).Returns(ValueTask.CompletedTask); } [Fact] @@ -78,13 +80,14 @@ public async Task When_Device_Is_Found_In_Api_Should_Update_Twin_And_Return() .ReturnsAsync(true); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(simulatedDevice.CreateOTAATwin()); + .ReturnsAsync(LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetOtaaDesiredTwinProperties())); using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -124,7 +127,7 @@ public async Task When_Api_Takes_Too_Long_Should_Return_Null() var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateOTAADevice(1, gatewayID: ServerConfiguration.GatewayID)); var joinRequest = simulatedDevice.CreateJoinRequest(); - using var loRaDevice = CreateLoRaDevice(simulatedDevice); + await using var loRaDevice = CreateLoRaDevice(simulatedDevice); loRaDevice.SetFcntDown(10); loRaDevice.SetFcntUp(20); @@ -136,7 +139,9 @@ public async Task When_Api_Takes_Too_Long_Should_Return_Null() .ReturnsAsync(loRaDevice); // Send to message processor - using var messageProcessor = new MessageDispatcher( + using var cache = NewMemoryCache(); + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, loRaDeviceRegistryMock.Object, FrameCounterUpdateStrategyProvider); @@ -157,7 +162,7 @@ public async Task When_Api_Takes_Too_Long_Should_Return_Null() loRaDeviceRegistryMock.VerifyAll(); LoRaDeviceApi.VerifyAll(); - LoRaDeviceClient.Setup(ldc => ldc.Dispose()); + LoRaDeviceClient.Setup(ldc => ldc.DisposeAsync()).Returns(ValueTask.CompletedTask); } [Fact] @@ -168,7 +173,7 @@ public async Task When_Mic_Check_Fails_Join_Process_Should_Fail() var joinRequest = simulatedDevice.CreateJoinRequest(wrongAppKey); - using var loRaDevice = CreateLoRaDevice(simulatedDevice); + await using var loRaDevice = CreateLoRaDevice(simulatedDevice); loRaDevice.SetFcntDown(10); loRaDevice.SetFcntUp(20); @@ -180,7 +185,9 @@ public async Task When_Mic_Check_Fails_Join_Process_Should_Fail() .ReturnsAsync(() => loRaDevice); // Send to message processor - using var messageProcessor = new MessageDispatcher( + using var cache = NewMemoryCache(); + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, loRaDeviceRegistryMock.Object, FrameCounterUpdateStrategyProvider); @@ -199,8 +206,8 @@ public async Task When_Mic_Check_Fails_Join_Process_Should_Fail() LoRaDeviceApi.VerifyAll(); loRaDeviceRegistryMock.VerifyAll(); - loRaDeviceRegistryMock.Setup(dr => dr.Dispose()); - LoRaDeviceClient.Setup(ldc => ldc.Dispose()); + loRaDeviceRegistryMock.Setup(dr => dr.DisposeAsync()).Returns(ValueTask.CompletedTask);; + LoRaDeviceClient.Setup(ldc => ldc.DisposeAsync()).Returns(ValueTask.CompletedTask);; } [Fact] @@ -213,8 +220,8 @@ public async Task When_Device_AppEUI_Does_Not_Match_Should_Return_Null() simulatedDevice.LoRaDevice.AppEui = new JoinEui(0xFFFFFFFFFFFFFFFF); - using var connectionManager = new SingleDeviceConnectionManager(LoRaDeviceClient.Object); - using var loRaDevice = TestUtils.CreateFromSimulatedDevice(simulatedDevice, connectionManager); + await using var connectionManager = new SingleDeviceConnectionManager(LoRaDeviceClient.Object); + await using var loRaDevice = TestUtils.CreateFromSimulatedDevice(simulatedDevice, connectionManager); loRaDevice.SetFcntDown(10); loRaDevice.SetFcntUp(20); @@ -224,7 +231,9 @@ public async Task When_Device_AppEUI_Does_Not_Match_Should_Return_Null() .ReturnsAsync(() => loRaDevice); // Send to message processor - using var messageProcessor = new MessageDispatcher( + using var cache = NewMemoryCache(); + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, loRaDeviceRegistryMock.Object, FrameCounterUpdateStrategyProvider); @@ -242,8 +251,8 @@ public async Task When_Device_AppEUI_Does_Not_Match_Should_Return_Null() LoRaDeviceApi.VerifyAll(); loRaDeviceRegistryMock.VerifyAll(); - loRaDeviceRegistryMock.Setup(dr => dr.Dispose()); - LoRaDeviceClient.Setup(ldc => ldc.Dispose()); + loRaDeviceRegistryMock.Setup(dr => dr.DisposeAsync()).Returns(ValueTask.CompletedTask); + LoRaDeviceClient.Setup(ldc => ldc.DisposeAsync()).Returns(ValueTask.CompletedTask); } [Fact] @@ -254,7 +263,7 @@ public async Task When_Device_Has_Different_Gateway_Should_Return_Null() var devEui = simulatedDevice.LoRaDevice.DevEui; - using var loRaDevice = CreateLoRaDevice(simulatedDevice); + await using var loRaDevice = CreateLoRaDevice(simulatedDevice); loRaDevice.IsOurDevice = false; loRaDevice.SetFcntDown(10); loRaDevice.SetFcntUp(20); @@ -265,7 +274,9 @@ public async Task When_Device_Has_Different_Gateway_Should_Return_Null() .ReturnsAsync(loRaDevice); // Send to message processor - using var messageProcessor = new MessageDispatcher( + using var cache = NewMemoryCache(); + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, loRaDeviceRegistryMock.Object, FrameCounterUpdateStrategyProvider); @@ -294,12 +305,7 @@ public async Task When_Getting_Device_Information_From_Twin_Returns_JoinAccept(s var devEui = simulatedDevice.LoRaDevice.DevEui; // Device twin will be queried - var twin = new Twin(); - twin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - twin.Properties.Desired[TwinProperty.AppEui] = simulatedDevice.LoRaDevice.AppEui?.ToString(); - twin.Properties.Desired[TwinProperty.AppKey] = simulatedDevice.LoRaDevice.AppKey?.ToString(); - if (deviceGatewayID != null) twin.Properties.Desired[TwinProperty.GatewayID] = deviceGatewayID; - twin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; + var twin = LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetOtaaDesiredTwinProperties()); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(twin); // Device twin will be updated @@ -320,10 +326,11 @@ public async Task When_Getting_Device_Information_From_Twin_Returns_JoinAccept(s .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEui, "aabb").AsList())); using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -368,10 +375,11 @@ public async Task When_Api_Returns_DevAlreadyUsed_Should_Return_Null() .ReturnsAsync(new SearchDevicesResult() { IsDevNonceAlreadyUsed = true }); using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -399,14 +407,11 @@ public async Task When_Getting_DLSettings_From_Twin_Returns_JoinAccept_With_Corr var devEui = simulatedDevice.LoRaDevice.DevEui; // Device twin will be queried - var twin = new Twin(); - twin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - twin.Properties.Desired[TwinProperty.AppEui] = simulatedDevice.LoRaDevice.AppEui?.ToString(); - twin.Properties.Desired[TwinProperty.AppKey] = simulatedDevice.LoRaDevice.AppKey?.ToString(); - twin.Properties.Desired[TwinProperty.GatewayID] = deviceGatewayID; - twin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; - twin.Properties.Desired[TwinProperty.RX1DROffset] = rx1DROffset; - twin.Properties.Desired[TwinProperty.RX2DataRate] = rx2datarate; + var twin = LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetOtaaDesiredTwinProperties() with + { + Rx1DROffset = rx1DROffset, + Rx2DataRate = rx2datarate + }); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(twin); @@ -418,10 +423,11 @@ public async Task When_Getting_DLSettings_From_Twin_Returns_JoinAccept_With_Corr .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEui, "aabb").AsList())); using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -473,14 +479,11 @@ public async Task When_Getting_Custom_RX2_DR_From_Twin_Returns_JoinAccept_With_C .ReturnsAsync((Message)null); // Device twin will be queried - var twin = new Twin(); - twin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - twin.Properties.Desired[TwinProperty.AppEui] = simulatedDevice.LoRaDevice.AppEui?.ToString(); - twin.Properties.Desired[TwinProperty.AppKey] = simulatedDevice.LoRaDevice.AppKey?.ToString(); - twin.Properties.Desired[TwinProperty.GatewayID] = deviceGatewayID; - twin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; - twin.Properties.Desired[TwinProperty.RX2DataRate] = rx2datarate; - twin.Properties.Desired[TwinProperty.PreferredWindow] = 2; + var twin = LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetOtaaDesiredTwinProperties() with + { + Rx2DataRate = rx2datarate, + PreferredWindow = ReceiveWindowNumber.ReceiveWindow2 + }); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(twin); @@ -500,10 +503,11 @@ public async Task When_Getting_Custom_RX2_DR_From_Twin_Returns_JoinAccept_With_C .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEui, "aabb").AsList())); using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -568,15 +572,12 @@ public async Task When_Join_With_Custom_Join_Update_Old_Desired_Properties() .ReturnsAsync((Message)null); // Device twin will be queried - var twin = new Twin(); - twin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - twin.Properties.Desired[TwinProperty.AppEui] = simulatedDevice.LoRaDevice.AppEui?.ToString(); - twin.Properties.Desired[TwinProperty.AppKey] = simulatedDevice.LoRaDevice.AppKey?.ToString(); - twin.Properties.Desired[TwinProperty.GatewayID] = deviceGatewayID; - twin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; - twin.Properties.Desired[TwinProperty.RX2DataRate] = afterJoinValues; - twin.Properties.Desired[TwinProperty.RX1DROffset] = afterJoinValues; - twin.Properties.Desired[TwinProperty.RXDelay] = afterJoinValues; + var twin = LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetOtaaDesiredTwinProperties() with + { + Rx2DataRate = (DataRateIndex)afterJoinValues, + Rx1DROffset = afterJoinValues, + RxDelay = afterJoinValues + }); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(twin); LoRaDeviceClient.Setup(x => x.UpdateReportedPropertiesAsync(It.IsNotNull(), It.IsAny())) @@ -605,10 +606,11 @@ public async Task When_Join_With_Custom_Join_Update_Old_Desired_Properties() await LoRaDeviceClient.Object.UpdateReportedPropertiesAsync(startingTwin, default); using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -637,16 +639,16 @@ public async Task When_Join_With_Custom_Join_Update_Old_Desired_Properties() [Theory] // Base dr is 2 // In case the offset is invalid, we rollback to offset 0. for europe that means = to upstream - [InlineData(0, DR2)] - [InlineData(1, DR1)] - [InlineData(2, DR0)] - [InlineData(3, DR0)] - [InlineData(4, DR0)] - [InlineData(6, DR2)] - [InlineData(12, DR2)] - [InlineData(-2, DR2)] - public async Task When_Getting_RX1_Offset_From_Twin_Returns_JoinAccept_With_Correct_Settings_And_Behaves_Correctly(int rx1offset, DataRateIndex expectedDR) + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(6)] + [InlineData(-2)] + public async Task When_Getting_RX1_Offset_From_Twin_Returns_JoinAccept_With_Correct_Settings_And_Behaves_Correctly(int rx1offset) { + var expectedDR = DR2; var deviceGatewayID = ServerGatewayID; var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateOTAADevice(1, gatewayID: deviceGatewayID)); var joinRequest = simulatedDevice.CreateJoinRequest(); @@ -672,14 +674,11 @@ public async Task When_Getting_RX1_Offset_From_Twin_Returns_JoinAccept_With_Corr .ReturnsAsync((Message)null); // Device twin will be queried - var twin = new Twin(); - twin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - twin.Properties.Desired[TwinProperty.AppEui] = simulatedDevice.LoRaDevice.AppEui?.ToString(); - twin.Properties.Desired[TwinProperty.AppKey] = simulatedDevice.LoRaDevice.AppKey?.ToString(); - twin.Properties.Desired[TwinProperty.GatewayID] = deviceGatewayID; - twin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; - twin.Properties.Desired[TwinProperty.RX1DROffset] = rx1offset; - twin.Properties.Desired[TwinProperty.PreferredWindow] = 1; + var twin = LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetOtaaDesiredTwinProperties() with + { + Rx1DROffset = rx1offset, + PreferredWindow = ReceiveWindowNumber.ReceiveWindow1 + }); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(twin); @@ -699,10 +698,11 @@ public async Task When_Getting_RX1_Offset_From_Twin_Returns_JoinAccept_With_Corr .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEui, "aabb").AsList())); using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -785,14 +785,11 @@ public async Task When_Getting_RXDelay_Offset_From_Twin_Returns_JoinAccept_With_ .ReturnsAsync((Message)null); // Device twin will be queried - var twin = new Twin(); - twin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - twin.Properties.Desired[TwinProperty.AppEui] = simulatedDevice.LoRaDevice.AppEui?.ToString(); - twin.Properties.Desired[TwinProperty.AppKey] = simulatedDevice.LoRaDevice.AppKey?.ToString(); - twin.Properties.Desired[TwinProperty.GatewayID] = deviceGatewayID; - twin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; - twin.Properties.Desired[TwinProperty.RXDelay] = rxDelay; - twin.Properties.Desired[TwinProperty.PreferredWindow] = 1; + var twin = LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetOtaaDesiredTwinProperties() with + { + RxDelay = rxDelay, + PreferredWindow = ReceiveWindowNumber.ReceiveWindow1 + }); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(twin); @@ -812,10 +809,11 @@ public async Task When_Getting_RXDelay_Offset_From_Twin_Returns_JoinAccept_With_ .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEui, "aabb").AsList())); using var memoryCache = new MemoryCache(new MemoryCacheOptions()); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, memoryCache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + memoryCache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); diff --git a/Tests/Unit/NetworkServer/MessageProcessorMultipleGatewayTest.cs b/Tests/Unit/NetworkServer/MessageProcessorMultipleGatewayTest.cs index 47f07490b6..4d9e2e0ab2 100644 --- a/Tests/Unit/NetworkServer/MessageProcessorMultipleGatewayTest.cs +++ b/Tests/Unit/NetworkServer/MessageProcessorMultipleGatewayTest.cs @@ -42,24 +42,26 @@ public async Task Multi_OTAA_Unconfirmed_Message_Should_Send_Data_To_IotHub_Upda .ReturnsAsync((Message)null) .ReturnsAsync((Message)null); - using var loRaDevice1 = CreateLoRaDevice(simulatedDevice); - using var connectionManager2 = new SingleDeviceConnectionManager(SecondLoRaDeviceClient.Object); - using var loRaDevice2 = TestUtils.CreateFromSimulatedDevice(simulatedDevice, connectionManager2, SecondRequestHandlerImplementation); + await using var loRaDevice1 = CreateLoRaDevice(simulatedDevice); + await using var connectionManager2 = new SingleDeviceConnectionManager(SecondLoRaDeviceClient.Object); + await using var loRaDevice2 = TestUtils.CreateFromSimulatedDevice(simulatedDevice, connectionManager2, SecondRequestHandlerImplementation); using var cache1 = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loRaDevice1); - using var loRaDeviceRegistry1 = new LoRaDeviceRegistry(ServerConfiguration, cache1, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loRaDevice1); + await using var loRaDeviceRegistry1 = new LoRaDeviceRegistry(ServerConfiguration, cache1, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); using var cache2 = EmptyMemoryCache(); - using var loraDeviceCache2 = CreateDeviceCache(loRaDevice2); - using var loRaDeviceRegistry2 = new LoRaDeviceRegistry(ServerConfiguration, cache2, SecondLoRaDeviceApi.Object, SecondLoRaDeviceFactory, loraDeviceCache2); + await using var loraDeviceCache2 = CreateDeviceCache(loRaDevice2); + await using var loRaDeviceRegistry2 = new LoRaDeviceRegistry(ServerConfiguration, cache2, SecondLoRaDeviceApi.Object, SecondLoRaDeviceFactory, loraDeviceCache2); // Send to message processor - using var messageProcessor1 = new MessageDispatcher( + await using var messageProcessor1 = TestMessageDispatcher.Create( + cache1, ServerConfiguration, loRaDeviceRegistry1, FrameCounterUpdateStrategyProvider); - using var messageProcessor2 = new MessageDispatcher( + await using var messageProcessor2 = TestMessageDispatcher.Create( + cache2, SecondServerConfiguration, loRaDeviceRegistry2, SecondFrameCounterUpdateStrategyProvider); @@ -96,8 +98,8 @@ public async Task Multi_OTAA_Unconfirmed_Message_Should_Send_Data_To_IotHub_Upda Assert.Equal(1U, loRaDevice2.FCntUp); // the following setup is required after VerifyAll() is called - LoRaDeviceClient.Setup(ldc => ldc.Dispose()); - SecondLoRaDeviceClient.Setup(ldc => ldc.Dispose()); + LoRaDeviceClient.Setup(ldc => ldc.DisposeAsync()).Returns(ValueTask.CompletedTask); + SecondLoRaDeviceClient.Setup(ldc => ldc.DisposeAsync()).Returns(ValueTask.CompletedTask); } } } diff --git a/Tests/Unit/NetworkServer/MessageProcessorSingleGatewayTest.cs b/Tests/Unit/NetworkServer/MessageProcessorSingleGatewayTest.cs index 059519077f..7367c19e33 100644 --- a/Tests/Unit/NetworkServer/MessageProcessorSingleGatewayTest.cs +++ b/Tests/Unit/NetworkServer/MessageProcessorSingleGatewayTest.cs @@ -6,7 +6,6 @@ namespace LoRaWan.Tests.Unit.NetworkServer using System; using System.Collections.Generic; using System.Linq; - using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using global::LoRaTools.LoRaMessage; @@ -47,10 +46,11 @@ public async Task Unknown_Device_Should_Not_Send_Messages(int searchDevicesDelay } using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -88,13 +88,14 @@ public async Task When_Payload_Has_Invalid_Mic_Should_Not_Send_Messages(int sear } LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(TestUtils.CreateABPTwin(simulatedDevice)); + .ReturnsAsync(simulatedDevice.GetDefaultAbpTwin()); using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -128,17 +129,18 @@ public async Task When_Payload_Has_Invalid_Mic_Should_Not_Send_Messages(int sear } [Fact] - public void Unknown_Region_Should_Return_Null() + public async Task Unknown_Region_Should_Return_Null() { // Setup var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1)); var payload = simulatedDevice.CreateUnconfirmedDataUpMessage("1234"); using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -165,11 +167,12 @@ public async Task ABP_Unconfirmed_Message_Should_Send_Data_To_IotHub_Update_Fcnt .ReturnsAsync((Message)null); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -192,7 +195,7 @@ public async Task ABP_Unconfirmed_Message_Should_Send_Data_To_IotHub_Update_Fcnt } [Fact] - public void When_Faulty_MAC_Message_Is_Received_Processing_Abort_Without_Infinite_Loop() + public async Task When_Faulty_MAC_Message_Is_Received_Processing_Abort_Without_Infinite_Loop() { var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, gatewayID: ServerConfiguration.GatewayID)) { @@ -208,11 +211,12 @@ public void When_Faulty_MAC_Message_Is_Received_Processing_Abort_Without_Infinit .ReturnsAsync((Message)null); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -258,11 +262,12 @@ public async Task OTAA_Confirmed_Message_Should_Send_Data_To_IotHub_Update_FcntU .ReturnsAsync(true); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -318,11 +323,12 @@ public async Task OTAA_Unconfirmed_Message_With_Fcnt_Change_Of_10_Should_Send_Da .ReturnsAsync((Message)null); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -382,11 +388,12 @@ public async Task When_ABP_Device_With_Relaxed_FrameCounter_Has_FCntUP_Zero_Or_O .ReturnsAsync(true); using var cache = EmptyMemoryCache(); - using var loraDeviceCache = CreateDeviceCache(loraDevice); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); + await using var loraDeviceCache = CreateDeviceCache(loraDevice); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, loraDeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -422,10 +429,11 @@ public async Task ABP_From_Another_Gateway_Unconfirmed_Message_Should_Load_Devic .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(simulatedDevice.DevAddr, simulatedDevice.DevEUI, "1234") { GatewayId = gateway2 }.AsList())); using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -476,17 +484,18 @@ public async Task When_New_ABP_Device_Instance_Is_Created_Should_Increment_FCntD // - send event // - receive c2d LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)) - .ReturnsAsync(simulatedDevice.CreateABPTwin()); + .ReturnsAsync(simulatedDevice.GetDefaultAbpTwin()); LoRaDeviceClient.Setup(x => x.SendEventAsync(It.IsNotNull(), null)) .ReturnsAsync(true); LoRaDeviceClient.Setup(x => x.ReceiveAsync(It.IsNotNull())) .ReturnsAsync((Message)null); using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageProcessor = new MessageDispatcher( + await using var messageProcessor = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); @@ -525,7 +534,7 @@ public async Task When_New_ABP_Device_Instance_Is_Created_Should_Increment_FCntD uint? deviceTwinFcntUp, uint? deviceTwinFcntDown) { - var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1)); + var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, gatewayID: ServerConfiguration.GatewayID)); var devEui = simulatedDevice.LoRaDevice.DevEui; var devAddr = simulatedDevice.LoRaDevice.DevAddr.Value; @@ -541,19 +550,8 @@ public async Task When_New_ABP_Device_Instance_Is_Created_Should_Increment_FCntD .ReturnsAsync((Message)null); // twin will be loaded - var initialTwin = new Twin(); - initialTwin.Properties.Desired[TwinProperty.DevEUI] = devEui.ToString(); - initialTwin.Properties.Desired[TwinProperty.AppEui] = simulatedDevice.LoRaDevice.AppEui?.ToString(); - initialTwin.Properties.Desired[TwinProperty.AppKey] = simulatedDevice.LoRaDevice.AppKey?.ToString(); - initialTwin.Properties.Desired[TwinProperty.NwkSKey] = simulatedDevice.LoRaDevice.NwkSKey?.ToString(); - initialTwin.Properties.Desired[TwinProperty.AppSKey] = simulatedDevice.LoRaDevice.AppSKey?.ToString(); - initialTwin.Properties.Desired[TwinProperty.DevAddr] = devAddr.ToString(); - initialTwin.Properties.Desired[TwinProperty.GatewayID] = ServerConfiguration.GatewayID; - initialTwin.Properties.Desired[TwinProperty.SensorDecoder] = simulatedDevice.LoRaDevice.SensorDecoder; - if (deviceTwinFcntDown.HasValue) - initialTwin.Properties.Reported[TwinProperty.FCntDown] = deviceTwinFcntDown.Value; - if (deviceTwinFcntUp.HasValue) - initialTwin.Properties.Reported[TwinProperty.FCntUp] = deviceTwinFcntUp.Value; + var initialTwin = LoRaDeviceTwin.Create(simulatedDevice.LoRaDevice.GetAbpDesiredTwinProperties() with { DevEui = devEui }, + new LoRaReportedTwinProperties { FCntDown = deviceTwinFcntDown, FCntUp = deviceTwinFcntUp }); LoRaDeviceClient.Setup(x => x.GetTwinAsync(CancellationToken.None)).ReturnsAsync(initialTwin); @@ -579,10 +577,11 @@ public async Task When_New_ABP_Device_Instance_Is_Created_Should_Increment_FCntD .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEui, "abc").AsList())); using var cache = NewMemoryCache(); - using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); + await using var deviceRegistry = new LoRaDeviceRegistry(ServerConfiguration, cache, LoRaDeviceApi.Object, LoRaDeviceFactory, DeviceCache); // Send to message processor - using var messageDispatcher = new MessageDispatcher( + await using var messageDispatcher = TestMessageDispatcher.Create( + cache, ServerConfiguration, deviceRegistry, FrameCounterUpdateStrategyProvider); diff --git a/Tests/Unit/NetworkServer/MetricRegistryTests.cs b/Tests/Unit/NetworkServer/MetricRegistryTests.cs index f12912cd8e..22c54e6d26 100644 --- a/Tests/Unit/NetworkServer/MetricRegistryTests.cs +++ b/Tests/Unit/NetworkServer/MetricRegistryTests.cs @@ -10,6 +10,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer using System.Diagnostics.Metrics; using System.Linq; using LoRaWan.NetworkServer; + using LoRaWan.Tests.Common; using Moq; using Xunit; @@ -140,13 +141,14 @@ public void GetTagsInOrder_Throws_When_Tag_Not_Found() Assert.Equal(LoRaProcessingErrorCode.TagNotSet, result.ErrorCode); } - public static object[][] Tag_Value_Is_Not_In_Tag_Names() => new[] - { - new object[] { Array.Empty(), KeyValuePair.Create("foo", (object?)"bar") }, - new object[] { new[] { "foo" }, KeyValuePair.Create("foo", (object?)"bar"), KeyValuePair.Create("baz", (object?)"bar") }, - new object[] { new[] { MetricRegistry.GatewayIdTagName }, KeyValuePair.Create("foo", (object?)"bar") }, - new object[] { new[] { MetricRegistry.GatewayIdTagName, "foo" }, KeyValuePair.Create("foo", (object?)"bar"), KeyValuePair.Create("baz", (object?)"bar") } - }; + public static TheoryData[]> Tag_Value_Is_Not_In_Tag_Names() => + TheoryDataFactory.From(new[] + { + (Array.Empty(), new[] { KeyValuePair.Create("foo", (object?)"bar") }), + (new[] { "foo" }, new[] { KeyValuePair.Create("foo", (object?)"bar"), KeyValuePair.Create("baz", (object?)"bar") }), + (new[] { MetricRegistry.GatewayIdTagName }, new[] { KeyValuePair.Create("foo", (object?)"bar") }), + (new[] { MetricRegistry.GatewayIdTagName, "foo" }, new[] { KeyValuePair.Create("foo", (object?)"bar"), KeyValuePair.Create("baz", (object?)"bar") }) + }); [Theory] [MemberData(nameof(Tag_Value_Is_Not_In_Tag_Names))] diff --git a/Tests/Unit/NetworkServer/ModuleConnectionHostTest.cs b/Tests/Unit/NetworkServer/ModuleConnectionHostTest.cs index 7bf1d9e0ed..e8dd73f2a4 100644 --- a/Tests/Unit/NetworkServer/ModuleConnectionHostTest.cs +++ b/Tests/Unit/NetworkServer/ModuleConnectionHostTest.cs @@ -4,67 +4,66 @@ namespace LoRaWan.Tests.Unit.NetworkServer { using Bogus; + using global::LoRaTools; using LoRaWan.NetworkServer; using LoRaWan.NetworkServer.BasicsStation.ModuleConnection; using LoRaWan.Tests.Common; + using Microsoft.Azure.Devices.Client; using Microsoft.Azure.Devices.Client.Exceptions; using Microsoft.Azure.Devices.Shared; using Microsoft.Extensions.Logging.Abstractions; using Moq; using System; using System.Configuration; - using System.Net; + using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Xunit; - public class ModuleConnectionHostTest + public sealed class ModuleConnectionHostTest : IAsyncDisposable { - + private readonly NetworkServerConfiguration networkServerConfiguration; private readonly Mock loRaModuleClientFactory = new(); private readonly Mock loRaModuleClient = new(); private readonly LoRaDeviceAPIServiceBase loRaDeviceApiServiceBase = Mock.Of(); private readonly Faker faker = new Faker(); + private readonly Mock lnsRemoteCall; + private readonly ModuleConnectionHost subject; public ModuleConnectionHostTest() { + this.networkServerConfiguration = new NetworkServerConfiguration(); this.loRaModuleClient.Setup(x => x.DisposeAsync()); this.loRaModuleClientFactory.Setup(x => x.CreateAsync()).ReturnsAsync(loRaModuleClient.Object); + this.lnsRemoteCall = new Mock(); + this.subject = new ModuleConnectionHost(this.networkServerConfiguration, + this.loRaModuleClientFactory.Object, + this.loRaDeviceApiServiceBase, + this.lnsRemoteCall.Object, + NullLogger.Instance, + TestMeter.Instance); } [Fact] public void When_Constructor_Receives_Null_Parameters_Should_Throw() { - var networkServerConfiguration = new NetworkServerConfiguration(); - var classCMessageSender = Mock.Of(); - var loRaDeviceRegistry = Mock.Of(); - var loRaModuleClientFactory = Mock.Of(); - // ASSERT ArgumentNullException ex; - ex = Assert.Throws(() => new ModuleConnectionHost(null, classCMessageSender, loRaModuleClientFactory, loRaDeviceRegistry, loRaDeviceApiServiceBase, NullLogger.Instance, TestMeter.Instance)); + ex = Assert.Throws(() => new ModuleConnectionHost(null, this.loRaModuleClientFactory.Object, this.loRaDeviceApiServiceBase, this.lnsRemoteCall.Object, NullLogger.Instance, TestMeter.Instance)); Assert.Equal("networkServerConfiguration", ex.ParamName); - ex = Assert.Throws(() => new ModuleConnectionHost(networkServerConfiguration, null, loRaModuleClientFactory, loRaDeviceRegistry, loRaDeviceApiServiceBase, NullLogger.Instance, TestMeter.Instance)); - Assert.Equal("defaultClassCDevicesMessageSender", ex.ParamName); - ex = Assert.Throws(() => new ModuleConnectionHost(networkServerConfiguration, classCMessageSender, null, loRaDeviceRegistry, loRaDeviceApiServiceBase, NullLogger.Instance, TestMeter.Instance)); + ex = Assert.Throws(() => new ModuleConnectionHost(networkServerConfiguration, null, this.loRaDeviceApiServiceBase, this.lnsRemoteCall.Object, NullLogger.Instance, TestMeter.Instance)); Assert.Equal("loRaModuleClientFactory", ex.ParamName); - ex = Assert.Throws(() => new ModuleConnectionHost(networkServerConfiguration, classCMessageSender, loRaModuleClientFactory, null, loRaDeviceApiServiceBase, NullLogger.Instance, TestMeter.Instance)); - Assert.Equal("loRaDeviceRegistry", ex.ParamName); - ex = Assert.Throws(() => new ModuleConnectionHost(networkServerConfiguration, classCMessageSender, loRaModuleClientFactory, loRaDeviceRegistry, null, NullLogger.Instance, TestMeter.Instance)); + ex = Assert.Throws(() => new ModuleConnectionHost(networkServerConfiguration, this.loRaModuleClientFactory.Object, null, this.lnsRemoteCall.Object, NullLogger.Instance, TestMeter.Instance)); Assert.Equal("loRaDeviceAPIService", ex.ParamName); + ex = Assert.Throws(() => new ModuleConnectionHost(networkServerConfiguration, this.loRaModuleClientFactory.Object, this.loRaDeviceApiServiceBase, null, NullLogger.Instance, TestMeter.Instance)); + Assert.Equal("lnsRemoteCallHandler", ex.ParamName); } [Fact] - public async Task On_Desired_Properties_Correct_Update_Should_Update() + public async Task On_Desired_Properties_Correct_Update_Should_Update_Api_Service_Configuration() { - var networkServerConfiguration = Mock.Of(); - var classCMessageSender = Mock.Of(); - var loRaDeviceRegistry = Mock.Of(); - var loRaModuleClientFactory = Mock.Of(); - - await using var moduleClient = new ModuleConnectionHost(networkServerConfiguration, classCMessageSender, loRaModuleClientFactory, loRaDeviceRegistry, loRaDeviceApiServiceBase, NullLogger.Instance, TestMeter.Instance); var url1 = this.faker.Internet.Url(); var authCode = this.faker.Internet.Password(); @@ -74,7 +73,7 @@ public async Task On_Desired_Properties_Correct_Update_Should_Update() FacadeAuthCode = authCode, }); - await moduleClient.OnDesiredPropertiesUpdate(new TwinCollection(input), null); + await this.subject.OnDesiredPropertiesUpdate(new TwinCollection(input), null); Assert.Equal(url1 + "/", loRaDeviceApiServiceBase.URL.ToString()); var url2 = this.faker.Internet.Url(); var input2 = JsonSerializer.Serialize(new @@ -82,17 +81,16 @@ public async Task On_Desired_Properties_Correct_Update_Should_Update() FacadeServerUrl = url2, FacadeAuthCode = authCode, }); - await moduleClient.OnDesiredPropertiesUpdate(new TwinCollection(input2), null); + await this.subject.OnDesiredPropertiesUpdate(new TwinCollection(input2), null); Assert.Equal(url2 + "/", loRaDeviceApiServiceBase.URL.ToString()); Assert.Equal(authCode, loRaDeviceApiServiceBase.AuthCode.ToString()); - } [Theory] [InlineData("{ FacadeServerUrl: 'url2', FacadeAuthCode: 'authCode' }")]// not a url [InlineData("{ FacadeAuthCode: 'authCode' }")] // no Url [InlineData("{ FacadeServerUrl: '', FacadeAuthCode: 'authCode' }")]// empty url - public async Task On_Desired_Properties_Incorrect_Update_Should_Not_Update(string twinUpdate) + public async Task On_Desired_Properties_Incorrect_Update_Should_Not_Update_Api_Service_Configuration(string twinUpdate) { var facadeUri = faker.Internet.Url(); var facadeCode = faker.Internet.Password(); @@ -102,29 +100,54 @@ public async Task On_Desired_Properties_Incorrect_Update_Should_Not_Update(strin FacadeAuthCode = facadeCode }; - var localLoRaDeviceApiServiceBase = new LoRaDeviceAPIService(networkServerConfiguration, Mock.Of(), NullLogger.Instance, TestMeter.Instance); - var classCMessageSender = Mock.Of(); - var loRaDeviceRegistry = Mock.Of(); - var loRaModuleClientFactory = Mock.Of(); - - await using var moduleClientFactory = new ModuleConnectionHost(networkServerConfiguration, classCMessageSender, loRaModuleClientFactory, loRaDeviceRegistry, localLoRaDeviceApiServiceBase, NullLogger.Instance, TestMeter.Instance); + var localLoRaDeviceApiServiceBase = new LoRaDeviceAPIService(networkServerConfiguration, Mock.Of(), NullLogger.Instance, TestMeter.Instance); + await using var moduleClientFactory = new ModuleConnectionHost(networkServerConfiguration, this.loRaModuleClientFactory.Object, localLoRaDeviceApiServiceBase, this.lnsRemoteCall.Object, NullLogger.Instance, TestMeter.Instance); await moduleClientFactory.OnDesiredPropertiesUpdate(new TwinCollection(twinUpdate), null); Assert.Equal(facadeUri + "/", localLoRaDeviceApiServiceBase.URL.ToString()); Assert.Equal(facadeCode, localLoRaDeviceApiServiceBase.AuthCode); } + [Theory] + [InlineData(0)] + [InlineData(5)] + [InlineData(400)] + [InlineData(1000)] + public async Task On_Desired_Properties_Correct_Update_Should_Update_Processing_Delay(int processingDelay) + { + Assert.Equal(LoRaWan.NetworkServer.Constants.DefaultProcessingDelayInMilliseconds, this.networkServerConfiguration.ProcessingDelayInMilliseconds); + + var input = JsonSerializer.Serialize(new + { + ProcessingDelayInMilliseconds = processingDelay, + }); + + await this.subject.OnDesiredPropertiesUpdate(new TwinCollection(input), null); + Assert.Equal(processingDelay, networkServerConfiguration.ProcessingDelayInMilliseconds); + } + + [Theory] + [InlineData("{ ProcessingDelayInMilliseconds: -400 }")] + [InlineData("{ ProcessingDelayInMilliseconds: '' }")] + [InlineData("{ ProcessingDelay: 200 }")] + public async Task On_Desired_Properties_Incorrect_Update_Should_Not_Update_Processing_Delay(string twinUpdate) + { + await this.subject.OnDesiredPropertiesUpdate(new TwinCollection(twinUpdate), null); + Assert.Equal(LoRaWan.NetworkServer.Constants.DefaultProcessingDelayInMilliseconds, this.networkServerConfiguration.ProcessingDelayInMilliseconds); + } + [Fact] public async Task InitModuleAsync_Update_Should_Perform_Happy_Path() { - var networkServerConfiguration = new NetworkServerConfiguration(); - var classCMessageSender = new Mock(MockBehavior.Strict); - var loRaDeviceRegistry = new Mock(MockBehavior.Strict); + var timeotNetworkServerConfiguration = new NetworkServerConfiguration() + { + // Change the iot edge timeout. + IoTEdgeTimeout = 5 + }; - // Change the iot edge timeout. - networkServerConfiguration.IoTEdgeTimeout = 5; var facadeUri = this.faker.Internet.Url(); var facadeCode = this.faker.Internet.Password(); + var processingDelay = 1000; var twinProperty = new TwinProperties { Desired = new TwinCollection( @@ -132,15 +155,17 @@ public async Task InitModuleAsync_Update_Should_Perform_Happy_Path() { FacadeServerUrl = facadeUri, FacadeAuthCode = facadeCode, + ProcessingDelayInMilliseconds = processingDelay })) }; loRaModuleClient.Setup(x => x.GetTwinAsync(It.IsAny())).ReturnsAsync(new Twin(twinProperty)); - await using var moduleClient = new ModuleConnectionHost(networkServerConfiguration, classCMessageSender.Object, this.loRaModuleClientFactory.Object, loRaDeviceRegistry.Object, loRaDeviceApiServiceBase, NullLogger.Instance, TestMeter.Instance); + await using var moduleClient = new ModuleConnectionHost(timeotNetworkServerConfiguration, this.loRaModuleClientFactory.Object, loRaDeviceApiServiceBase, this.lnsRemoteCall.Object, NullLogger.Instance, TestMeter.Instance); await moduleClient.CreateAsync(CancellationToken.None); Assert.Equal(facadeUri + "/", loRaDeviceApiServiceBase.URL.ToString()); Assert.Equal(facadeCode, loRaDeviceApiServiceBase.AuthCode); + Assert.Equal(processingDelay, timeotNetworkServerConfiguration.ProcessingDelayInMilliseconds); } [Theory] @@ -148,9 +173,6 @@ public async Task InitModuleAsync_Update_Should_Perform_Happy_Path() [InlineData("{ FacadeAuthCode: 'asdasdada' }")] public async Task InitModuleAsync_Fails_When_Required_Twins_Are_Not_Set(string twin) { - var networkServerConfiguration = new NetworkServerConfiguration(); - var classCMessageSender = new Mock(MockBehavior.Strict); - var loRaDeviceRegistry = new Mock(MockBehavior.Strict); var loRaModuleClient = new Mock(); loRaModuleClient.Setup(x => x.DisposeAsync()); var loRaModuleClientFactory = new Mock(); @@ -163,130 +185,89 @@ public async Task InitModuleAsync_Fails_When_Required_Twins_Are_Not_Set(string t }; loRaModuleClient.Setup(x => x.GetTwinAsync(It.IsAny())).ReturnsAsync(new Twin(twinProperty)); - await using var moduleClient = new ModuleConnectionHost(networkServerConfiguration, classCMessageSender.Object, this.loRaModuleClientFactory.Object, loRaDeviceRegistry.Object, loRaDeviceApiServiceBase, NullLogger.Instance, TestMeter.Instance); + await using var moduleClient = new ModuleConnectionHost(this.networkServerConfiguration, this.loRaModuleClientFactory.Object, loRaDeviceApiServiceBase, this.lnsRemoteCall.Object, NullLogger.Instance, TestMeter.Instance); await Assert.ThrowsAsync(() => moduleClient.CreateAsync(CancellationToken.None)); } - [Fact] - public async Task InitModuleAsync_Fails_When_Fail_IoT_Hub_Communication() - { - var networkServerConfiguration = new NetworkServerConfiguration(); - var classCMessageSender = new Mock(MockBehavior.Strict); - var loRaDeviceRegistry = new Mock(MockBehavior.Strict); - - // Change the iot edge timeout. - networkServerConfiguration.IoTEdgeTimeout = 5; - - loRaModuleClient.Setup(x => x.GetTwinAsync(It.IsAny())).Throws(); - - await using var moduleClient = new ModuleConnectionHost(networkServerConfiguration, classCMessageSender.Object, this.loRaModuleClientFactory.Object, loRaDeviceRegistry.Object, loRaDeviceApiServiceBase, NullLogger.Instance, TestMeter.Instance); - var ex = await Assert.ThrowsAsync(() => moduleClient.CreateAsync(CancellationToken.None)); - Assert.Equal(LoRaProcessingErrorCode.TwinFetchFailed, ex.ErrorCode); - } - - [Fact] - public async Task OnDirectMethodCall_ClearCache_When_Correct_Should_Work() + [Theory] + [InlineData("")] + [InlineData("500 ms")] + [InlineData("-200")] + [InlineData("invalidDelay")] + public async Task InitModuleAsync_Does_Not_Fail_When_Processing_Delay_Missing_Or_Incorrect(string processingDelay) { - var networkServerConfiguration = new NetworkServerConfiguration(); - var classCMessageSender = new Mock(MockBehavior.Strict); - var loRaDeviceRegistry = new Mock(MockBehavior.Strict); - loRaDeviceRegistry.Setup(x => x.ResetDeviceCache()); + var facadeUri = this.faker.Internet.Url(); + var facadeCode = this.faker.Internet.Password(); + var twinProperty = new TwinProperties + { + Desired = new TwinCollection( + JsonSerializer.Serialize(new + { + FacadeServerUrl = facadeUri, + FacadeAuthCode = facadeCode, + ProcessingDelayInMilliseconds = processingDelay + })) + }; - // Change the iot edge timeout. - networkServerConfiguration.IoTEdgeTimeout = 5; + this.loRaModuleClient.Setup(x => x.GetTwinAsync(It.IsAny())).ReturnsAsync(new Twin(twinProperty)); - await using var moduleClient = new ModuleConnectionHost(networkServerConfiguration, classCMessageSender.Object, this.loRaModuleClientFactory.Object, loRaDeviceRegistry.Object, loRaDeviceApiServiceBase, NullLogger.Instance, TestMeter.Instance); - await moduleClient.OnDirectMethodCalled(new Microsoft.Azure.Devices.Client.MethodRequest(Constants.CloudToDeviceClearCache), null); - loRaDeviceRegistry.VerifyAll(); + await this.subject.CreateAsync(CancellationToken.None); + Assert.Equal(LoRaWan.NetworkServer.Constants.DefaultProcessingDelayInMilliseconds, this.networkServerConfiguration.ProcessingDelayInMilliseconds); } [Fact] - public async Task OnDirectMethodCall_CloudToDeviceDecoderElementName_When_Correct_Should_Work() + public async Task InitModuleAsync_Fails_When_Fail_IoT_Hub_Communication() { - var networkServerConfiguration = new NetworkServerConfiguration(); - var classCMessageSender = new Mock(MockBehavior.Strict); - classCMessageSender.Setup(x => x.SendAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); - var loRaDeviceRegistry = new Mock(MockBehavior.Strict); - // Change the iot edge timeout. - networkServerConfiguration.IoTEdgeTimeout = 5; - - await using var moduleClient = new ModuleConnectionHost(networkServerConfiguration, classCMessageSender.Object, this.loRaModuleClientFactory.Object, loRaDeviceRegistry.Object, loRaDeviceApiServiceBase, NullLogger.Instance, TestMeter.Instance); + this.networkServerConfiguration.IoTEdgeTimeout = 5; - var c2d = "{\"test\":\"asd\"}"; + this.loRaModuleClient.Setup(x => x.GetTwinAsync(It.IsAny())).Throws(); - var response = await moduleClient.OnDirectMethodCalled(new Microsoft.Azure.Devices.Client.MethodRequest(Constants.CloudToDeviceDecoderElementName, Encoding.UTF8.GetBytes(c2d), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)), null); - Assert.Equal((int)HttpStatusCode.OK, response.Status); + var ex = await Assert.ThrowsAsync(() => this.subject.CreateAsync(CancellationToken.None)); + Assert.Equal(LoRaProcessingErrorCode.TwinFetchFailed, ex.ErrorCode); } [Fact] - public async Task OnDirectMethodCall_When_Null_Or_Empty_MethodName_Should_Throw() + public async Task OnDirectMethodCall_Should_Invoke_ClearCache() { - var networkServerConfiguration = new NetworkServerConfiguration(); - var classCMessageSender = new Mock(MockBehavior.Strict); - classCMessageSender.Setup(x => x.SendAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); - var loRaDeviceRegistry = new Mock(MockBehavior.Strict); - - // Change the iot edge timeout. - networkServerConfiguration.IoTEdgeTimeout = 5; - - await using var moduleClient = new ModuleConnectionHost(networkServerConfiguration, classCMessageSender.Object, this.loRaModuleClientFactory.Object, loRaDeviceRegistry.Object, loRaDeviceApiServiceBase, NullLogger.Instance, TestMeter.Instance); - - await Assert.ThrowsAnyAsync(async () => await moduleClient.OnDirectMethodCalled(null, null)); + await this.subject.OnDirectMethodCalled(new MethodRequest(LoRaWan.NetworkServer.Constants.CloudToDeviceClearCache), null); + this.lnsRemoteCall.Verify(l => l.ExecuteAsync(new LnsRemoteCall(RemoteCallKind.ClearCache, null), CancellationToken.None), Times.Once); } [Fact] - public async Task OnDirectMethodCall_CloudToDeviceDecoderElementName_When_Incorrect_Should_Return_NotFound() + public async Task OnDirectMethodCall_Should_Invoke_DropConnection() { - var networkServerConfiguration = new NetworkServerConfiguration(); - var classCMessageSender = new Mock(MockBehavior.Strict); - classCMessageSender.Setup(x => x.SendAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); - var loRaDeviceRegistry = new Mock(MockBehavior.Strict); - - // Change the iot edge timeout. - networkServerConfiguration.IoTEdgeTimeout = 5; + // arrange + var json = @"{""foo"":""bar""}"; + var methodRequest = new MethodRequest(LoRaWan.NetworkServer.Constants.CloudToDeviceCloseConnection, Encoding.UTF8.GetBytes(json)); - await using var moduleClient = new ModuleConnectionHost(networkServerConfiguration, classCMessageSender.Object, this.loRaModuleClientFactory.Object, loRaDeviceRegistry.Object, loRaDeviceApiServiceBase, NullLogger.Instance, TestMeter.Instance); - var c2d = "{\"test\":\"asd\"}"; + // act + await this.subject.OnDirectMethodCalled(methodRequest, null); - var response = await moduleClient.OnDirectMethodCalled(new Microsoft.Azure.Devices.Client.MethodRequest(this.faker.Random.String2(8), Encoding.UTF8.GetBytes(c2d)), null); - Assert.Equal((int)HttpStatusCode.BadRequest, response.Status); + // assert + this.lnsRemoteCall.Verify(l => l.ExecuteAsync(new LnsRemoteCall(RemoteCallKind.CloseConnection, json), CancellationToken.None), Times.Once); } [Fact] - public async Task SendCloudToDeviceMessageAsync_When_ClassC_Msg_Is_Null_Or_Empty_Should_Return_Not_Found() + public async Task OnDirectMethodCall_Should_Invoke_SendCloudToDeviceMessageAsync() { - var networkServerConfiguration = new NetworkServerConfiguration(); - var classCMessageSender = new Mock(MockBehavior.Strict); - classCMessageSender.Setup(x => x.SendAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); - var loRaDeviceRegistry = new Mock(MockBehavior.Strict); - var loRaModuleClient = new Mock(); - loRaModuleClient.Setup(x => x.DisposeAsync()); - // Change the iot edge timeout. - networkServerConfiguration.IoTEdgeTimeout = 5; - await using var moduleClient = new ModuleConnectionHost(networkServerConfiguration, classCMessageSender.Object, this.loRaModuleClientFactory.Object, loRaDeviceRegistry.Object, loRaDeviceApiServiceBase, NullLogger.Instance, TestMeter.Instance); + // arrange + var json = @"{""foo"":""bar""}"; + var methodRequest = new MethodRequest(LoRaWan.NetworkServer.Constants.CloudToDeviceDecoderElementName, Encoding.UTF8.GetBytes(json)); - var response = await moduleClient.OnDirectMethodCalled(new Microsoft.Azure.Devices.Client.MethodRequest(Constants.CloudToDeviceDecoderElementName, null), null); - Assert.Equal((int)HttpStatusCode.BadRequest, response.Status); + // act + await this.subject.OnDirectMethodCalled(methodRequest, null); - var response2 = await moduleClient.OnDirectMethodCalled(new Microsoft.Azure.Devices.Client.MethodRequest(Constants.CloudToDeviceDecoderElementName, Array.Empty()), null); - Assert.Equal((int)HttpStatusCode.BadRequest, response2.Status); + // assert + this.lnsRemoteCall.Verify(l => l.ExecuteAsync(new LnsRemoteCall(RemoteCallKind.CloudToDeviceMessage, json), CancellationToken.None), Times.Once); } [Fact] - public async Task SendCloudToDeviceMessageAsync_When_ClassC_Msg_Is_Not_CorrectJson_Should_Return_Not_Found() + public async Task OnDirectMethodCall_When_Null_Or_Empty_MethodName_Should_Throw() { - var networkServerConfiguration = new NetworkServerConfiguration(); - var classCMessageSender = new Mock(MockBehavior.Strict); - classCMessageSender.Setup(x => x.SendAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); - var loRaDeviceRegistry = new Mock(MockBehavior.Strict); - - // Change the iot edge timeout. - networkServerConfiguration.IoTEdgeTimeout = 5; - await using var moduleClient = new ModuleConnectionHost(networkServerConfiguration, classCMessageSender.Object, this.loRaModuleClientFactory.Object, loRaDeviceRegistry.Object, loRaDeviceApiServiceBase, NullLogger.Instance, TestMeter.Instance); - - var response = await moduleClient.OnDirectMethodCalled(new Microsoft.Azure.Devices.Client.MethodRequest(Constants.CloudToDeviceDecoderElementName, Encoding.UTF8.GetBytes(faker.Random.String2(10))), null); - Assert.Equal((int)HttpStatusCode.BadRequest, response.Status); + await Assert.ThrowsAnyAsync(async () => await this.subject.OnDirectMethodCalled(null, null)); } + + public async ValueTask DisposeAsync() => await this.subject.DisposeAsync(); } } diff --git a/Tests/Unit/NetworkServer/MultiGatewayFrameCounterUpdateStrategyTest.cs b/Tests/Unit/NetworkServer/MultiGatewayFrameCounterUpdateStrategyTest.cs index d8d5dd6b8c..2ae4597ea6 100644 --- a/Tests/Unit/NetworkServer/MultiGatewayFrameCounterUpdateStrategyTest.cs +++ b/Tests/Unit/NetworkServer/MultiGatewayFrameCounterUpdateStrategyTest.cs @@ -13,7 +13,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer using Moq; using Xunit; - public sealed class MultiGatewayFrameCounterUpdateStrategyTest : IDisposable + public sealed class MultiGatewayFrameCounterUpdateStrategyTest : IAsyncDisposable { private readonly Mock deviceClient; private readonly Mock deviceApi; @@ -30,10 +30,10 @@ public MultiGatewayFrameCounterUpdateStrategyTest() this.device = new LoRaDevice(new DevAddr(1), new DevEui(2), connectionManager); } - public void Dispose() + public async ValueTask DisposeAsync() { - this.device.Dispose(); - this.connectionManager.Dispose(); + await this.device.DisposeAsync(); + await this.connectionManager.DisposeAsync(); } [Theory] diff --git a/Tests/Unit/NetworkServer/PreferredGatewayResultTests.cs b/Tests/Unit/NetworkServer/PreferredGatewayResultTests.cs new file mode 100644 index 0000000000..ba3cdcbc45 --- /dev/null +++ b/Tests/Unit/NetworkServer/PreferredGatewayResultTests.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Unit.NetworkServer +{ + using Newtonsoft.Json; + using Xunit; + + public sealed class PreferredGatewayResultTests + { + [Fact] + public void Can_Deserialize() + { + // arrange + var input = new global::LoraKeysManagerFacade.PreferredGatewayResult(12, new global::LoraKeysManagerFacade.LoRaDevicePreferredGateway("gateway", 13)); + + // act + var result = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(input)); + + // assert + Assert.Equal(input.RequestFcntUp, result!.RequestFcntUp); + Assert.Equal(input.PreferredGatewayID, result.PreferredGatewayID); + Assert.Equal(input.Conflict, result.Conflict); + Assert.Equal(input.CurrentFcntUp, result.CurrentFcntUp); + Assert.Equal(input.ErrorMessage, result.ErrorMessage); + } + } +} diff --git a/Tests/Unit/NetworkServer/ProcessingDelayTest.cs b/Tests/Unit/NetworkServer/ProcessingDelayTest.cs new file mode 100644 index 0000000000..2f93ded15f --- /dev/null +++ b/Tests/Unit/NetworkServer/ProcessingDelayTest.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tests.Unit.NetworkServer +{ + using LoRaWan.Tests.Common; + using Xunit; + using Xunit.Abstractions; + + public class ProcessingDelayTest : MessageProcessorTestBase + { + public ProcessingDelayTest(ITestOutputHelper testOutputHelper) : + base(testOutputHelper) + { } + + [Theory] + [InlineData(null, true)] + [InlineData(400, true)] + [InlineData(1500, true)] + [InlineData(0, false)] + [InlineData(-100, false)] + [InlineData(-1000, false)] + public void IsProcessingDelayEnbled(int? processingDelay, bool processingDelayEnabled) + { + if (processingDelay is { } delay) + ServerConfiguration.ProcessingDelayInMilliseconds = delay; + + Assert.Equal(processingDelayEnabled, RequestHandlerImplementation.IsProcessingDelayEnabled()); + } + } +} diff --git a/Tests/Unit/NetworkServer/SingleGatewayFrameCounterUpdateStrategyTest.cs b/Tests/Unit/NetworkServer/SingleGatewayFrameCounterUpdateStrategyTest.cs index a02f4de043..7f929533db 100644 --- a/Tests/Unit/NetworkServer/SingleGatewayFrameCounterUpdateStrategyTest.cs +++ b/Tests/Unit/NetworkServer/SingleGatewayFrameCounterUpdateStrategyTest.cs @@ -13,7 +13,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer using Moq; using Xunit; - public sealed class SingleGatewayFrameCounterUpdateStrategyTest : IDisposable + public sealed class SingleGatewayFrameCounterUpdateStrategyTest : IAsyncDisposable { private readonly Mock deviceClient; private readonly ILoRaDeviceClientConnectionManager connectionManager; @@ -26,10 +26,10 @@ public SingleGatewayFrameCounterUpdateStrategyTest() this.device = new LoRaDevice(new DevAddr(1), new DevEui(2), connectionManager); } - public void Dispose() + public async ValueTask DisposeAsync() { - this.device.Dispose(); - this.connectionManager.Dispose(); + await this.device.DisposeAsync(); + await this.connectionManager.DisposeAsync(); } [Theory] diff --git a/Tests/Unit/NetworkServer/TaskExtensionTests.cs b/Tests/Unit/NetworkServer/TaskExtensionTests.cs new file mode 100644 index 0000000000..87a69afd02 --- /dev/null +++ b/Tests/Unit/NetworkServer/TaskExtensionTests.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Unit.NetworkServer +{ + using System; + using System.Linq; + using System.Threading.Tasks; + using LoRaWan.NetworkServer; + using LoRaWan.Tests.Common; + using Xunit; + + public sealed class TaskExtensionTests + { + [Fact] + public void GetExceptions_When_Task_Is_Not_Completed_Throws() + { + var tcs = new TaskCompletionSource(); + var ex = Assert.Throws(() => new[] { tcs.Task }.GetExceptions()); + Assert.Equal("tasks", ex.ParamName); + Assert.Contains("All tasks must have completed.", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + public static TheoryData GetExceptions_Success_TheoryData() => TheoryDataFactory.From(new[] + { + (new[] { Task.FromException(new InvalidOperationException("A")), Task.FromException(new LoRaProcessingException("B")) }, + new Exception[] { new InvalidOperationException("A"), new LoRaProcessingException("B") }), + (new[] { Task.CompletedTask, Task.Run(async () => { await Task.Yield(); throw new InvalidOperationException("A"); }) }, + new Exception[] { new InvalidOperationException("A") }), + (new[] { Task.FromException(new AggregateException(new InvalidOperationException("A"))), Task.FromException(new LoRaProcessingException("B")) }, + new Exception[] { new AggregateException(new InvalidOperationException("A")), new LoRaProcessingException("B") }), + (new[] { Task.FromException(new OperationCanceledException("A")), Task.CompletedTask }, + new Exception[] { new OperationCanceledException("A") }), + (new[] { Task.FromException(new OperationCanceledException("A")), Task.FromException(new InvalidOperationException("B")) }, + new Exception[] { new OperationCanceledException("A"), new InvalidOperationException("B") }), + (new[] { Task.Run(() => + { + var tcs = new TaskCompletionSource(); + tcs.SetException(new Exception[] { new OperationCanceledException("A"), new InvalidOperationException("B") }); + return tcs.Task; + }) }, + new Exception[] { new AggregateException(new OperationCanceledException("A"), new InvalidOperationException("B")) }) + }); + + [Theory] + [MemberData(nameof(GetExceptions_Success_TheoryData))] + public async Task GetExceptions_Success(Task[] tasks, Exception[] expectedExceptions) + { + // arrange + _ = await Task.WhenAny(Task.WhenAll(tasks)); + + // act + var result = tasks.GetExceptions(); + + // assert + Assert.Equal(expectedExceptions.Select(e => e.GetType()), result.Select(e => e.GetType())); + Assert.Equal(expectedExceptions.Select(e => e.Message), result.Select(e => e.Message)); + } + + [Fact] + public void TryGetCanceledException_Returns_Null_If_Not_Canceled() + { + var tcs = new TaskCompletionSource(); + Assert.False(tcs.Task.TryGetCanceledException(out var ex)); + Assert.Null(ex); + } + + public static TheoryData TryGetCanceledException_Returns_Exception_TheoryData() => TheoryDataFactory.From(new Exception[] + { + new OperationCanceledException("A"), + new TaskCanceledException("B") + }); + + [Theory] + [MemberData(nameof(TryGetCanceledException_Returns_Exception_TheoryData))] + public async Task TryGetCanceledException_Returns_Exception(Exception exception) + { + // arrange + var t = Task.Run(() => throw exception); + _ = await Task.WhenAny(t); + + // act + assert + Assert.True(t.TryGetCanceledException(out var result)); + Assert.IsType(exception.GetType(), result); + Assert.Equal(exception.Message, result!.Message); + } + } +} diff --git a/Tests/Unit/NetworkServerDiscovery/TagBasedLnsDiscoveryTests.cs b/Tests/Unit/NetworkServerDiscovery/TagBasedLnsDiscoveryTests.cs new file mode 100644 index 0000000000..a4b37c145c --- /dev/null +++ b/Tests/Unit/NetworkServerDiscovery/TagBasedLnsDiscoveryTests.cs @@ -0,0 +1,230 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace LoRaWan.Tests.Unit.NetworkServerDiscovery +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + using global::LoRaTools; + using global::LoRaTools.IoTHubImpl; + using LoRaWan.NetworkServerDiscovery; + using LoRaWan.Tests.Common; + using Microsoft.Azure.Devices; + using Microsoft.Azure.Devices.Shared; + using Microsoft.Extensions.Caching.Memory; + using Microsoft.Extensions.Logging.Abstractions; + using Moq; + using Xunit; + + public sealed class TagBasedLnsDiscoveryTests : IDisposable + { + private static readonly StationEui StationEui = new StationEui(1); + private static readonly string[] LnsUris = new[] { "ws://foo:5000/bar/baz", "wss://baz:5001/baz", "ws://baz" }; + + private readonly Mock registryManagerMock; + private readonly MemoryCache memoryCache; + private readonly TagBasedLnsDiscovery subject; + + public TagBasedLnsDiscoveryTests() + { + this.registryManagerMock = new Mock(); + this.registryManagerMock.Setup(rm => rm.FindLnsByNetworkId(It.IsAny())).Returns(Mock.Of>()); + this.memoryCache = new MemoryCache(new MemoryCacheOptions()); + this.subject = new TagBasedLnsDiscovery(memoryCache, this.registryManagerMock.Object, NullLogger.Instance); + } + + [Fact] + public async Task ResolveLnsAsync_Resolves_Lns_By_Tag() + { + // arrange + const string networkId = "foo"; + SetupLbsTwinResponse(StationEui, networkId); + SetupIotHubQueryResponse(networkId, LnsUris); + + // act + var result = await this.subject.ResolveLnsAsync(StationEui, CancellationToken.None); + + // assert + Assert.Contains(result, LnsUris.Select(u => new Uri(u))); + } + + [Fact] + public async Task ResolveLnsAsync_Throws_If_No_Lns_Matches() + { + // arrange + SetupLbsTwinResponse(StationEui, "firstnetwork"); + SetupIotHubQueryResponse("someothernetwork", LnsUris); + + // act + assert + var ex = await Assert.ThrowsAsync(() => this.subject.ResolveLnsAsync(StationEui, CancellationToken.None)); + Assert.Equal(LoRaProcessingErrorCode.LnsDiscoveryFailed, ex.ErrorCode); + } + + [Theory] + [InlineData("asdf123.")] + [InlineData("'asdf123'")] + [InlineData("network+")] + public async Task ResolveLnsAsync_Throws_If_NetworkId_Is_Invalid(string networkId) + { + // arrange + SetupLbsTwinResponse(StationEui, networkId); + + // act + assert + var ex = await Assert.ThrowsAsync(() => this.subject.ResolveLnsAsync(StationEui, CancellationToken.None)); + Assert.Equal(LoRaProcessingErrorCode.InvalidDeviceConfiguration, ex.ErrorCode); + } + + [Fact] + public async Task ResolveLnsAsync_Throws_If_Network_Is_Empty() + { + SetupLbsTwinResponse(StationEui, string.Empty); + _ = await Assert.ThrowsAsync(() => this.subject.ResolveLnsAsync(StationEui, CancellationToken.None)); + } + + [Fact] + public async Task ResolveLnsAsync_Uses_Round_Robin_Distribution() + { + // arrange + const int numberOfRequests = 5; + const string networkId = "foo"; + SetupLbsTwinResponse(StationEui, networkId); + SetupIotHubQueryResponse(networkId, LnsUris); + + // act + assert + for (var i = 0; i < numberOfRequests; ++i) + { + var result = await this.subject.ResolveLnsAsync(StationEui, CancellationToken.None); + Assert.Equal(new Uri(LnsUris[i % LnsUris.Length]), result); + } + } + + [Fact] + public async Task ResolveLnsAsync_Tracks_Last_Returned_Lns_By_Station() + { + // arrange + const string networkId = "foo"; + var firstStation = new StationEui(1); + var secondStation = new StationEui(2); + SetupLbsTwinResponse(firstStation, networkId); + SetupLbsTwinResponse(secondStation, networkId); + SetupIotHubQueryResponse(networkId, LnsUris); + + // act + var first = await this.subject.ResolveLnsAsync(firstStation, CancellationToken.None); + var second = await this.subject.ResolveLnsAsync(secondStation, CancellationToken.None); + + // assert + Assert.Equal(new Uri(LnsUris[0]), first); + Assert.Equal(new Uri(LnsUris[0]), second); + } + + [Fact] + public async Task ResolveLnsAsync_Caches_Lns_By_Network() + { + // arrange + const string networkId = "foo"; + var firstStation = new StationEui(1); + var secondStation = new StationEui(2); + SetupLbsTwinResponse(firstStation, networkId); + SetupLbsTwinResponse(secondStation, networkId); + SetupIotHubQueryResponse(networkId, LnsUris); + + // act + _ = await this.subject.ResolveLnsAsync(firstStation, CancellationToken.None); + _ = await this.subject.ResolveLnsAsync(secondStation, CancellationToken.None); + + // assert + this.registryManagerMock.Verify(rm => rm.FindLnsByNetworkId(It.IsAny()), Times.Once); + } + + [Fact] + public async Task ResolveLnsAsync_Throws_If_Twin_Not_Found() + { + var ex = await Assert.ThrowsAsync(() => this.subject.ResolveLnsAsync(StationEui, CancellationToken.None)); + Assert.Equal(LoRaProcessingErrorCode.TwinFetchFailed, ex.ErrorCode); + } + + public static TheoryData Erroneous_Host_Address_TheoryData() => TheoryDataFactory.From(new[] + { + null, "", "http://mylns:5000", "htt://mylns:5000", "ws:/mylns:5000" + }); + + [Theory] + [MemberData(nameof(Erroneous_Host_Address_TheoryData))] + public async Task ResolveLnsAsync_Is_Resilient_Against_Erroneous_Host_Address(string? hostAddress) + { + // arrange + const string networkId = "foo"; + SetupLbsTwinResponse(StationEui, networkId); + SetupIotHubQueryResponse(networkId, LnsUris.Concat(new[] { hostAddress }).ToList()); + + // act + var result = await this.subject.ResolveLnsAsync(StationEui, CancellationToken.None); + + // assert + Assert.Contains(result, LnsUris.Select(u => new Uri(u))); + } + + [Theory] + [MemberData(nameof(Erroneous_Host_Address_TheoryData))] + public async Task ResolveLnsAsync_Throws_if_Only_Lns_Is_Misconfigured(string? hostAddress) + { + // arrange + const string networkId = "foo"; + SetupLbsTwinResponse(StationEui, networkId); + SetupIotHubQueryResponse(networkId, new[] { hostAddress }); + + // act + assert + var ex = await Assert.ThrowsAsync(() => this.subject.ResolveLnsAsync(StationEui, CancellationToken.None)); + Assert.Equal(LoRaProcessingErrorCode.LnsDiscoveryFailed, ex.ErrorCode); + } + + [Fact] + public async Task ResolveLnsAsync_Caches_Station_Network() + { + // arrange + const string networkId = "foo"; + SetupLbsTwinResponse(StationEui, networkId); + SetupIotHubQueryResponse(networkId, LnsUris); + + // act + _ = await this.subject.ResolveLnsAsync(StationEui, CancellationToken.None); + _ = await this.subject.ResolveLnsAsync(StationEui, CancellationToken.None); + + // assert + this.registryManagerMock.Verify(rm => rm.GetTwinAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + private void SetupLbsTwinResponse(StationEui stationEui, string networkId) + { + this.registryManagerMock + .Setup(rm => rm.GetTwinAsync(stationEui.ToString(), It.IsAny())) + .ReturnsAsync(new IoTHubDeviceTwin(new Twin { Tags = new TwinCollection(@$"{{""network"":""{networkId}""}}") })); + } + + private void SetupIotHubQueryResponse(string networkId, IList hostAddresses) + { + var queryMock = new Mock>(); + var i = 0; + queryMock.Setup(q => q.HasMoreResults).Returns(() => i++ % 2 == 0); + queryMock.Setup(q => q.GetNextPageAsync()).ReturnsAsync(from ha in hostAddresses + select ha is { } someHa ? JsonSerializer.Serialize(new { hostAddress = ha, deviceId = Guid.NewGuid().ToString() }) + : JsonSerializer.Serialize(new { deviceId = Guid.NewGuid().ToString() })); + this.registryManagerMock + .Setup(rm => rm.FindLnsByNetworkId(networkId)) + .Returns(queryMock.Object); + } + + public void Dispose() + { + this.memoryCache.Dispose(); + this.subject.Dispose(); + } + } +} diff --git a/Tools/Cli-LoRa-Device-Provisioning/Cli-LoRa-Device-Provisioning.csproj b/Tools/Cli-LoRa-Device-Provisioning/Cli-LoRa-Device-Provisioning.csproj index 2d4b6bfede..f7ae4bd722 100644 --- a/Tools/Cli-LoRa-Device-Provisioning/Cli-LoRa-Device-Provisioning.csproj +++ b/Tools/Cli-LoRa-Device-Provisioning/Cli-LoRa-Device-Provisioning.csproj @@ -29,13 +29,13 @@ - + - + diff --git a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/AS923-1.json b/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/AS923-1.json new file mode 100644 index 0000000000..53e07d3890 --- /dev/null +++ b/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/AS923-1.json @@ -0,0 +1,128 @@ +{ + "routerConfig": { + "NetID": [ + 1 + ], + "JoinEui": [], + "region": "AS923", + "hwspec": "sx1301/1", + "freq_range": [ + 915000000, + 928000000 + ], + "DRs": [ + [ + 12, + 125, + 0 + ], + [ + 11, + 125, + 0 + ], + [ + 10, + 125, + 0 + ], + [ + 9, + 125, + 0 + ], + [ + 8, + 125, + 0 + ], + [ + 7, + 125, + 0 + ], + [ + 7, + 250, + 0 + ] + ], + "sx1301_conf": [ + { + "radio_0": { + "enable": true, + "freq": 923500000 + }, + "radio_1": { + "enable": true, + "freq": 924300000 + }, + "chan_FSK": { + "enable": true, + "radio": 1, + "if": 500000 + }, + "chan_Lora_std": { + "enable": true, + "radio": 1, + "if": 200000, + "bandwidth": 250000, + "spread_factor": 7 + }, + "chan_multiSF_0": { + "enable": true, + "radio": 1, + "if": -300000 + }, + "chan_multiSF_1": { + "enable": true, + "radio": 1, + "if": -100000 + }, + "chan_multiSF_2": { + "enable": true, + "radio": 1, + "if": 100000 + }, + "chan_multiSF_3": { + "enable": true, + "radio": 1, + "if": 300000 + }, + "chan_multiSF_4": { + "enable": true, + "radio": 0, + "if": -300000 + }, + "chan_multiSF_5": { + "enable": true, + "radio": 0, + "if": -100000 + }, + "chan_multiSF_6": { + "enable": true, + "radio": 0, + "if": 100000 + }, + "chan_multiSF_7": { + "enable": true, + "radio": 0, + "if": 300000 + } + } + ], + "nocca": true, + "nodc": true, + "nodwell": true, + "desiredTxParams": { + "downlinkDwellLimit": true, + "uplinkDwellLimit": true, + "eirp": 5 + } + }, + "desiredTxParams": { + "downlinkDwellLimit": true, + "uplinkDwellLimit": true, + "eirp": 5 + } +} diff --git a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/AS923-2.json b/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/AS923-2.json new file mode 100644 index 0000000000..cdda0f493b --- /dev/null +++ b/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/AS923-2.json @@ -0,0 +1,128 @@ +{ + "routerConfig": { + "NetID": [ + 1 + ], + "JoinEui": [], + "region": "AS923", + "hwspec": "sx1301/1", + "freq_range": [ + 915000000, + 928000000 + ], + "DRs": [ + [ + 12, + 125, + 0 + ], + [ + 11, + 125, + 0 + ], + [ + 10, + 125, + 0 + ], + [ + 9, + 125, + 0 + ], + [ + 8, + 125, + 0 + ], + [ + 7, + 125, + 0 + ], + [ + 7, + 250, + 0 + ] + ], + "sx1301_conf": [ + { + "radio_0": { + "enable": true, + "freq": 921700000 + }, + "radio_1": { + "enable": true, + "freq": 922500000 + }, + "chan_FSK": { + "enable": true, + "radio": 0, + "if": 300000 + }, + "chan_Lora_std": { + "enable": true, + "radio": 1, + "if": 200000, + "bandwidth": 250000, + "spread_factor": 7 + }, + "chan_multiSF_0": { + "enable": true, + "radio": 1, + "if": -300000 + }, + "chan_multiSF_1": { + "enable": true, + "radio": 1, + "if": -100000 + }, + "chan_multiSF_2": { + "enable": true, + "radio": 1, + "if": 100000 + }, + "chan_multiSF_3": { + "enable": true, + "radio": 1, + "if": 300000 + }, + "chan_multiSF_4": { + "enable": true, + "radio": 0, + "if": -300000 + }, + "chan_multiSF_5": { + "enable": true, + "radio": 0, + "if": -100000 + }, + "chan_multiSF_6": { + "enable": true, + "radio": 0, + "if": 100000 + }, + "chan_multiSF_7": { + "enable": true, + "radio": 0, + "if": 300000 + } + } + ], + "nocca": true, + "nodc": true, + "nodwell": true, + "desiredTxParams": { + "downlinkDwellLimit": true, + "uplinkDwellLimit": true, + "eirp": 5 + } + }, + "desiredTxParams": { + "downlinkDwellLimit": true, + "uplinkDwellLimit": true, + "eirp": 5 + } +} diff --git a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/AS923-3.json b/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/AS923-3.json new file mode 100644 index 0000000000..a139dd4196 --- /dev/null +++ b/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/AS923-3.json @@ -0,0 +1,128 @@ +{ + "routerConfig": { + "NetID": [ + 1 + ], + "JoinEui": [], + "region": "AS923", + "hwspec": "sx1301/1", + "freq_range": [ + 915000000, + 928000000 + ], + "DRs": [ + [ + 12, + 125, + 0 + ], + [ + 11, + 125, + 0 + ], + [ + 10, + 125, + 0 + ], + [ + 9, + 125, + 0 + ], + [ + 8, + 125, + 0 + ], + [ + 7, + 125, + 0 + ], + [ + 7, + 250, + 0 + ] + ], + "sx1301_conf": [ + { + "radio_0": { + "enable": true, + "freq": 916900000 + }, + "radio_1": { + "enable": true, + "freq": 917700000 + }, + "chan_FSK": { + "enable": false, + "radio": 1, + "if": 500000 + }, + "chan_Lora_std": { + "enable": true, + "radio": 1, + "if": 200000, + "bandwidth": 250000, + "spread_factor": 7 + }, + "chan_multiSF_0": { + "enable": true, + "radio": 1, + "if": -300000 + }, + "chan_multiSF_1": { + "enable": true, + "radio": 1, + "if": -100000 + }, + "chan_multiSF_2": { + "enable": true, + "radio": 1, + "if": 100000 + }, + "chan_multiSF_3": { + "enable": true, + "radio": 1, + "if": 300000 + }, + "chan_multiSF_4": { + "enable": true, + "radio": 0, + "if": -300000 + }, + "chan_multiSF_5": { + "enable": true, + "radio": 0, + "if": -100000 + }, + "chan_multiSF_6": { + "enable": true, + "radio": 0, + "if": 100000 + }, + "chan_multiSF_7": { + "enable": true, + "radio": 0, + "if": 300000 + } + } + ], + "nocca": true, + "nodc": true, + "nodwell": true, + "desiredTxParams": { + "downlinkDwellLimit": true, + "uplinkDwellLimit": true, + "eirp": 5 + } + }, + "desiredTxParams": { + "downlinkDwellLimit": true, + "uplinkDwellLimit": true, + "eirp": 5 + } +} diff --git a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/CN470RP1.json b/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/CN470RP1.json index 90ba0058bc..2dfecdeb36 100644 --- a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/CN470RP1.json +++ b/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/CN470RP1.json @@ -1,121 +1,124 @@ { - "NetID": [ - 1 - ], - "JoinEui": [ - [ - "0000000000000000", - "FFFFFFFFFFFFFFFF" - ] - ], - "region": "CN470RP1", - "hwspec": "sx1301/1", - "freq_range": [ - 470000000, - 510000000 - ], - "DRs": [ - [ - 12, - 125, - 0 + "routerConfig": { + + "NetID": [ + 1 ], - [ - 11, - 125, - 0 + "JoinEui": [ + [ + "0000000000000000", + "FFFFFFFFFFFFFFFF" + ] ], - [ - 10, - 125, - 0 + "region": "CN470RP1", + "hwspec": "sx1301/1", + "freq_range": [ + 470000000, + 510000000 ], - [ - 9, - 125, - 0 + "DRs": [ + [ + 12, + 125, + 0 + ], + [ + 11, + 125, + 0 + ], + [ + 10, + 125, + 0 + ], + [ + 9, + 125, + 0 + ], + [ + 8, + 125, + 0 + ], + [ + 7, + 125, + 0 + ], + [ + 7, + 500, + 0 + ] ], - [ - 8, - 125, - 0 - ], - [ - 7, - 125, - 0 - ], - [ - 7, - 500, - 0 - ] - ], - "sx1301_conf": [ - { - "radio_0": { - "enable": true, - "freq": 470600000 - }, - "radio_1": { - "enable": true, - "freq": 478400000 - }, - "chan_FSK": { - "enable": false, - "radio": 0, - "if": 1 - }, - "chan_Lora_std": { - "enable": false, - "radio": 0, - "if": 1, - "bandwidth": 125000, - "spread_factor": 7 - }, - "chan_multiSF_0": { - "enable": true, - "radio": 0, - "if": -300000 - }, - "chan_multiSF_1": { - "enable": true, - "radio": 0, - "if": -100000 - }, - "chan_multiSF_2": { - "enable": true, - "radio": 0, - "if": 100000 - }, - "chan_multiSF_3": { - "enable": true, - "radio": 0, - "if": 300000 - }, - "chan_multiSF_4": { - "enable": true, - "radio": 1, - "if": -300000 - }, - "chan_multiSF_5": { - "enable": true, - "radio": 1, - "if": -100000 - }, - "chan_multiSF_6": { - "enable": true, - "radio": 1, - "if": 100000 - }, - "chan_multiSF_7": { - "enable": true, - "radio": 1, - "if": 300000 + "sx1301_conf": [ + { + "radio_0": { + "enable": true, + "freq": 470600000 + }, + "radio_1": { + "enable": true, + "freq": 478400000 + }, + "chan_FSK": { + "enable": false, + "radio": 0, + "if": 1 + }, + "chan_Lora_std": { + "enable": false, + "radio": 0, + "if": 1, + "bandwidth": 125000, + "spread_factor": 7 + }, + "chan_multiSF_0": { + "enable": true, + "radio": 0, + "if": -300000 + }, + "chan_multiSF_1": { + "enable": true, + "radio": 0, + "if": -100000 + }, + "chan_multiSF_2": { + "enable": true, + "radio": 0, + "if": 100000 + }, + "chan_multiSF_3": { + "enable": true, + "radio": 0, + "if": 300000 + }, + "chan_multiSF_4": { + "enable": true, + "radio": 1, + "if": -300000 + }, + "chan_multiSF_5": { + "enable": true, + "radio": 1, + "if": -100000 + }, + "chan_multiSF_6": { + "enable": true, + "radio": 1, + "if": 100000 + }, + "chan_multiSF_7": { + "enable": true, + "radio": 1, + "if": 300000 + } } - } - ], - "nocca": true, - "nodc": true, - "nodwell": true + ], + "nocca": true, + "nodc": true, + "nodwell": true + } } diff --git a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/CN470RP2.json b/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/CN470RP2.json index 6f596f3a74..6026500a0a 100644 --- a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/CN470RP2.json +++ b/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/CN470RP2.json @@ -1,122 +1,124 @@ { - "NetID": [ - 1 - ], - "JoinEui": [ - [ - "0000000000000000", - "FFFFFFFFFFFFFFFF" - ] - ], - "region": "CN470RP2", - "hwspec": "sx1301/1", - "freq_range": [ - 470000000, - 510000000 - ], - "DRs": [ - [ - 12, - 125, - 0 + "routerConfig": { + "NetID": [ + 1 ], - [ - 11, - 125, - 0 + "JoinEui": [ + [ + "0000000000000000", + "FFFFFFFFFFFFFFFF" + ] ], - [ - 10, - 125, - 0 + "region": "CN470RP2", + "hwspec": "sx1301/1", + "freq_range": [ + 470000000, + 510000000 ], - [ - 9, - 125, - 0 + "DRs": [ + [ + 12, + 125, + 0 + ], + [ + 11, + 125, + 0 + ], + [ + 10, + 125, + 0 + ], + [ + 9, + 125, + 0 + ], + [ + 8, + 125, + 0 + ], + [ + 7, + 125, + 0 + ], + [ + 7, + 500, + 0 + ] ], - [ - 8, - 125, - 0 - ], - [ - 7, - 125, - 0 - ], - [ - 7, - 500, - 0 - ] - ], - "sx1301_conf": [ - { - "radio_0": { - "enable": true, - "freq": 498700000 - }, - "radio_1": { - "enable": true, - "freq": 499600000 - }, - "chan_FSK": { - "enable": false, - "radio": 0, - "if": 1 - }, - "chan_Lora_std": { - "enable": false, - "radio": 0, - "if": 1, - "bandwidth": 125000, - "spread_factor": 7 - }, - "chan_multiSF_0": { - "enable": true, - "radio": 0, - "if": -400000 - }, - "chan_multiSF_1": { - "enable": true, - "radio": 0, - "if": 0 - }, - "chan_multiSF_2": { - "enable": true, - "radio": 0, - "if": 200000 - }, - "chan_multiSF_3": { - "enable": true, - "radio": 0, - "if": 400000 - }, - "chan_multiSF_4": { - "enable": true, - "radio": 1, - "if": -300000 - }, - "chan_multiSF_5": { - "enable": true, - "radio": 1, - "if": -100000 - }, - "chan_multiSF_6": { - "enable": true, - "radio": 1, - "if": 100000 - }, - "chan_multiSF_7": { - "enable": true, - "radio": 1, - "if": 300000 + "sx1301_conf": [ + { + "radio_0": { + "enable": true, + "freq": 498700000 + }, + "radio_1": { + "enable": true, + "freq": 499600000 + }, + "chan_FSK": { + "enable": false, + "radio": 0, + "if": 1 + }, + "chan_Lora_std": { + "enable": false, + "radio": 0, + "if": 1, + "bandwidth": 125000, + "spread_factor": 7 + }, + "chan_multiSF_0": { + "enable": true, + "radio": 0, + "if": -400000 + }, + "chan_multiSF_1": { + "enable": true, + "radio": 0, + "if": 0 + }, + "chan_multiSF_2": { + "enable": true, + "radio": 0, + "if": 200000 + }, + "chan_multiSF_3": { + "enable": true, + "radio": 0, + "if": 400000 + }, + "chan_multiSF_4": { + "enable": true, + "radio": 1, + "if": -300000 + }, + "chan_multiSF_5": { + "enable": true, + "radio": 1, + "if": -100000 + }, + "chan_multiSF_6": { + "enable": true, + "radio": 1, + "if": 100000 + }, + "chan_multiSF_7": { + "enable": true, + "radio": 1, + "if": 300000 + } } - } - ], - "nocca": true, - "nodc": true, - "nodwell": true + ], + "nocca": true, + "nodc": true, + "nodwell": true + } } diff --git a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/EU863.json b/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/EU863.json index 95545e858e..bd91f496de 100644 --- a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/EU863.json +++ b/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/EU863.json @@ -1,121 +1,123 @@ { - "NetID": [ - 1 - ], - "JoinEui": [ - [ - "0000000000000000", - "FFFFFFFFFFFFFFFF" - ] - ], - "region": "EU863", - "hwspec": "sx1301/1", - "freq_range": [ - 863000000, - 870000000 - ], - "DRs": [ - [ - 12, - 125, - 0 + "routerConfig": { + "NetID": [ + 1 ], - [ - 11, - 125, - 0 + "JoinEui": [ + [ + "0000000000000000", + "FFFFFFFFFFFFFFFF" + ] ], - [ - 10, - 125, - 0 + "region": "EU863", + "hwspec": "sx1301/1", + "freq_range": [ + 863000000, + 870000000 ], - [ - 9, - 125, - 0 + "DRs": [ + [ + 12, + 125, + 0 + ], + [ + 11, + 125, + 0 + ], + [ + 10, + 125, + 0 + ], + [ + 9, + 125, + 0 + ], + [ + 8, + 125, + 0 + ], + [ + 7, + 125, + 0 + ], + [ + 7, + 250, + 0 + ] ], - [ - 8, - 125, - 0 - ], - [ - 7, - 125, - 0 - ], - [ - 7, - 250, - 0 - ] - ], - "sx1301_conf": [ - { - "radio_0": { - "enable": true, - "freq": 867500000 - }, - "radio_1": { - "enable": true, - "freq": 868500000 - }, - "chan_FSK": { - "enable": true, - "radio": 1, - "if": 300000 - }, - "chan_Lora_std": { - "enable": true, - "radio": 1, - "if": -200000, - "bandwidth": 250000, - "spread_factor": 7 - }, - "chan_multiSF_0": { - "enable": true, - "radio": 1, - "if": -400000 - }, - "chan_multiSF_1": { - "enable": true, - "radio": 1, - "if": -200000 - }, - "chan_multiSF_2": { - "enable": true, - "radio": 1, - "if": 0 - }, - "chan_multiSF_3": { - "enable": true, - "radio": 0, - "if": -400000 - }, - "chan_multiSF_4": { - "enable": true, - "radio": 0, - "if": -200000 - }, - "chan_multiSF_5": { - "enable": true, - "radio": 0, - "if": 0 - }, - "chan_multiSF_6": { - "enable": true, - "radio": 0, - "if": 200000 - }, - "chan_multiSF_7": { - "enable": true, - "radio": 0, - "if": 400000 + "sx1301_conf": [ + { + "radio_0": { + "enable": true, + "freq": 867500000 + }, + "radio_1": { + "enable": true, + "freq": 868500000 + }, + "chan_FSK": { + "enable": true, + "radio": 1, + "if": 300000 + }, + "chan_Lora_std": { + "enable": true, + "radio": 1, + "if": -200000, + "bandwidth": 250000, + "spread_factor": 7 + }, + "chan_multiSF_0": { + "enable": true, + "radio": 1, + "if": -400000 + }, + "chan_multiSF_1": { + "enable": true, + "radio": 1, + "if": -200000 + }, + "chan_multiSF_2": { + "enable": true, + "radio": 1, + "if": 0 + }, + "chan_multiSF_3": { + "enable": true, + "radio": 0, + "if": -400000 + }, + "chan_multiSF_4": { + "enable": true, + "radio": 0, + "if": -200000 + }, + "chan_multiSF_5": { + "enable": true, + "radio": 0, + "if": 0 + }, + "chan_multiSF_6": { + "enable": true, + "radio": 0, + "if": 200000 + }, + "chan_multiSF_7": { + "enable": true, + "radio": 0, + "if": 400000 + } } - } - ], - "nocca": true, - "nodc": true, - "nodwell": true + ], + "nocca": true, + "nodc": true, + "nodwell": true + } } diff --git a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/US902.json b/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/US902.json index 63a964b9d3..e0c3a053ac 100644 --- a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/US902.json +++ b/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/US902.json @@ -1,156 +1,158 @@ { - "NetID": [ - 1 - ], - "JoinEui": [ - [ - "0000000000000000", - "FFFFFFFFFFFFFFFF" - ] - ], - "region": "US902", - "hwspec": "sx1301/1", - "freq_range": [ - 902000000, - 928000000 - ], - "DRs": [ - [ - 10, - 125, - 0 - ], - [ - 9, - 125, - 0 - ], - [ - 8, - 125, - 0 - ], - [ - 7, - 125, - 0 - ], - [ - 8, - 500, - 0 - ], - [ - 0, - 0, - 0 - ], - [ - 0, - 0, - 0 - ], - [ - 0, - 0, - 0 - ], - [ - 12, - 500, + "routerConfig": { + "NetID": [ 1 ], - [ - 11, - 500, - 1 + "JoinEui": [ + [ + "0000000000000000", + "FFFFFFFFFFFFFFFF" + ] ], - [ - 10, - 500, - 1 + "region": "US902", + "hwspec": "sx1301/1", + "freq_range": [ + 902000000, + 928000000 ], - [ - 9, - 500, - 1 + "DRs": [ + [ + 10, + 125, + 0 + ], + [ + 9, + 125, + 0 + ], + [ + 8, + 125, + 0 + ], + [ + 7, + 125, + 0 + ], + [ + 8, + 500, + 0 + ], + [ + 0, + 0, + 0 + ], + [ + 0, + 0, + 0 + ], + [ + 0, + 0, + 0 + ], + [ + 12, + 500, + 1 + ], + [ + 11, + 500, + 1 + ], + [ + 10, + 500, + 1 + ], + [ + 9, + 500, + 1 + ], + [ + 8, + 500, + 1 + ], + [ + 7, + 500, + 1 + ] ], - [ - 8, - 500, - 1 - ], - [ - 7, - 500, - 1 - ] - ], - "sx1301_conf": [ - { - "radio_0": { - "enable": true, - "freq": 902700000 - }, - "radio_1": { - "enable": true, - "freq": 903400000 - }, - "chan_FSK": { - "enable": true, - "radio": 1, - "if": 300000 - }, - "chan_Lora_std": { - "enable": true, - "radio": 0, - "if": 300000, - "bandwidth": 500000, - "spread_factor": 8 - }, - "chan_multiSF_0": { - "enable": true, - "radio": 0, - "if": -400000 - }, - "chan_multiSF_1": { - "enable": true, - "radio": 0, - "if": -200000 - }, - "chan_multiSF_2": { - "enable": true, - "radio": 0, - "if": 0 - }, - "chan_multiSF_3": { - "enable": true, - "radio": 0, - "if": 200000 - }, - "chan_multiSF_4": { - "enable": true, - "radio": 1, - "if": -300000 - }, - "chan_multiSF_5": { - "enable": true, - "radio": 1, - "if": -100000 - }, - "chan_multiSF_6": { - "enable": true, - "radio": 1, - "if": 100000 - }, - "chan_multiSF_7": { - "enable": true, - "radio": 1, - "if": 300000 + "sx1301_conf": [ + { + "radio_0": { + "enable": true, + "freq": 902700000 + }, + "radio_1": { + "enable": true, + "freq": 903400000 + }, + "chan_FSK": { + "enable": true, + "radio": 1, + "if": 300000 + }, + "chan_Lora_std": { + "enable": true, + "radio": 0, + "if": 300000, + "bandwidth": 500000, + "spread_factor": 8 + }, + "chan_multiSF_0": { + "enable": true, + "radio": 0, + "if": -400000 + }, + "chan_multiSF_1": { + "enable": true, + "radio": 0, + "if": -200000 + }, + "chan_multiSF_2": { + "enable": true, + "radio": 0, + "if": 0 + }, + "chan_multiSF_3": { + "enable": true, + "radio": 0, + "if": 200000 + }, + "chan_multiSF_4": { + "enable": true, + "radio": 1, + "if": -300000 + }, + "chan_multiSF_5": { + "enable": true, + "radio": 1, + "if": -100000 + }, + "chan_multiSF_6": { + "enable": true, + "radio": 1, + "if": 100000 + }, + "chan_multiSF_7": { + "enable": true, + "radio": 1, + "if": 300000 + } } - } - ], - "nocca": true, - "nodc": true, - "nodwell": true + ], + "nocca": true, + "nodc": true, + "nodwell": true + } } diff --git a/Tools/Cli-LoRa-Device-Provisioning/Helpers/IoTDeviceHelper.cs b/Tools/Cli-LoRa-Device-Provisioning/Helpers/IoTDeviceHelper.cs index 2f86d068bd..8c3fe590a3 100644 --- a/Tools/Cli-LoRa-Device-Provisioning/Helpers/IoTDeviceHelper.cs +++ b/Tools/Cli-LoRa-Device-Provisioning/Helpers/IoTDeviceHelper.cs @@ -265,7 +265,7 @@ public static object CleanOptions(object optsObject, bool isNewDevice) if (!string.IsNullOrEmpty(opts.FCntResetCounter)) opts.FCntResetCounter = ValidationHelper.CleanString(opts.FCntResetCounter); - return (object)opts; + return opts; } public static AddOptions CompleteMissingAddOptions(AddOptions opts, ConfigurationHelper configurationHelper) @@ -944,7 +944,8 @@ public static Twin CreateConcentratorTwin(AddOptions opts, uint crcChecksum, Uri var fileName = Path.Combine(DefaultRouterConfigFolder, $"{opts.Region.ToUpperInvariant()}.json"); var jsonString = File.ReadAllText(fileName); var propObject = JsonConvert.DeserializeObject(jsonString); - twinProperties.Desired[TwinProperty.RouterConfig] = propObject; + twinProperties.Desired[TwinProperty.RouterConfig] = propObject[TwinProperty.RouterConfig]; + twinProperties.Desired[TwinProperty.DesiredTxParams] = propObject[TwinProperty.DesiredTxParams]; if (!opts.NoCups) { diff --git a/Tools/Cli-LoRa-Device-Provisioning/Program.cs b/Tools/Cli-LoRa-Device-Provisioning/Program.cs index 0734f72a54..0b83451c78 100644 --- a/Tools/Cli-LoRa-Device-Provisioning/Program.cs +++ b/Tools/Cli-LoRa-Device-Provisioning/Program.cs @@ -19,7 +19,7 @@ namespace LoRaWan.Tools.CLI public static class Program { - private static readonly ConfigurationHelper configurationHelper = new ConfigurationHelper(); + private static readonly ConfigurationHelper ConfigurationHelper = new ConfigurationHelper(); public static async Task Main(string[] args) { @@ -35,7 +35,7 @@ public static async Task Main(string[] args) Console.ResetColor(); Console.WriteLine(); - if (!configurationHelper.ReadConfig(args)) + if (!ConfigurationHelper.ReadConfig(args)) { WriteToConsole("Failed to parse configuration.", ConsoleColor.Red); return (int)ExitCode.Error; @@ -92,14 +92,14 @@ private static async Task RunListAndReturnExitCode(ListOptions opts) if (!int.TryParse(opts.Total, out var total)) total = -1; - var isSuccess = await IoTDeviceHelper.QueryDevices(configurationHelper, page, total); + var isSuccess = await IoTDeviceHelper.QueryDevices(ConfigurationHelper, page, total); return isSuccess; } private static async Task RunQueryAndReturnExitCode(QueryOptions opts) { - var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.DevEui, configurationHelper); + var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.DevEui, ConfigurationHelper); if (twin != null) { @@ -115,12 +115,12 @@ private static async Task RunQueryAndReturnExitCode(QueryOptions opts) private static async Task RunVerifyAndReturnExitCode(VerifyOptions opts) { - var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.DevEui, configurationHelper); + var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.DevEui, ConfigurationHelper); if (twin != null) { StatusConsole.WriteTwin(opts.DevEui, twin); - return IoTDeviceHelper.VerifyDeviceTwin(opts.DevEui, opts.NetId, twin, configurationHelper, true); + return IoTDeviceHelper.VerifyDeviceTwin(opts.DevEui, opts.NetId, twin, ConfigurationHelper, true); } else { @@ -134,7 +134,7 @@ private static async Task RunBulkVerifyAndReturnExitCode(BulkVerifyOptions if (!int.TryParse(opts.Page, out var page)) page = 0; - var isSuccess = await IoTDeviceHelper.QueryDevicesAndVerify(configurationHelper, page); + var isSuccess = await IoTDeviceHelper.QueryDevicesAndVerify(ConfigurationHelper, page); Console.WriteLine(); if (isSuccess) @@ -159,13 +159,13 @@ private static async Task RunAddAndReturnExitCode(AddOptions opts) { var isVerified = IoTDeviceHelper.VerifyConcentrator(opts); if (!isVerified) return false; - if (configurationHelper.CertificateStorageContainerClient is null) + if (!opts.NoCups && ConfigurationHelper.CertificateStorageContainerClient is null) { StatusConsole.WriteLogLine(MessageType.Error, "Storage account is not correctly configured."); return false; } - if (await IoTDeviceHelper.QueryDeviceTwin(opts.StationEui, configurationHelper) is not null) + if (await IoTDeviceHelper.QueryDeviceTwin(opts.StationEui, ConfigurationHelper) is not null) { StatusConsole.WriteLogLine(MessageType.Error, "Station was already created, please use the 'update' verb to update an existing station."); return false; @@ -174,24 +174,24 @@ private static async Task RunAddAndReturnExitCode(AddOptions opts) if (opts.NoCups) { var twin = IoTDeviceHelper.CreateConcentratorTwin(opts, 0, null); - return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, configurationHelper, true); + return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, ConfigurationHelper, true); } else { return await UploadCertificateBundleAsync(opts.CertificateBundleLocation, opts.StationEui, async (crcHash, bundleStorageUri) => { var twin = IoTDeviceHelper.CreateConcentratorTwin(opts, crcHash, bundleStorageUri); - return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, configurationHelper, true); + return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, ConfigurationHelper, true); }); } } - opts = IoTDeviceHelper.CompleteMissingAddOptions(opts, configurationHelper); + opts = IoTDeviceHelper.CompleteMissingAddOptions(opts, ConfigurationHelper); - if (IoTDeviceHelper.VerifyDevice(opts, null, null, null, configurationHelper, true)) + if (IoTDeviceHelper.VerifyDevice(opts, null, null, null, ConfigurationHelper, true)) { var twin = IoTDeviceHelper.CreateDeviceTwin(opts); - isSuccess = await IoTDeviceHelper.WriteDeviceTwin(twin, opts.DevEui, configurationHelper, true); + isSuccess = await IoTDeviceHelper.WriteDeviceTwin(twin, opts.DevEui, ConfigurationHelper, true); } else { @@ -200,7 +200,7 @@ private static async Task RunAddAndReturnExitCode(AddOptions opts) if (isSuccess) { - var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.DevEui, configurationHelper); + var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.DevEui, ConfigurationHelper); StatusConsole.WriteTwin(opts.DevEui, twin); } @@ -215,7 +215,7 @@ private static async Task RunRotateCertificateAndReturnExitCodeAsync(Rotat return false; } - var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.StationEui, configurationHelper); + var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.StationEui, ConfigurationHelper); if (twin is null) { @@ -241,17 +241,17 @@ private static async Task RunRotateCertificateAndReturnExitCodeAsync(Rotat twin.Properties.Desired[TwinProperty.Cups][TwinProperty.CupsCredentialUrl] = bundleStorageUri; twin.Properties.Desired[TwinProperty.Cups][TwinProperty.TcCredentialUrl] = bundleStorageUri; - return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, configurationHelper, isNewDevice: false); + return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, ConfigurationHelper, isNewDevice: false); }); // Clean up old certificate bundles try { - _ = await configurationHelper.CertificateStorageContainerClient.DeleteBlobIfExistsAsync(oldCupsCredentialBundleLocation.Segments.Last()); + _ = await ConfigurationHelper.CertificateStorageContainerClient.DeleteBlobIfExistsAsync(oldCupsCredentialBundleLocation.Segments.Last()); } finally { - _ = await configurationHelper.CertificateStorageContainerClient.DeleteBlobIfExistsAsync(oldTcCredentialBundleLocation.Segments.Last()); + _ = await ConfigurationHelper.CertificateStorageContainerClient.DeleteBlobIfExistsAsync(oldTcCredentialBundleLocation.Segments.Last()); } return success; @@ -259,7 +259,7 @@ private static async Task RunRotateCertificateAndReturnExitCodeAsync(Rotat private static async Task RunRevokeAndReturnExitCodeAsync(RevokeOptions opts) { - var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.StationEui, configurationHelper); + var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.StationEui, ConfigurationHelper); if (twin is null) { @@ -276,7 +276,7 @@ private static async Task RunRevokeAndReturnExitCodeAsync(RevokeOptions op t?.Remove(); twin.Properties.Desired[TwinProperty.ClientThumbprint] = clientThumprints; - return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, configurationHelper, isNewDevice: false); + return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, ConfigurationHelper, isNewDevice: false); } private static async Task RunUpdateAndReturnExitCode(UpdateOptions opts) @@ -284,17 +284,17 @@ private static async Task RunUpdateAndReturnExitCode(UpdateOptions opts) var isSuccess = false; opts = IoTDeviceHelper.CleanOptions(opts, false) as UpdateOptions; - opts = IoTDeviceHelper.CompleteMissingUpdateOptions(opts, configurationHelper); + opts = IoTDeviceHelper.CompleteMissingUpdateOptions(opts, ConfigurationHelper); - var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.DevEui, configurationHelper); + var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.DevEui, ConfigurationHelper); if (twin != null) { twin = IoTDeviceHelper.UpdateDeviceTwin(twin, opts); - if (IoTDeviceHelper.VerifyDeviceTwin(opts.DevEui, opts.NetId, twin, configurationHelper, true)) + if (IoTDeviceHelper.VerifyDeviceTwin(opts.DevEui, opts.NetId, twin, ConfigurationHelper, true)) { - isSuccess = await IoTDeviceHelper.WriteDeviceTwin(twin, opts.DevEui, configurationHelper, false); + isSuccess = await IoTDeviceHelper.WriteDeviceTwin(twin, opts.DevEui, ConfigurationHelper, false); if (isSuccess) { @@ -325,7 +325,7 @@ private static async Task RunUpdateAndReturnExitCode(UpdateOptions opts) private static async Task UploadCertificateBundleAsync(string certificateBundleLocation, string stationEui, Func> uploadSuccessActionAsync) { var certificateBundleBlobName = $"{stationEui}-{Guid.NewGuid():N}"; - var blobClient = configurationHelper.CertificateStorageContainerClient.GetBlobClient(certificateBundleBlobName); + var blobClient = ConfigurationHelper.CertificateStorageContainerClient.GetBlobClient(certificateBundleBlobName); var fileContent = File.ReadAllBytes(certificateBundleLocation); try @@ -378,7 +378,7 @@ private static async Task RunUpgradeFirmwareAndReturnExitCodeAsync(Upgrade // Upload firmware file to storage account var success = await UploadFirmwareAsync(opts.FirmwareLocation, opts.StationEui, opts.Package, async (firmwareBlobUri) => { - var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.StationEui, configurationHelper); + var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.StationEui, ConfigurationHelper); if (twin is null) { @@ -398,7 +398,7 @@ private static async Task RunUpgradeFirmwareAndReturnExitCodeAsync(Upgrade twin.Properties.Desired[TwinProperty.Cups][TwinProperty.FirmwareKeyChecksum] = checksum; twin.Properties.Desired[TwinProperty.Cups][TwinProperty.FirmwareSignature] = File.ReadAllText(opts.DigestLocation, Encoding.UTF8); - return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, configurationHelper, isNewDevice: false); + return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, ConfigurationHelper, isNewDevice: false); }); return success; @@ -407,7 +407,7 @@ private static async Task RunUpgradeFirmwareAndReturnExitCodeAsync(Upgrade private static async Task UploadFirmwareAsync(string firmwareLocation, string stationEui, string package, Func> uploadSuccessActionAsync) { var firmwareBlobName = $"{stationEui}-{package}"; - var blobClient = configurationHelper.FirmwareStorageContainerClient.GetBlobClient(firmwareBlobName); + var blobClient = ConfigurationHelper.FirmwareStorageContainerClient.GetBlobClient(firmwareBlobName); var fileContent = File.ReadAllBytes(firmwareLocation); StatusConsole.WriteLogLine(MessageType.Info, $"Uploading firmware {firmwareBlobName} to storage account..."); @@ -443,7 +443,7 @@ private static async Task UploadFirmwareAsync(string firmwareLocation, str private static async Task RunRemoveAndReturnExitCode(RemoveOptions opts) { - return await IoTDeviceHelper.RemoveDevice(opts.DevEui, configurationHelper); + return await IoTDeviceHelper.RemoveDevice(opts.DevEui, ConfigurationHelper); } private static void WriteAzureLogo() diff --git a/Tools/Cli-LoRa-Device-Provisioning/TwinProperty.cs b/Tools/Cli-LoRa-Device-Provisioning/TwinProperty.cs index d2e485a0db..0deb0e85cb 100644 --- a/Tools/Cli-LoRa-Device-Provisioning/TwinProperty.cs +++ b/Tools/Cli-LoRa-Device-Provisioning/TwinProperty.cs @@ -58,5 +58,10 @@ public static class TwinProperty /// Defines the connection keep alive timeout /// public const string KeepAliveTimeout = "KeepAliveTimeout"; + + /// + /// Defines the tx params in case of a dwell time enabled network + /// + public const string DesiredTxParams = "desiredTxParams"; } } diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000000..6ce97590ac --- /dev/null +++ b/nuget.config @@ -0,0 +1,8 @@ + + + + + + + +