diff --git a/.eslintignore b/.eslintignore index bbb10ce0..a8142711 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,4 +6,7 @@ dist # don't lint nyc coverage output coverage # don't lint proto files and output -proto \ No newline at end of file +proto +# don't lint jest infrastructure files +jest.setup.js +test/e2e/helpers/CustomNodeEnvironment.cjs \ No newline at end of file diff --git a/.github/workflows/test-e2e-testcontainers.yml b/.github/workflows/test-e2e-testcontainers.yml new file mode 100644 index 00000000..741ada7e --- /dev/null +++ b/.github/workflows/test-e2e-testcontainers.yml @@ -0,0 +1,109 @@ +# +# Copyright 2022 The Dapr Authors +# 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. +# + +# End-to-end tests using testcontainers. +# +# Each test suite spins up its own Docker network, Dapr sidecar, and any +# required backing services (Redis, MongoDB, EMQX MQTT) entirely within the +# runner – no external Dapr CLI install, no `dapr init`, no static service +# containers required. +# +# To test against a specific Dapr runtime version trigger this workflow +# manually and supply `dapr_runtime_ver` (e.g. "1.17.0-rc.1"). When left +# blank the default version (1.16.12) is used. + +name: Test - E2E (testcontainers) + +on: + push: + branches: + - main + - release-* + tags: + - v* + pull_request: + branches: + - main + - release-* + # Manual trigger – optionally pin a specific Dapr runtime version. + workflow_dispatch: + inputs: + dapr_runtime_ver: + description: > + Dapr runtime version to test against (e.g. 1.16.4, 1.17.0-rc.1). + Leave blank to use the version pinned in @dapr/testcontainer-node. + required: false + default: "" + +permissions: + contents: read + +jobs: + test-e2e: + name: E2E (${{ matrix.node_version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node_version: [22.16.0] + + env: + # Forwarded into test/e2e/helpers/containers.ts so that all three Dapr + # images (daprd, placement, scheduler) are pulled at the requested version. + # Defaults to 1.16.12 on push/PR; can be overridden via workflow_dispatch input. + DAPR_RUNTIME_VER: ${{ github.event.inputs.dapr_runtime_ver || '1.16.12' }} + + steps: + - name: Checkout JS-SDK + uses: actions/checkout@v4 + + - name: Setup Node ${{ matrix.node_version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node_version }} + registry-url: "https://registry.npmjs.org" + + - name: Install dependencies + run: npm ci + + - name: Build package + run: npm run build + + - name: Run E2E tests – HTTP (client + server + actors) + run: npm run test:e2e:http + env: + DAPR_RUNTIME_VER: ${{ env.DAPR_RUNTIME_VER }} + + - name: Run E2E tests – gRPC (client + server + apiToken) + run: npm run test:e2e:grpc + env: + DAPR_RUNTIME_VER: ${{ env.DAPR_RUNTIME_VER }} + + - name: Run E2E tests – Common (HTTP and gRPC, each isolated) + run: npm run test:e2e:common + env: + DAPR_RUNTIME_VER: ${{ env.DAPR_RUNTIME_VER }} + + - name: Run E2E tests – Workflow + run: npm run test:e2e:workflow + env: + DAPR_RUNTIME_VER: ${{ env.DAPR_RUNTIME_VER }} + + - name: Verify TypeScript build (e2e typescript-build test) + run: | + cd test/e2e/typescript-build + npm install + npm run start + + - name: Upload test coverage + uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index b960cae0..e354b53d 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -14,102 +14,27 @@ name: Test - E2E on: - push: - branches: - - main - - release-* - tags: - - v* - pull_request: - branches: - - main - - release-* - # Manual trigger + # Manual trigger – optionally specify a Dapr runtime version to test against. workflow_dispatch: - # Dispatch on external events - repository_dispatch: - types: [e2e-test] + inputs: + dapr_runtime_ver: + description: > + Dapr runtime version to test against (e.g. 1.16.4, 1.17.0-rc.1). + Leave blank to use the version pinned in @dapr/testcontainer-node. + required: false + default: "" jobs: test-e2e: runs-on: ubuntu-latest env: - GOVER: 1.22 - DAPR_CLI_VER: 1.16.1 - DAPR_RUNTIME_VER: 1.17.3 - DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/master/install/install.sh - DAPR_CLI_REF: "" - DAPR_REF: "" NODE_VER: 22.16.0 - services: - emqx: - image: emqx/emqx - ports: - - 1883:1883 - - 8081:8081 - - 8083:8083 - - 8883:8883 - # - 8084:8084 // this port is already used? - - 18083:18083 - mongodb: - image: mongo - ports: - - 27017:27017 + # DAPR_RUNTIME_VER is forwarded into every test process so that the helpers + # in test/e2e/helpers/containers.ts can build the correct Docker image tags. + # On a push/PR it is empty (uses the testcontainer-node default). On a + # manual dispatch it comes from the input above. + DAPR_RUNTIME_VER: ${{ github.event.inputs.dapr_runtime_ver || '' }} steps: - - name: Set up Dapr CLI - run: wget -q ${{ env.DAPR_INSTALL_URL }} -O - | /bin/bash -s ${{ env.DAPR_CLI_VER }} - - - name: Set up Go ${{ env.GOVER }} - if: env.DAPR_REF != '' || env.DAPR_CLI_REF != '' - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GOVER }} - - - name: Checkout Dapr CLI repo to custom reference - uses: actions/checkout@v4 - if: env.DAPR_CLI_REF != '' - with: - repository: dapr/cli - ref: ${{ env.DAPR_CLI_REF }} - path: cli - - - name: Checkout Dapr runtime repo to custom reference - uses: actions/checkout@v4 - if: env.DAPR_REF != '' - with: - repository: dapr/dapr - ref: ${{ env.DAPR_REF }} - path: dapr - - - name: Build and override dapr cli with referenced commit - if: env.DAPR_CLI_REF != '' - run: | - cd cli - make - sudo cp dist/linux_amd64/release/dapr /usr/local/bin/dapr - cd .. - - - name: Initialize Dapr runtime ${{ env.DAPR_RUNTIME_VER }} - run: | - dapr uninstall --all - dapr init --runtime-version ${{ env.DAPR_RUNTIME_VER }} - - - name: Build and override daprd with referenced commit. - if: env.DAPR_REF != '' - run: | - cd dapr - make - mkdir -p $HOME/.dapr/bin/ - cp dist/linux_amd64/release/daprd $HOME/.dapr/bin/daprd - cd .. - - - name: Override placement service. - if: env.DAPR_REF != '' - run: | - docker stop dapr_placement - cd dapr - ./dist/linux_amd64/release/placement & - - name: Checkout JS-SDK uses: actions/checkout@v4 @@ -121,11 +46,16 @@ jobs: node-version: ${{ env.NODE_VER }} registry-url: "https://registry.npmjs.org" + - name: Install dependencies + run: npm ci + - name: Build Package run: npm run build - name: Run E2E tests id: tests + env: + DAPR_RUNTIME_VER: ${{ env.DAPR_RUNTIME_VER }} run: npm run test:e2e:all - name: Run E2E test to show successful typescript build @@ -133,7 +63,7 @@ jobs: run: | cd test/e2e/typescript-build npm install - dapr run --app-id typescript-build npm run start + npm run start - name: Upload test coverage uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 diff --git a/jest.config.js b/jest.config.js index a8997e7d..78f83f2e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,10 +13,17 @@ limitations under the License. module.exports = { preset: "ts-jest", - testEnvironment: "node", + testEnvironment: "/test/e2e/helpers/CustomNodeEnvironment.cjs", + setupFiles: ["/jest.setup.js"], collectCoverage: true, coverageReporters: ["lcov"], + collectCoverageFrom: ["src/**/*.ts", "!src/proto/**"], modulePathIgnorePatterns: ["/build/"], // Load .github/scripts/*.js files as native CJS — they have no TypeScript syntax. transformIgnorePatterns: ["/node_modules/", "/.github/"], + // Post-process results to remove spurious "Test suite failed to run" entries + // caused by empty AggregateErrors from testcontainers/ssh2 SubtleCrypto handle + // GC during container teardown. All individual tests pass; only the suite-level + // unhandledRejection handler catches these, so we strip them here. + testResultsProcessor: "/test/e2e/helpers/filterAggregateErrors.cjs", }; diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 00000000..44b269ce --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,120 @@ +/* +Copyright 2022 The Dapr Authors +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. +*/ + +/** + * Jest 27's jest-environment-node runs tests in a sandboxed VM context that + * does NOT automatically expose all Web API globals (ReadableStream, fetch, + * Blob, DOMException, etc.) that Node.js 18+ added to the real globalThis. + * testcontainers → undici references these at module-load time, so we must + * inject them (via setupFiles) before any test module is imported. + * + * NOTE: jest-environment-node v27 already injects URL, URLSearchParams, + * TextEncoder, TextDecoder, AbortController, AbortSignal, Event, EventTarget, + * performance, Buffer, ArrayBuffer, and Uint8Array, so we skip those. + * + * Strategy: use require() against Node's built-in modules (stream/web, buffer, + * url, worker_threads, crypto, perf_hooks) and against the locally-installed + * undici package to get every remaining Web API. We do NOT use the + * "outer-realm globalThis escape" trick (Buffer.constructor('return globalThis')()) + * because in Node 24 that technique crashes the process with an assertion failure + * (isolate_data not set) when any property is read from the returned proxy. + */ + +/* eslint-disable @typescript-eslint/no-require-imports */ +const streamWeb = require("stream/web"); +const { Blob, File } = require("buffer"); +const { MessageChannel, MessagePort, BroadcastChannel } = require("worker_threads"); +const { webcrypto } = require("crypto"); + +// ── stream/web ────────────────────────────────────────────────────────────── +// These MUST be set before requiring undici, which references ReadableStream +// at module-load time. +const streamWebNames = [ + "ReadableStream", + "ReadableStreamDefaultController", + "ReadableStreamDefaultReader", + "ReadableByteStreamController", + "ReadableStreamBYOBReader", + "ReadableStreamBYOBRequest", + "WritableStream", + "WritableStreamDefaultController", + "WritableStreamDefaultWriter", + "TransformStream", + "TransformStreamDefaultController", + "ByteLengthQueuingStrategy", + "CountQueuingStrategy", + "TextEncoderStream", + "TextDecoderStream", + "CompressionStream", + "DecompressionStream", +]; + +for (const name of streamWebNames) { + if (typeof global[name] === "undefined" && streamWeb[name] !== undefined) { + global[name] = streamWeb[name]; + } +} + +// ── buffer (before undici) ──────────────────────────────────────────────────── +if (typeof global.Blob === "undefined") global.Blob = Blob; +if (typeof global.File === "undefined") global.File = File; + +// ── worker_threads (before undici) ─────────────────────────────────────────── +if (typeof global.MessageChannel === "undefined") global.MessageChannel = MessageChannel; +if (typeof global.MessagePort === "undefined") global.MessagePort = MessagePort; +if (typeof global.BroadcastChannel === "undefined") global.BroadcastChannel = BroadcastChannel; + +// ── crypto (before undici) ──────────────────────────────────────────────────── +if (typeof global.crypto === "undefined") global.crypto = webcrypto; + +// ── DOMException (before undici) ───────────────────────────────────────────── +// DOMException is not exported by any Node.js built-in module. Provide a +// minimal standards-compliant shim so that undici's WebSocket implementation +// (which uses `class X extends DOMException`) can load without crashing. +if (typeof global.DOMException === "undefined") { + class DOMException extends Error { + constructor(message = "", name = "Error") { + super(message); + this.name = name; + } + } + global.DOMException = DOMException; +} + +// Now it is safe to require undici — all required globals are defined. +const undici = require("undici"); +/* eslint-enable @typescript-eslint/no-require-imports */ + +// ── undici (fetch API + WebSocket) ─────────────────────────────────────────── +const undiciGlobals = ["fetch", "Headers", "Request", "Response", "FormData", "WebSocket", "CloseEvent", "MessageEvent"]; +for (const name of undiciGlobals) { + if (typeof global[name] === "undefined" && undici[name] !== undefined) { + global[name] = undici[name]; + } +} + +// ── AggregateError suppression ──────────────────────────────────────────────── +// Suppression of the empty AggregateErrors emitted by testcontainers/ssh2 +// SubtleCrypto handles during container teardown is handled by the custom +// testEnvironment (test/e2e/helpers/CustomNodeEnvironment.cjs). +// +// This file (jest.setup.js) runs via runtime.requireModule() inside the VM +// context created by jest-environment-node. The `process` object visible here +// is a copy (createProcessObject()) whose `on` / `addListener` methods are +// bound to the real EventEmitter. Any modifications made to process.on here +// would only shadow the copy's own `on` property, and jest-circus still uses +// the underlying bound method — so filtering here has no effect. +// +// The custom environment's setup() method patches this.global.process.on (the +// exact object that jest-circus receives as parentProcess) before jest-circus +// installs its unhandledRejection handler, which is what actually works. diff --git a/package-lock.json b/package-lock.json index b411373f..7a987293 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "3.6.1", "license": "apache-2.0", "dependencies": { + "@dapr/durabletask-js": "^1.0.0", "@grpc/grpc-js": "^1.9.3", "@js-temporal/polyfill": "^0.3.0", "@types/google-protobuf": "^3.15.5", @@ -20,6 +21,7 @@ "node-fetch": "^2.6.7" }, "devDependencies": { + "@dapr/testcontainer-node": "^0.5.1", "@types/body-parser": "^1.19.1", "@types/express": "^4.17.15", "@types/jest": "^27.0.1", @@ -550,38 +552,19 @@ "node": ">=6.9.0" } }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "dev": true, + "license": "Apache-2.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/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "node_modules/@dapr/durabletask-js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@dapr/durabletask-js/-/durabletask-js-1.0.0.tgz", @@ -592,6 +575,17 @@ "google-protobuf": "^3.21.2" } }, + "node_modules/@dapr/testcontainer-node": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@dapr/testcontainer-node/-/testcontainer-node-0.5.1.tgz", + "integrity": "sha512-2g86ssTUdbMV2OgvJKOmUgt5HxTGV4gIMhcFkkrFWIgRoL1QcjJQ+L6DyzVUBswTNwmPPFSM2bLOkXjeGIK4Pw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "testcontainers": "^11.5.1", + "yaml": "^2.8.1" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -717,6 +711,109 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1135,6 +1232,16 @@ "node": ">=12" } }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1170,6 +1277,17 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -1321,6 +1439,29 @@ "@types/node": "*" } }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-4.0.1.tgz", + "integrity": "sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, "node_modules/@types/express": { "version": "4.17.18", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz", @@ -1476,6 +1617,36 @@ "@types/node": "*" } }, + "node_modules/@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2-streams": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.13.tgz", + "integrity": "sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -1703,6 +1874,19 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1893,6 +2077,92 @@ "node": ">= 8" } }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1931,11 +2201,50 @@ "node": ">=8" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/babel-jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", @@ -2034,6 +2343,134 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.0.tgz", + "integrity": "sha512-xzqKsCFxAek9aezYhjJuJRXBIaYlg/0OGDTZp+T8eYmYMlm66cs6cYko02drIyjN2CBbi+I6L7YfXyqpqtKRXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.7.tgz", + "integrity": "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.0.tgz", + "integrity": "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", + "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -2051,6 +2488,58 @@ "node": ">=8" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/body-parser": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", @@ -2201,12 +2690,67 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.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/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2344,6 +2888,13 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, "node_modules/ci-info": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", @@ -2422,6 +2973,23 @@ "node": ">= 0.8" } }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2467,6 +3035,55 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2521,12 +3138,13 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2663,6 +3281,114 @@ "node": ">=8" } }, + "node_modules/docker-compose": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.4.2.tgz", + "integrity": "sha512-rPHigTKGaEHpkUmfd69QgaOp+Os5vGJwG/Ry8lcr8W/382AmI+z/D7qoa9BybKIkqNppaIbs8RYeHSevdQjWww==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/docker-modem": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.7.tgz", + "integrity": "sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.15.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/docker-modem/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/dockerode": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.10.tgz", + "integrity": "sha512-8L/P9JynLBiG7/coiA4FlQXegHltRqS0a+KqI44P1zgQh8QLHTg7FKOwhkBgSJwZTeHsq30WRoVFLuwkfK0YFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.7", + "protobufjs": "^7.3.2", + "tar-fs": "^2.1.4", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dockerode/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/dockerode/node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/dockerode/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2709,6 +3435,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3107,6 +3840,36 @@ "node": ">= 0.6" } }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3237,6 +4000,13 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -3438,6 +4208,36 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -3470,6 +4270,13 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3548,6 +4355,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-port": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.2.0.tgz", + "integrity": "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -3911,6 +4731,27 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -4104,6 +4945,13 @@ "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "dev": true }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4185,6 +5033,22 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", @@ -4974,6 +5838,52 @@ "node": ">=6" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/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, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -5209,6 +6119,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -5219,10 +6162,10 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/multimatch": { "version": "4.0.0", @@ -5240,6 +6183,14 @@ "node": ">=8" } }, + "node_modules/nan": { + "version": "2.26.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz", + "integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5551,6 +6502,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5629,6 +6587,30 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -5933,6 +6915,23 @@ "node": ">=8" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -5946,6 +6945,36 @@ "node": ">= 6" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/properties-reader": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/properties-reader/-/properties-reader-3.0.1.tgz", + "integrity": "sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "mkdirp": "^3.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/properties?sponsor=1" + } + }, "node_modules/protobufjs": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", @@ -6108,6 +7137,56 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6198,6 +7277,16 @@ "node": ">=10" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -6397,12 +7486,6 @@ "node": ">= 0.8" } }, - "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==", - "license": "MIT" - }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -6565,12 +7648,59 @@ "source-map": "^0.6.0" } }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", + "dev": true, + "license": "ISC" + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/ssh-remote-port-forward": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz", + "integrity": "sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ssh2": "^0.5.48", + "ssh2": "^1.4.0" + } + }, + "node_modules/ssh-remote-port-forward/node_modules/@types/ssh2": { + "version": "0.5.52", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz", + "integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2-streams": "*" + } + }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -6600,6 +7730,28 @@ "node": ">= 0.8" } }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -6626,6 +7778,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "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, + "license": "MIT", + "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", @@ -6637,6 +7805,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "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, + "license": "MIT", + "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", @@ -6710,6 +7892,44 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -6740,6 +7960,40 @@ "node": ">=8" } }, + "node_modules/testcontainers": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-11.14.0.tgz", + "integrity": "sha512-r9pniwv/iwzyHaI7gwAvAm4Y+IvjJg3vBWdjrUCaDMc2AXIr4jKbq7jJO18Mw2ybs73pZy1Aj7p/4RVBGMRWjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@types/dockerode": "^4.0.1", + "archiver": "^7.0.1", + "async-lock": "^1.4.1", + "byline": "^5.0.0", + "debug": "^4.4.3", + "docker-compose": "^1.4.2", + "dockerode": "^4.0.10", + "get-port": "^7.2.0", + "proper-lockfile": "^4.1.2", + "properties-reader": "^3.0.1", + "ssh-remote-port-forward": "^1.0.4", + "tar-fs": "^3.1.2", + "tmp": "^0.2.5", + "undici": "^7.24.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -6752,6 +8006,16 @@ "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==", "dev": true }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -6887,6 +8151,13 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6973,6 +8244,23 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/undici": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.8.tgz", + "integrity": "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -7038,6 +8326,13 @@ "requires-port": "^1.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -7046,6 +8341,20 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", @@ -7183,6 +8492,25 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "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, + "license": "MIT", + "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", @@ -7249,6 +8577,22 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -7296,6 +8640,21 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } } }, "dependencies": { @@ -7674,37 +9033,18 @@ "@babel/helper-validator-identifier": "^7.27.1" } }, + "@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "dev": true + }, "@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 }, - "@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "dependencies": { - "@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - } - } - }, "@dapr/durabletask-js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@dapr/durabletask-js/-/durabletask-js-1.0.0.tgz", @@ -7714,6 +9054,16 @@ "google-protobuf": "^3.21.2" } }, + "@dapr/testcontainer-node": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@dapr/testcontainer-node/-/testcontainer-node-0.5.1.tgz", + "integrity": "sha512-2g86ssTUdbMV2OgvJKOmUgt5HxTGV4gIMhcFkkrFWIgRoL1QcjJQ+L6DyzVUBswTNwmPPFSM2bLOkXjeGIK4Pw==", + "dev": true, + "requires": { + "testcontainers": "^11.5.1", + "yaml": "^2.8.1" + } + }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -7800,6 +9150,71 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "requires": { + "ansi-regex": "^6.2.2" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -8127,6 +9542,15 @@ "tslib": "^2.3.1" } }, + "@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "requires": { + "debug": "^4.1.1" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -8153,6 +9577,13 @@ "fastq": "^1.6.0" } }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -8291,6 +9722,27 @@ "@types/node": "*" } }, + "@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "@types/dockerode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-4.0.1.tgz", + "integrity": "sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==", + "dev": true, + "requires": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, "@types/express": { "version": "4.17.18", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz", @@ -8446,6 +9898,35 @@ "@types/node": "*" } }, + "@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "requires": { + "@types/node": "^18.11.18" + }, + "dependencies": { + "@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + } + } + }, + "@types/ssh2-streams": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.13.tgz", + "integrity": "sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -8584,6 +10065,15 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "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==", + "dev": true, + "requires": { + "event-target-shim": "^5.0.0" + } + }, "accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -8717,6 +10207,70 @@ "picomatch": "^2.0.4" } }, + "archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "requires": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + } + }, + "archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "requires": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.2" + } + } + } + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -8746,11 +10300,39 @@ "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", "dev": true }, + "asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "dev": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "dev": true, + "requires": {} + }, "babel-jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", @@ -8828,6 +10410,75 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "requires": {} + }, + "bare-fs": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.0.tgz", + "integrity": "sha512-xzqKsCFxAek9aezYhjJuJRXBIaYlg/0OGDTZp+T8eYmYMlm66cs6cYko02drIyjN2CBbi+I6L7YfXyqpqtKRXA==", + "dev": true, + "requires": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + } + }, + "bare-os": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.7.tgz", + "integrity": "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==", + "dev": true + }, + "bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "requires": { + "bare-os": "^3.0.1" + } + }, + "bare-stream": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.0.tgz", + "integrity": "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==", + "dev": true, + "requires": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + } + }, + "bare-url": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", + "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", + "dev": true, + "requires": { + "bare-path": "^3.0.0" + } + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, "big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -8839,6 +10490,40 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "body-parser": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", @@ -8950,12 +10635,41 @@ "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==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true + }, "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 }, + "buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "dev": true, + "optional": true + }, + "byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", + "dev": true + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -9040,6 +10754,12 @@ } } }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, "ci-info": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", @@ -9095,6 +10815,19 @@ "delayed-stream": "~1.0.0" } }, + "compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "requires": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -9130,6 +10863,39 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "dev": true, + "optional": true, + "requires": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + } + }, + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true + }, + "crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "requires": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + } + }, "cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -9176,12 +10942,12 @@ } }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "decimal.js": { @@ -9269,6 +11035,93 @@ "path-type": "^4.0.0" } }, + "docker-compose": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.4.2.tgz", + "integrity": "sha512-rPHigTKGaEHpkUmfd69QgaOp+Os5vGJwG/Ry8lcr8W/382AmI+z/D7qoa9BybKIkqNppaIbs8RYeHSevdQjWww==", + "dev": true, + "requires": { + "yaml": "^2.2.2" + } + }, + "docker-modem": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.7.tgz", + "integrity": "sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.15.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "dockerode": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.10.tgz", + "integrity": "sha512-8L/P9JynLBiG7/coiA4FlQXegHltRqS0a+KqI44P1zgQh8QLHTg7FKOwhkBgSJwZTeHsq30WRoVFLuwkfK0YFg==", + "dev": true, + "requires": { + "@balena/dockerignore": "^1.0.2", + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.7", + "protobufjs": "^7.3.2", + "tar-fs": "^2.1.4", + "uuid": "^10.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + } + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -9305,6 +11158,12 @@ "gopd": "^1.2.0" } }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -9585,6 +11444,27 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "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==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true + }, + "events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "requires": { + "bare-events": "^2.7.0" + } + }, "execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -9692,6 +11572,12 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, "fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -9864,6 +11750,24 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } + } + }, "form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -9886,6 +11790,12 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -9938,6 +11848,12 @@ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, + "get-port": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.2.0.tgz", + "integrity": "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==", + "dev": true + }, "get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -10190,6 +12106,12 @@ "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==", + "dev": true + }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -10328,6 +12250,12 @@ "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "dev": true }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -10393,6 +12321,16 @@ "istanbul-lib-report": "^3.0.0" } }, + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, "jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", @@ -11016,6 +12954,47 @@ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true }, + "lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "requires": { + "readable-stream": "^2.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "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 + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -11189,6 +13168,24 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, + "minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true + }, + "mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, "mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -11196,10 +13193,9 @@ "dev": true }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "multimatch": { "version": "4.0.0", @@ -11214,6 +13210,13 @@ "minimatch": "^3.0.4" } }, + "nan": { + "version": "2.26.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz", + "integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==", + "dev": true, + "optional": true + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -11438,6 +13441,12 @@ "p-timeout": "^3.0.0" } }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -11494,6 +13503,24 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + } + } + }, "path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -11706,6 +13733,18 @@ } } }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, "prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -11716,6 +13755,27 @@ "sisteransi": "^1.0.5" } }, + "proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "properties-reader": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/properties-reader/-/properties-reader-3.0.1.tgz", + "integrity": "sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==", + "dev": true, + "requires": { + "@kwsites/file-exists": "^1.1.1", + "mkdirp": "^3.0.1" + } + }, "protobufjs": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", @@ -11832,6 +13892,48 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "requires": { + "minimatch": "^5.1.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -11897,6 +13999,12 @@ "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==", "dev": true }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -12033,11 +14141,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" } } }, @@ -12159,12 +14262,52 @@ "source-map": "^0.6.0" } }, + "split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", + "dev": true + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "ssh-remote-port-forward": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz", + "integrity": "sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==", + "dev": true, + "requires": { + "@types/ssh2": "^0.5.48", + "ssh2": "^1.4.0" + }, + "dependencies": { + "@types/ssh2": { + "version": "0.5.52", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz", + "integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/ssh2-streams": "*" + } + } + } + }, + "ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "dev": true, + "requires": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2", + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, "stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -12187,6 +14330,26 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, + "streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "dev": true, + "requires": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, "string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -12207,6 +14370,17 @@ "strip-ansi": "^6.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@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, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -12215,6 +14389,15 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, "strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -12264,6 +14447,39 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "dev": true, + "requires": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "dev": true, + "requires": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "requires": { + "streamx": "^2.12.5" + } + }, "terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -12285,6 +14501,38 @@ "minimatch": "^3.0.4" } }, + "testcontainers": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-11.14.0.tgz", + "integrity": "sha512-r9pniwv/iwzyHaI7gwAvAm4Y+IvjJg3vBWdjrUCaDMc2AXIr4jKbq7jJO18Mw2ybs73pZy1Aj7p/4RVBGMRWjg==", + "dev": true, + "requires": { + "@balena/dockerignore": "^1.0.2", + "@types/dockerode": "^4.0.1", + "archiver": "^7.0.1", + "async-lock": "^1.4.1", + "byline": "^5.0.0", + "debug": "^4.4.3", + "docker-compose": "^1.4.2", + "dockerode": "^4.0.10", + "get-port": "^7.2.0", + "proper-lockfile": "^4.1.2", + "properties-reader": "^3.0.1", + "ssh-remote-port-forward": "^1.0.4", + "tar-fs": "^3.1.2", + "tmp": "^0.2.5", + "undici": "^7.24.5" + } + }, + "text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "requires": { + "b4a": "^1.6.4" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -12297,6 +14545,12 @@ "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==", "dev": true }, + "tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -12385,6 +14639,12 @@ } } }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -12443,6 +14703,18 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "undici": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.8.tgz", + "integrity": "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==", + "dev": true + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -12482,11 +14754,23 @@ "requires-port": "^1.0.0" } }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, + "uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true + }, "v8-to-istanbul": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", @@ -12595,6 +14879,17 @@ "strip-ansi": "^6.0.0" } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -12643,6 +14938,12 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true + }, "yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -12675,6 +14976,17 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "requires": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + } } } } diff --git a/package.json b/package.json index 9b50bb26..5a50f412 100644 --- a/package.json +++ b/package.json @@ -7,21 +7,21 @@ "test": "npm run test:unit:all && npm run test:e2e:all", "test:load": "jest --runInBand --detectOpenHandles", "test:load:http": "TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol http --app-port 50001 --dapr-http-port 50000 --resources-path ./test/components -- npm run test:load 'test/load'", - "test:e2e": "jest --runInBand --detectOpenHandles", - "test:e2e:all": "npm run test:e2e:http && npm run test:e2e:grpc && npm run test:e2e:common && npm run test:e2e:workflow", + "test:e2e": "jest --runInBand --forceExit", + "test:e2e:all": "npm run prebuild && npm run test:e2e:http && npm run test:e2e:grpc && npm run test:e2e:common && npm run test:e2e:workflow", "test:e2e:grpc": "npm run test:e2e:grpc:client && npm run test:e2e:grpc:server && npm run test:e2e:grpc:clientWithApiToken", - "test:e2e:grpc:client": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol grpc --app-port 50001 --dapr-grpc-port 50000 --resources-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/grpc/*client.test.ts' ]", - "test:e2e:grpc:clientWithApiToken": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 DAPR_API_TOKEN=test dapr run --app-id test-suite --app-protocol grpc --app-port 50001 --dapr-grpc-port 50000 --resources-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/grpc/clientWithApiToken.test.ts' ]", - "test:e2e:grpc:server": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol grpc --app-port 50001 --dapr-grpc-port 50000 --max-body-size 10Mi --resources-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/grpc/*server.test.ts' ]", + "test:e2e:grpc:client": "jest --runInBand --forceExit --testMatch [ '**/test/e2e/grpc/*client.test.ts' ]", + "test:e2e:grpc:clientWithApiToken": "jest --runInBand --forceExit --testMatch [ '**/test/e2e/grpc/clientWithApiToken.test.ts' ]", + "test:e2e:grpc:server": "jest --runInBand --forceExit --testMatch [ '**/test/e2e/grpc/*server.test.ts' ]", "test:e2e:http": "npm run test:e2e:http:client && npm run test:e2e:http:server && npm run test:e2e:http:actors", - "test:e2e:http:client": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol http --app-port 50001 --dapr-http-port 50000 --resources-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/http/(client).test.ts' ]", - "test:e2e:http:server": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol http --app-port 50001 --dapr-http-port 50000 --max-body-size 10Mi --resources-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/http/(server).test.ts' ]", - "test:e2e:http:actors": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol http --app-port 50001 --dapr-http-port 50000 --resources-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/http/actors.test.ts' ]", + "test:e2e:http:client": "jest --runInBand --forceExit --testMatch [ '**/test/e2e/http/(client).test.ts' ]", + "test:e2e:http:server": "jest --runInBand --forceExit --testMatch [ '**/test/e2e/http/(server).test.ts' ]", + "test:e2e:http:actors": "jest --runInBand --forceExit --testMatch [ '**/test/e2e/http/actors.test.ts' ]", "test:e2e:common": "npm run test:e2e:common:client && npm run test:e2e:common:server", - "test:e2e:common:client": "./scripts/test-e2e-common.sh client", - "test:e2e:common:server": "./scripts/test-e2e-common.sh server", - "test:e2e:workflow": "npm run prebuild && dapr run --app-id workflow-test-suite --app-protocol grpc --dapr-grpc-port 4001 --resources-path ./test/components/workflow -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/workflow/workflow.test.ts' ]", - "test:e2e:workflow:internal": "jest test/e2e/workflow --runInBand --detectOpenHandles", + "test:e2e:common:client": "jest --runInBand --forceExit --testMatch [ '**/test/e2e/common/client.test.ts' ]", + "test:e2e:common:server": "jest --runInBand --forceExit --testMatch [ '**/test/e2e/common/server.test.ts' ]", + "test:e2e:workflow": "jest --runInBand --forceExit --testMatch [ '**/test/e2e/workflow/workflow.test.ts' ]", + "test:e2e:workflow:internal": "jest test/e2e/workflow --runInBand --forceExit", "test:e2e:workflow:durabletask": "./scripts/test-e2e-workflow.sh", "test:unit": "jest --runInBand --detectOpenHandles", "test:unit:all": "npm run test:unit:main && npm run test:unit:http && npm run test:unit:grpc && npm run test:unit:common && npm run test:unit:actors && npm run test:unit:logger && npm run test:unit:utils && npm run test:unit:errors && npm run test:unit:scripts && npm run test:unit:workflow", @@ -59,6 +59,7 @@ "node-fetch": "^2.6.7" }, "devDependencies": { + "@dapr/testcontainer-node": "^0.5.1", "@types/body-parser": "^1.19.1", "@types/express": "^4.17.15", "@types/jest": "^27.0.1", diff --git a/src/actors/runtime/ActorRuntime.ts b/src/actors/runtime/ActorRuntime.ts index f3974c53..42454da0 100644 --- a/src/actors/runtime/ActorRuntime.ts +++ b/src/actors/runtime/ActorRuntime.ts @@ -61,6 +61,15 @@ export default class ActorRuntime { return ActorRuntime.instance; } + /** + * Resets the singleton instance. Call this in tests when you need to + * re-initialize the runtime with a different DaprClient (e.g. after a + * testcontainer has started and the real sidecar port is known). + */ + static resetForTesting(): void { + ActorRuntime.instance = undefined as unknown as ActorRuntime; + } + registerActor(actorCls: Class): void { // Create an ActorManager if it hasn't been registered yet if (!this.actorManagers.has(actorCls.name)) { diff --git a/test/e2e/common/client.test.ts b/test/e2e/common/client.test.ts index 5f1bdb40..a74496cc 100644 --- a/test/e2e/common/client.test.ts +++ b/test/e2e/common/client.test.ts @@ -12,6 +12,8 @@ limitations under the License. */ import { randomUUID } from "crypto"; +import { Network, StartedNetwork, StartedTestContainer } from "testcontainers"; +import { DaprContainer, StartedDaprContainer } from "@dapr/testcontainer-node"; import { CommunicationProtocolEnum, DaprClient, @@ -21,49 +23,69 @@ import { } from "../../../src"; import { sleep } from "../../../src/utils/NodeJS.util"; import { LockStatus } from "../../../src/types/lock/UnlockResponse"; +import { + startRedisContainer, + startMongoDbContainer, + buildStateRedisComponent, + buildPubSubRedisComponent, + buildLockRedisComponent, + buildStateMongoDbComponent, + DAPR_TEST_RUNTIME_IMAGE, + DAPR_TEST_PLACEMENT_IMAGE, + DAPR_TEST_SCHEDULER_IMAGE, + runWithCleanupErrorSuppression, +} from "../helpers/containers"; -const daprHost = "127.0.0.1"; -const daprGrpcPort = "50000"; -const daprHttpPort = "3500"; const loggerSettings = { level: LogLevel.Debug, }; -describe("common/client", () => { - let httpClient: DaprClient; - let grpcClient: DaprClient; +describe("common/client/http", () => { + let network: StartedNetwork; + let redisContainer: StartedTestContainer; + let mongoDbContainer: StartedTestContainer; + let daprContainer: StartedDaprContainer; + let client: DaprClient; beforeAll(async () => { - httpClient = new DaprClient({ - daprHost, - daprPort: daprHttpPort, + network = await new Network().start(); + [redisContainer, mongoDbContainer] = await Promise.all([ + startRedisContainer(network), + startMongoDbContainer(network), + ]); + + daprContainer = await new DaprContainer(DAPR_TEST_RUNTIME_IMAGE) + .withPlacementImage(DAPR_TEST_PLACEMENT_IMAGE) + .withSchedulerImage(DAPR_TEST_SCHEDULER_IMAGE) + .withNetwork(network) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(buildStateRedisComponent()) + .withComponent(buildPubSubRedisComponent()) + .withComponent(buildLockRedisComponent()) + .withComponent(buildStateMongoDbComponent()) + .start(); + + client = new DaprClient({ + daprHost: daprContainer.getHost(), + daprPort: daprContainer.getHttpPort().toString(), communicationProtocol: CommunicationProtocolEnum.HTTP, logger: loggerSettings, }); - await httpClient.daprClient.getClient(); - - grpcClient = new DaprClient({ - daprHost, - daprPort: daprGrpcPort, - communicationProtocol: CommunicationProtocolEnum.GRPC, - logger: loggerSettings, - }); - await grpcClient.daprClient.getClient(); - }, 10 * 1000); + await client.daprClient.getClient(); + }, 180 * 1000); afterAll(async () => { - await httpClient.stop(); - await grpcClient.stop(); + await runWithCleanupErrorSuppression(async () => { + await client.stop(); + await daprContainer.stop(); + await mongoDbContainer.stop(); + await redisContainer.stop(); + await network.stop(); + }); }); - // Helper function to run the test for both HTTP and gRPC. - const runIt = (name: string, fn: (client: DaprClient) => void) => { - it("http/" + name, () => fn(httpClient)); - it("grpc/" + name, () => fn(grpcClient)); - }; - describe("client", () => { - runIt("should return isInitialized is true if the sidecar has been started", async (client: DaprClient) => { + it("should return isInitialized is true if the sidecar has been started", async () => { expect(client.getIsInitialized()).toBe(true); }); }); @@ -79,34 +101,34 @@ describe("common/client", () => { id: "A234-1234-1234", }; - runIt("should be able to publish a plain text", async (client: DaprClient) => { + it("should be able to publish a plain text", async () => { const res = await client.pubsub.publish(pubSubName, topic, "Hello World"); expect(res.error).toEqual(undefined); }); - runIt("should be able to publish a JSON", async (client: DaprClient) => { + it("should be able to publish a JSON", async () => { const res = await client.pubsub.publish(pubSubName, topic, { hello: "world" }); expect(res.error).toEqual(undefined); }); - runIt("should be able to publish a cloud event", async (client: DaprClient) => { + it("should be able to publish a cloud event", async () => { const res = await client.pubsub.publish(pubSubName, topic, ce); expect(res.error).toEqual(undefined); }); - runIt("should be able to publish multiple plain text messages", async (client: DaprClient) => { + it("should be able to publish multiple plain text messages", async () => { const messages = ["Hello World", "Hello World 2"]; const res = await client.pubsub.publishBulk(pubSubName, topic, messages); expect(res.failedMessages.length).toEqual(0); }); - runIt("should be able to publish multiple JSON messages", async (client: DaprClient) => { + it("should be able to publish multiple JSON messages", async () => { const messages = [{ hello: "world" }, { hello: "world 2" }]; const res = await client.pubsub.publishBulk(pubSubName, topic, messages); expect(res.failedMessages.length).toEqual(0); }); - runIt("should be able to publish multiple custom bulk publish messages", async (client: DaprClient) => { + it("should be able to publish multiple custom bulk publish messages", async () => { const messages = [ { entryID: "1", @@ -128,7 +150,7 @@ describe("common/client", () => { expect(res.failedMessages.length).toEqual(0); }); - runIt("should fail the entire request on duplicate entry IDs", async (client: DaprClient) => { + it("should fail the entire request on duplicate entry IDs", async () => { const messages = [ { entryID: "1", @@ -152,7 +174,7 @@ describe("common/client", () => { }); describe("distributed lock", () => { - runIt("should be able to acquire a new lock and unlock", async (client: DaprClient) => { + it("should be able to acquire a new lock and unlock", async () => { const resourceId = randomUUID(); const lock = await client.lock.lock("redislock", resourceId, "owner1", 1000); expect(lock.success).toEqual(true); @@ -160,13 +182,13 @@ describe("common/client", () => { expect(unlock.status).toEqual(LockStatus.Success); }); - runIt("should be not be able to unlock when the lock is not acquired", async (client: DaprClient) => { + it("should be not be able to unlock when the lock is not acquired", async () => { const resourceId = randomUUID(); const unlock = await client.lock.unlock("redislock", resourceId, "owner1"); expect(unlock.status).toEqual(LockStatus.LockDoesNotExist); }); - runIt("should be able to acquire a lock after the previous lock is expired", async (client: DaprClient) => { + it("should be able to acquire a lock after the previous lock is expired", async () => { const resourceId = randomUUID(); let lock = await client.lock.lock("redislock", resourceId, "owner1", 5); expect(lock.success).toEqual(true); @@ -175,48 +197,36 @@ describe("common/client", () => { expect(lock.success).toEqual(false); }); - runIt( - "should not be able to acquire a lock when the same lock is acquired by another owner", - async (client: DaprClient) => { - const resourceId = randomUUID(); - const lockOne = await client.lock.lock("redislock", resourceId, "owner1", 5); - expect(lockOne.success).toEqual(true); - const lockTwo = await client.lock.lock("redislock", resourceId, "owner2", 5); - expect(lockTwo.success).toEqual(false); - }, - ); - - runIt( - "should be able to acquire a lock when a different lock is acquired by another owner", - async (client: DaprClient) => { - const lockOne = await client.lock.lock("redislock", randomUUID(), "owner1", 5); - expect(lockOne.success).toEqual(true); - const lockTwo = await client.lock.lock("redislock", randomUUID(), "owner2", 5); - expect(lockTwo.success).toEqual(true); - }, - ); - - runIt( - "should not be able to acquire a lock when that lock is acquired by another owner/process", - async (client: DaprClient) => { - const resourceId = randomUUID(); - const lockOne = await client.lock.lock("redislock", resourceId, "owner3", 5); - expect(lockOne.success).toEqual(true); - const lockTwo = await client.lock.lock("redislock", resourceId, "owner4", 5); - expect(lockTwo.success).toEqual(false); - }, - ); - - runIt( - "should not be able to unlock a lock when that lock is acquired by another owner/process", - async (client: DaprClient) => { - const resourceId = randomUUID(); - const lockOne = await client.lock.lock("redislock", resourceId, "owner5", 5); - expect(lockOne.success).toEqual(true); - const unlock = await client.lock.unlock("redislock", resourceId, "owner6"); - expect(unlock.status).toEqual(LockStatus.LockBelongsToOthers); - }, - ); + it("should not be able to acquire a lock when the same lock is acquired by another owner", async () => { + const resourceId = randomUUID(); + const lockOne = await client.lock.lock("redislock", resourceId, "owner1", 5); + expect(lockOne.success).toEqual(true); + const lockTwo = await client.lock.lock("redislock", resourceId, "owner2", 5); + expect(lockTwo.success).toEqual(false); + }); + + it("should be able to acquire a lock when a different lock is acquired by another owner", async () => { + const lockOne = await client.lock.lock("redislock", randomUUID(), "owner1", 5); + expect(lockOne.success).toEqual(true); + const lockTwo = await client.lock.lock("redislock", randomUUID(), "owner2", 5); + expect(lockTwo.success).toEqual(true); + }); + + it("should not be able to acquire a lock when that lock is acquired by another owner/process", async () => { + const resourceId = randomUUID(); + const lockOne = await client.lock.lock("redislock", resourceId, "owner3", 5); + expect(lockOne.success).toEqual(true); + const lockTwo = await client.lock.lock("redislock", resourceId, "owner4", 5); + expect(lockTwo.success).toEqual(false); + }); + + it("should not be able to unlock a lock when that lock is acquired by another owner/process", async () => { + const resourceId = randomUUID(); + const lockOne = await client.lock.lock("redislock", resourceId, "owner5", 5); + expect(lockOne.success).toEqual(true); + const unlock = await client.lock.unlock("redislock", resourceId, "owner6"); + expect(unlock.status).toEqual(LockStatus.LockBelongsToOthers); + }); }); describe("state", () => { @@ -224,15 +234,12 @@ describe("common/client", () => { const stateStoreMongoDbName = "state-mongodb"; beforeEach(async () => { - await httpClient.state.delete(stateStoreName, "key-1"); - await grpcClient.state.delete(stateStoreName, "key-1"); - await httpClient.state.delete(stateStoreName, "key-2"); - await grpcClient.state.delete(stateStoreName, "key-2"); - await httpClient.state.delete(stateStoreName, "key-3"); - await grpcClient.state.delete(stateStoreName, "key-3"); + await client.state.delete(stateStoreName, "key-1"); + await client.state.delete(stateStoreName, "key-2"); + await client.state.delete(stateStoreName, "key-3"); }); - runIt("should be able to save the state", async (client: DaprClient) => { + it("should be able to save the state", async () => { await client.state.save(stateStoreName, [ { key: "key-1", @@ -252,7 +259,7 @@ describe("common/client", () => { expect(res).toEqual("value-1"); }); - runIt("should be able to add metadata, etag and options", async (client: DaprClient) => { + it("should be able to add metadata, etag and options", async () => { await client.state.save(stateStoreName, [ { key: "key-1", @@ -285,7 +292,7 @@ describe("common/client", () => { expect(res2).toBeFalsy(); }); - runIt("should be able to save the state with request metadata", async (client: DaprClient) => { + it("should be able to save the state with request metadata", async () => { await client.state.save( stateStoreName, [ @@ -332,7 +339,7 @@ describe("common/client", () => { expect(res3.find((r) => r.key === "key-2")?.data).toBeFalsy(); }); - runIt("should be able to get the state in bulk", async (client: DaprClient) => { + it("should be able to get the state in bulk", async () => { await client.state.save(stateStoreName, [ { key: "key-1", @@ -358,7 +365,7 @@ describe("common/client", () => { ); }); - runIt("should be able to delete a key from the state store", async (client: DaprClient) => { + it("should be able to delete a key from the state store", async () => { await client.state.save(stateStoreName, [ { key: "key-1", @@ -379,36 +386,33 @@ describe("common/client", () => { expect(res).toEqual(""); }); - runIt( - "should be able to perform a transaction that replaces a key and deletes another", - async (client: DaprClient) => { - await client.state.transaction(stateStoreName, [ - { - operation: "upsert", - request: { - key: "key-1", - value: "my-new-data-1", - }, + it("should be able to perform a transaction that replaces a key and deletes another", async () => { + await client.state.transaction(stateStoreName, [ + { + operation: "upsert", + request: { + key: "key-1", + value: "my-new-data-1", }, - { - operation: "delete", - request: { - key: "key-3", - }, + }, + { + operation: "delete", + request: { + key: "key-3", }, - ]); + }, + ]); - const resTransactionDelete = await client.state.get(stateStoreName, "key-3"); - const resTransactionUpsert = await client.state.get(stateStoreName, "key-1"); - expect(resTransactionDelete).toEqual(""); - expect(resTransactionUpsert).toEqual("my-new-data-1"); - }, - ); + const resTransactionDelete = await client.state.get(stateStoreName, "key-3"); + const resTransactionUpsert = await client.state.get(stateStoreName, "key-1"); + expect(resTransactionDelete).toEqual(""); + expect(resTransactionUpsert).toEqual("my-new-data-1"); + }); - // TODO: Use runIt when gRPC client supports query state. + // TODO: Use gRPC describe block when gRPC client supports query state. it("should be able to query state", async () => { // First save our data - await httpClient.state.save(stateStoreMongoDbName, [ + await client.state.save(stateStoreMongoDbName, [ { key: "key-1", value: { @@ -499,7 +503,7 @@ describe("common/client", () => { }, ]); - const res = await httpClient.state.query(stateStoreMongoDbName, { + const res = await client.state.query(stateStoreMongoDbName, { filter: { OR: [ { @@ -536,12 +540,12 @@ describe("common/client", () => { expect(res.results.map((i) => i.key).indexOf("key-8")).toBeGreaterThan(-1); for (let i = 1; i <= 8; i++) { - await httpClient.state.delete(stateStoreMongoDbName, `key-${i}`); + await client.state.delete(stateStoreMongoDbName, `key-${i}`); } }); it("should return an empty object when result is empty", async () => { - const result = await httpClient.state.query(stateStoreMongoDbName, { + const result = await client.state.query(stateStoreMongoDbName, { filter: { EQ: { state: "statenotfound" } }, sort: [ { @@ -557,3 +561,373 @@ describe("common/client", () => { }); }); }); + +describe("common/client/grpc", () => { + let network: StartedNetwork; + let redisContainer: StartedTestContainer; + let mongoDbContainer: StartedTestContainer; + let daprContainer: StartedDaprContainer; + let client: DaprClient; + + beforeAll(async () => { + network = await new Network().start(); + [redisContainer, mongoDbContainer] = await Promise.all([ + startRedisContainer(network), + startMongoDbContainer(network), + ]); + + daprContainer = await new DaprContainer(DAPR_TEST_RUNTIME_IMAGE) + .withPlacementImage(DAPR_TEST_PLACEMENT_IMAGE) + .withSchedulerImage(DAPR_TEST_SCHEDULER_IMAGE) + .withNetwork(network) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(buildStateRedisComponent()) + .withComponent(buildPubSubRedisComponent()) + .withComponent(buildLockRedisComponent()) + .withComponent(buildStateMongoDbComponent()) + .start(); + + client = new DaprClient({ + daprHost: daprContainer.getHost(), + daprPort: daprContainer.getGrpcPort().toString(), + communicationProtocol: CommunicationProtocolEnum.GRPC, + logger: loggerSettings, + }); + await client.daprClient.getClient(); + }, 180 * 1000); + + afterAll(async () => { + await runWithCleanupErrorSuppression(async () => { + await client.stop(); + await daprContainer.stop(); + await mongoDbContainer.stop(); + await redisContainer.stop(); + await network.stop(); + }); + }); + + describe("client", () => { + it("should return isInitialized is true if the sidecar has been started", async () => { + expect(client.getIsInitialized()).toBe(true); + }); + }); + + describe("pubsub", () => { + const pubSubName = "pubsub-redis"; + const topic = "test-topic"; + + const ce = { + specversion: "1.0", + type: "com.github.pull.create", + source: "https://github.com/cloudevents/spec/pull", + id: "A234-1234-1234", + }; + + it("should be able to publish a plain text", async () => { + const res = await client.pubsub.publish(pubSubName, topic, "Hello World"); + expect(res.error).toEqual(undefined); + }); + + it("should be able to publish a JSON", async () => { + const res = await client.pubsub.publish(pubSubName, topic, { hello: "world" }); + expect(res.error).toEqual(undefined); + }); + + it("should be able to publish a cloud event", async () => { + const res = await client.pubsub.publish(pubSubName, topic, ce); + expect(res.error).toEqual(undefined); + }); + + it("should be able to publish multiple plain text messages", async () => { + const messages = ["Hello World", "Hello World 2"]; + const res = await client.pubsub.publishBulk(pubSubName, topic, messages); + expect(res.failedMessages.length).toEqual(0); + }); + + it("should be able to publish multiple JSON messages", async () => { + const messages = [{ hello: "world" }, { hello: "world 2" }]; + const res = await client.pubsub.publishBulk(pubSubName, topic, messages); + expect(res.failedMessages.length).toEqual(0); + }); + + it("should be able to publish multiple custom bulk publish messages", async () => { + const messages = [ + { + entryID: "1", + event: "Hello World", + contentType: "text/plain", + }, + { + entryID: "2", + event: { hello: "world" }, + contentType: "application/json", + }, + { + entryID: "3", + event: { ...ce, data: "Hello World", datacontenttype: "text/plain" }, + contentType: "application/cloudevents+json", + }, + ]; + const res = await client.pubsub.publishBulk(pubSubName, topic, messages); + expect(res.failedMessages.length).toEqual(0); + }); + + it("should fail the entire request on duplicate entry IDs", async () => { + const messages = [ + { + entryID: "1", + event: "Hello World", + contentType: "text/plain", + }, + { + entryID: "1", + event: { hello: "world 2" }, + contentType: "application/json", + }, + { + entryID: "3", + event: "Hello World 3", + contentType: "text/plain", + }, + ]; + const res = await client.pubsub.publishBulk(pubSubName, topic, messages); + expect(res.failedMessages.length).toEqual(3); + }); + }); + + describe("distributed lock", () => { + it("should be able to acquire a new lock and unlock", async () => { + const resourceId = randomUUID(); + const lock = await client.lock.lock("redislock", resourceId, "owner1", 1000); + expect(lock.success).toEqual(true); + const unlock = await client.lock.unlock("redislock", resourceId, "owner1"); + expect(unlock.status).toEqual(LockStatus.Success); + }); + + it("should be not be able to unlock when the lock is not acquired", async () => { + const resourceId = randomUUID(); + const unlock = await client.lock.unlock("redislock", resourceId, "owner1"); + expect(unlock.status).toEqual(LockStatus.LockDoesNotExist); + }); + + it("should be able to acquire a lock after the previous lock is expired", async () => { + const resourceId = randomUUID(); + let lock = await client.lock.lock("redislock", resourceId, "owner1", 5); + expect(lock.success).toEqual(true); + await new Promise((resolve) => setTimeout(resolve, 2000)); + lock = await client.lock.lock("redislock", resourceId, "owner2", 5); + expect(lock.success).toEqual(false); + }); + + it("should not be able to acquire a lock when the same lock is acquired by another owner", async () => { + const resourceId = randomUUID(); + const lockOne = await client.lock.lock("redislock", resourceId, "owner1", 5); + expect(lockOne.success).toEqual(true); + const lockTwo = await client.lock.lock("redislock", resourceId, "owner2", 5); + expect(lockTwo.success).toEqual(false); + }); + + it("should be able to acquire a lock when a different lock is acquired by another owner", async () => { + const lockOne = await client.lock.lock("redislock", randomUUID(), "owner1", 5); + expect(lockOne.success).toEqual(true); + const lockTwo = await client.lock.lock("redislock", randomUUID(), "owner2", 5); + expect(lockTwo.success).toEqual(true); + }); + + it("should not be able to acquire a lock when that lock is acquired by another owner/process", async () => { + const resourceId = randomUUID(); + const lockOne = await client.lock.lock("redislock", resourceId, "owner3", 5); + expect(lockOne.success).toEqual(true); + const lockTwo = await client.lock.lock("redislock", resourceId, "owner4", 5); + expect(lockTwo.success).toEqual(false); + }); + + it("should not be able to unlock a lock when that lock is acquired by another owner/process", async () => { + const resourceId = randomUUID(); + const lockOne = await client.lock.lock("redislock", resourceId, "owner5", 5); + expect(lockOne.success).toEqual(true); + const unlock = await client.lock.unlock("redislock", resourceId, "owner6"); + expect(unlock.status).toEqual(LockStatus.LockBelongsToOthers); + }); + }); + + describe("state", () => { + const stateStoreName = "state-redis"; + + beforeEach(async () => { + await client.state.delete(stateStoreName, "key-1"); + await client.state.delete(stateStoreName, "key-2"); + await client.state.delete(stateStoreName, "key-3"); + }); + + it("should be able to save the state", async () => { + await client.state.save(stateStoreName, [ + { + key: "key-1", + value: "value-1", + }, + { + key: "key-2", + value: "value-2", + }, + { + key: "key-3", + value: "value-3", + }, + ]); + + const res = await client.state.get(stateStoreName, "key-1"); + expect(res).toEqual("value-1"); + }); + + it("should be able to add metadata, etag and options", async () => { + await client.state.save(stateStoreName, [ + { + key: "key-1", + value: "value-1", + etag: "1234", + options: { + concurrency: StateConcurrencyEnum.CONCURRENCY_FIRST_WRITE, + consistency: StateConsistencyEnum.CONSISTENCY_STRONG, + }, + metadata: { + hello: "world", + ttlInSeconds: "1", + }, + }, + { + key: "key-2", + value: "value-2", + }, + { + key: "key-3", + value: "value-3", + }, + ]); + + const res1 = await client.state.get(stateStoreName, "key-1"); + expect(res1).toEqual("value-1"); + + await sleep(2000); + const res2 = await client.state.get(stateStoreName, "key-1"); + expect(res2).toBeFalsy(); + }); + + it("should be able to save the state with request metadata", async () => { + await client.state.save( + stateStoreName, + [ + { + key: "key-1", + value: "value-1", + metadata: { + ttlInSeconds: "1", + }, + }, + { + key: "key-2", + value: "value-2", + }, + ], + { + metadata: { + ttlInSeconds: "3", // this should override the ttl in the state item + }, + }, + ); + + const res1 = await client.state.getBulk(stateStoreName, ["key-1", "key-2"]); + expect(res1.length).toEqual(2); + expect(res1.find((r) => r.key === "key-1")?.data).toEqual("value-1"); + expect(res1.find((r) => r.key === "key-2")?.data).toEqual("value-2"); + + // wait for the first ttl to expire + await sleep(1500); + + // key-1 should still be there since its TTL is overridden by the request metadata + const res2 = await client.state.getBulk(stateStoreName, ["key-1", "key-2"]); + expect(res2.length).toEqual(2); + expect(res2.find((r) => r.key === "key-1")?.data).toEqual("value-1"); + expect(res2.find((r) => r.key === "key-2")?.data).toEqual("value-2"); + + // wait for the second ttl to expire + await sleep(2000); + + const res3 = await client.state.getBulk(stateStoreName, ["key-1", "key-2"]); + expect(res3.length).toEqual(2); + // HTTP returns undefined, gRPC returns "" for non-existent keys + expect(res3.find((r) => r.key === "key-1")?.data).toBeFalsy(); + expect(res3.find((r) => r.key === "key-2")?.data).toBeFalsy(); + }); + + it("should be able to get the state in bulk", async () => { + await client.state.save(stateStoreName, [ + { + key: "key-1", + value: "value-1", + }, + { + key: "key-2", + value: "value-2", + }, + { + key: "key-3", + value: "value-3", + }, + ]); + + const res = await client.state.getBulk(stateStoreName, ["key-3", "key-2"]); + + expect(res).toEqual( + expect.arrayContaining([ + expect.objectContaining({ key: "key-2", data: "value-2" }), + expect.objectContaining({ key: "key-3", data: "value-3" }), + ]), + ); + }); + + it("should be able to delete a key from the state store", async () => { + await client.state.save(stateStoreName, [ + { + key: "key-1", + value: "value-1", + }, + { + key: "key-2", + value: "value-2", + }, + { + key: "key-3", + value: "value-3", + }, + ]); + + await client.state.delete(stateStoreName, "key-2"); + const res = await client.state.get(stateStoreName, "key-2"); + expect(res).toEqual(""); + }); + + it("should be able to perform a transaction that replaces a key and deletes another", async () => { + await client.state.transaction(stateStoreName, [ + { + operation: "upsert", + request: { + key: "key-1", + value: "my-new-data-1", + }, + }, + { + operation: "delete", + request: { + key: "key-3", + }, + }, + ]); + + const resTransactionDelete = await client.state.get(stateStoreName, "key-3"); + const resTransactionUpsert = await client.state.get(stateStoreName, "key-1"); + expect(resTransactionDelete).toEqual(""); + expect(resTransactionUpsert).toEqual("my-new-data-1"); + }); + }); +}); diff --git a/test/e2e/common/server.test.ts b/test/e2e/common/server.test.ts index c962b282..2d859715 100644 --- a/test/e2e/common/server.test.ts +++ b/test/e2e/common/server.test.ts @@ -11,20 +11,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { CommunicationProtocolEnum, DaprServer, DaprPubSubStatusEnum } from "../../../src"; -import * as NodeJSUtil from "../../../src/utils/NodeJS.util"; - -const serverHost = "127.0.0.1"; -const serverGrpcPort = "50001"; -const serverHttpPort = "3501"; -const daprHost = "127.0.0.1"; -const daprGrpcPort = "50000"; -const daprHttpPort = "3500"; -const customPort = "50002"; - -const protocolHttp = "http"; -const protocolGrpc = "grpc"; -const pubSubName = "pubsub-mqtt"; // MQTT is required by the tests with wilcard routes +import { Network, StartedNetwork, StartedTestContainer, TestContainers } from "testcontainers"; +import { DaprContainer, StartedDaprContainer } from "@dapr/testcontainer-node"; +import { CommunicationProtocolEnum, DaprClient, DaprServer, DaprPubSubStatusEnum } from "../../../src"; +import { DaprGrpcAppContainer, StartedGrpcDaprContainer } from "../helpers/DaprGrpcAppContainer"; +import { + startRedisContainer, + startMqttContainer, + buildPubSubMqttComponent, + DAPR_TEST_RUNTIME_IMAGE, + DAPR_TEST_PLACEMENT_IMAGE, + DAPR_TEST_SCHEDULER_IMAGE, + runWithCleanupErrorSuppression, +} from "../helpers/containers"; + +const pubSubName = "pubsub-mqtt"; // MQTT is required by the tests with wildcard routes const topicDefault = "test-topic"; const topicRawPayload = "test-topic-raw"; const topicOptionsWithCallback = "test-topic-options-callback"; @@ -50,164 +51,232 @@ const bulkSubscribeClodEventTopic = "bulk-subscribe-ce-topic"; const bulkSubscribeCloudEventToRawPayloadTopic = "bulk-subscribe-ce-rp-topic"; const bulkSubscribeRawPayloadToClodEventTopic = "bulk-subscribe-rp-ce-topic"; -// Set timeout to 10s for all tests -jest.setTimeout(10000); +const sampleRoutes = { + default: "/default", + rules: [ + { + match: `event.type == "type-1"`, + path: "/type-1", + }, + { + match: `event.type == "type-2"`, + path: "/type-2", + }, + ], +}; + +function getDataFromCEObject(obj: object) { + const values = Object.values(obj); + return values[0]; +} + +describe("common/server/http", () => { + jest.setTimeout(30000); -describe("common/server", () => { + let network: StartedNetwork; + let redisContainer: StartedTestContainer; + let mqttContainer: StartedTestContainer; + let daprContainer: StartedDaprContainer; let httpServer: DaprServer; - let grpcServer: DaprServer; - const getTopic = (topic: string, protocol: string) => protocol + "-" + topic; + const protocol = "http"; + const appPort = 3501; + const getTopic = (topic: string) => protocol + "-" + topic; + const mockSubscribeHandler = jest.fn(async (_data: object, _headers: object) => null); const mockBulkSubscribeRawPayloadHandler = jest.fn(async (_data: object, _headers: object) => null); const mockBulkSubscribeCEHandler = jest.fn(async (_data: object, _headers: object) => null); const mockBulkSubscribeCloudEventToRawPayloadHandler = jest.fn(async (_data: object, _headers: object) => null); const mockBulkSubscribeRawPayloadToCloudEventHandler = jest.fn(async (_data: object, _headers: object) => null); const mockSubscribeDeadletterHandler = jest.fn(async (_data: object, _headers: object) => null); + const mockSubscribeErrorHandler = jest.fn(async (_data: object, _headers: object) => { + throw new Error("This will DROP the message!"); + }); - const mockSubscribeStatusHandlerVars: { - [protocol: string]: { - counter: number; - }; - } = { - http: { - counter: 0, - }, - grpc: { - counter: 0, - }, - }; - - const mockSubscribeStatusHandler = jest.fn(async (_protocol: string, _data: string, _headers: object) => { + const statusHandlerVars = { counter: 0 }; + const mockSubscribeStatusHandler = jest.fn(async (_data: string, _headers: object) => { switch (_data) { case "DROP": return DaprPubSubStatusEnum.DROP; case "TEST_RETRY_TWICE": - if (mockSubscribeStatusHandlerVars[_protocol]["counter"] < 2) { - mockSubscribeStatusHandlerVars[_protocol]["counter"]++; + if (statusHandlerVars.counter < 2) { + statusHandlerVars.counter++; return DaprPubSubStatusEnum.RETRY; } - - // Once we reach the SUCCESS, we reset it - // we can check the state of # calls in the mock - mockSubscribeStatusHandlerVars[_protocol]["counter"] = 0; - + // Once we reach SUCCESS, reset the counter + statusHandlerVars.counter = 0; return DaprPubSubStatusEnum.SUCCESS; default: - mockSubscribeStatusHandlerVars[_protocol]["counter"] = 0; + statusHandlerVars.counter = 0; return DaprPubSubStatusEnum.SUCCESS; } }); - const mockSubscribeErrorHandler = jest.fn(async (_data: object, _headers: object) => { - throw new Error("This will DROP the message!"); - }); + beforeAll(async () => { + network = await new Network().start(); + [redisContainer, mqttContainer] = await Promise.all([startRedisContainer(network), startMqttContainer(network)]); - const sampleRoutes = { - default: "/default", - rules: [ - { - match: `event.type == "type-1"`, - path: "/type-1", - }, - { - match: `event.type == "type-2"`, - path: "/type-2", - }, - ], - }; + await TestContainers.exposeHostPorts(appPort); - beforeAll(async () => { httpServer = new DaprServer({ - serverHost, - serverPort: serverHttpPort, + serverHost: "127.0.0.1", + serverPort: "3501", communicationProtocol: CommunicationProtocolEnum.HTTP, clientOptions: { - daprHost, - daprPort: daprHttpPort, + // Placeholder — replaced with real container ports after daprContainer starts below. + daprHost: "127.0.0.1", + daprPort: "3500", }, }); - grpcServer = new DaprServer({ - serverHost, - serverPort: serverGrpcPort, - communicationProtocol: CommunicationProtocolEnum.GRPC, - clientOptions: { - daprHost, - daprPort: daprGrpcPort, - }, + + // Register all subscriptions before starting the server + await httpServer.pubsub.subscribe(pubSubName, getTopic(topicDefault), mockSubscribeHandler); + + await httpServer.pubsub.subscribeBulk(pubSubName, getTopic(bulkSubscribeTopic), mockBulkSubscribeRawPayloadHandler, { + metadata: { rawPayload: true }, }); - await setupPubSubSubscriptions(); - // Sleep to make the tests less flaky. - // TODO: https://github.com/dapr/js-sdk/issues/560 - await NodeJSUtil.sleep(2000); + await httpServer.pubsub.subscribeBulk(pubSubName, getTopic(bulkSubscribeClodEventTopic), mockBulkSubscribeCEHandler); - await httpServer.start(); - await grpcServer.start(); - // Sleep for 2 seconds to get servers ready - await NodeJSUtil.sleep(2000); - }); + await httpServer.pubsub.subscribeBulk( + pubSubName, + getTopic(bulkSubscribeCloudEventToRawPayloadTopic), + mockBulkSubscribeCloudEventToRawPayloadHandler, + { metadata: { rawPayload: true } }, + ); + + await httpServer.pubsub.subscribeBulk( + pubSubName, + getTopic(bulkSubscribeRawPayloadToClodEventTopic), + mockBulkSubscribeRawPayloadToCloudEventHandler, + ); + + await httpServer.pubsub.subscribe(pubSubName, getTopic(topicRawPayload), mockSubscribeHandler, undefined, { + rawPayload: true, + }); + + await httpServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicOptionsWithCallback), { + callback: mockSubscribeHandler, + }); + + await httpServer.pubsub.subscribe(pubSubName, getTopic(topicSimpleRoute), mockSubscribeHandler, routeSimple); + + await httpServer.pubsub.subscribe( + pubSubName, + getTopic(topicLeadingSlashRoute), + mockSubscribeHandler, + routeWithLeadingSlash, + ); + + await httpServer.pubsub.subscribe(pubSubName, getTopic(topicCustomRules), mockSubscribeHandler, sampleRoutes); + + await httpServer.pubsub.subscribe(pubSubName, getTopic(topicWildcardHash), mockSubscribeHandler); + await httpServer.pubsub.subscribe(pubSubName, getTopic(topicWildcardPlus), mockSubscribeHandler); + await httpServer.pubsub.subscribe(pubSubName, getTopic(topicWithBulk), mockSubscribeHandler); + + await httpServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicCustomRulesInOptions), { + route: sampleRoutes, + }); + + await httpServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithoutCallback), {}); + + await httpServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithDeadletter), { + deadLetterTopic: getTopic(deadLetterTopic), + }); + + await httpServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithDeadletterInOptions), { + deadLetterTopic: getTopic(deadLetterTopic), + deadLetterCallback: mockSubscribeHandler, + }); + + await httpServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithDeadletterInOptionsDefault), { + deadLetterCallback: mockSubscribeHandler, + }); + + await httpServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithDeadletterAndErrorCb), { + deadLetterCallback: mockSubscribeHandler, + callback: mockSubscribeErrorHandler, + }); + + // Configure subscriptions for Status Message testing + // to test, we use the message as the status (SUCCESS, RETRY, DROP, Other) + await httpServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithStatusCb), { + deadLetterCallback: mockSubscribeDeadletterHandler, + callback: (_data, _headers) => mockSubscribeStatusHandler(_data as string, _headers), + }); + + // Start ONLY the HTTP listener (no sidecar wait) so the app is already + // listening when the Dapr container probes it during its own init. + await httpServer.daprServer.start("127.0.0.1", "3501"); + + daprContainer = await new DaprContainer(DAPR_TEST_RUNTIME_IMAGE) + .withPlacementImage(DAPR_TEST_PLACEMENT_IMAGE) + .withSchedulerImage(DAPR_TEST_SCHEDULER_IMAGE) + .withNetwork(network) + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(buildPubSubMqttComponent()) + .start(); + + // Patch the DaprClient with real container ports, then wait for sidecar. + (httpServer as any).client = new DaprClient({ + daprHost: daprContainer.getHost(), + daprPort: daprContainer.getHttpPort().toString(), + communicationProtocol: CommunicationProtocolEnum.HTTP, + }); + await httpServer.client.start(); + }, 300 * 1000); beforeEach(() => { jest.clearAllMocks(); }); afterAll(async () => { - await httpServer.stop(); - await grpcServer.stop(); - }); + await runWithCleanupErrorSuppression(async () => { + await httpServer.stop(); + await daprContainer.stop(); + await mqttContainer.stop(); + await redisContainer.stop(); + await network.stop(); + }); + }, 60 * 1000); - // Helper function to run the test for both HTTP and gRPC. - const runIt = async (name: string, fn: (server: DaprServer, protocol: string) => void) => { - it(protocolHttp + "/" + name, async () => fn(httpServer, protocolHttp)); - it(protocolGrpc + "/" + name, async () => fn(grpcServer, protocolGrpc)); - }; describe("pubsub", () => { - runIt( - "should mark messages as processed successfully (SUCCESS) by-default, and the same message should not be received anymore", - async (server: DaprServer, protocol: string) => { - const res = await server.client.pubsub.publish(pubSubName, getTopic(topicWithStatusCb, protocol), "SUCCESS"); - expect(res.error).toBeUndefined(); - // Delay a bit for event to arrive - await new Promise((resolve, _reject) => setTimeout(resolve, 2000)); - expect(mockSubscribeStatusHandler.mock.calls.length).toBe(1); - expect(mockSubscribeStatusHandler.mock.calls[0][1]).toEqual("SUCCESS"); - expect(mockSubscribeDeadletterHandler.mock.calls.length).toBe(0); - }, - ); + it("should mark messages as processed successfully (SUCCESS) by-default, and the same message should not be received anymore", async () => { + const res = await httpServer.client.pubsub.publish(pubSubName, getTopic(topicWithStatusCb), "SUCCESS"); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 2000)); + expect(mockSubscribeStatusHandler.mock.calls.length).toBe(1); + expect(mockSubscribeStatusHandler.mock.calls[0][0]).toEqual("SUCCESS"); + expect(mockSubscribeDeadletterHandler.mock.calls.length).toBe(0); + }); - runIt( - "should mark messages as retried (RETRY), and the same message should be received again until we send SUCCESS", - async (server: DaprServer, protocol: string) => { - const res = await server.client.pubsub.publish( - pubSubName, - getTopic(topicWithStatusCb, protocol), - "TEST_RETRY_TWICE", - ); - expect(res.error).toBeUndefined(); - // Delay a bit for event to arrive - await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); - // 3 as we retry twice - expect(mockSubscribeStatusHandler.mock.calls.length).toBe(3); - expect(mockSubscribeDeadletterHandler.mock.calls.length).toBe(0); - }, - ); + it("should mark messages as retried (RETRY), and the same message should be received again until we send SUCCESS", async () => { + const res = await httpServer.client.pubsub.publish(pubSubName, getTopic(topicWithStatusCb), "TEST_RETRY_TWICE"); + expect(res.error).toBeUndefined(); + // Poll until all 3 deliveries arrive (EMQX QoS1 retry interval is 3 s; poll up to 45 s for CI headroom) + const deadline = Date.now() + 45_000; + while (mockSubscribeStatusHandler.mock.calls.length < 3 && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 500)); + } + // 3 as we retry twice + expect(mockSubscribeStatusHandler.mock.calls.length).toBe(3); + expect(mockSubscribeDeadletterHandler.mock.calls.length).toBe(0); + }, 60_000); - runIt( - "should mark messages as dropped (DROP), and the message should be deadlettered", - async (server: DaprServer, protocol: string) => { - const res = await server.client.pubsub.publish(pubSubName, getTopic(topicWithStatusCb, protocol), "DROP"); - expect(res.error).toBeUndefined(); - // Delay a bit for event to arrive - await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); - expect(mockSubscribeStatusHandler.mock.calls.length).toBe(1); - expect(mockSubscribeStatusHandler.mock.calls[0][1]).toEqual("DROP"); - }, - ); + it("should mark messages as dropped (DROP), and the message should be deadlettered", async () => { + const res = await httpServer.client.pubsub.publish(pubSubName, getTopic(topicWithStatusCb), "DROP"); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeStatusHandler.mock.calls.length).toBe(1); + expect(mockSubscribeStatusHandler.mock.calls[0][0]).toEqual("DROP"); + }); - runIt("should be able to send and receive plain events", async (server: DaprServer, protocol: string) => { - const res = await server.client.pubsub.publish(pubSubName, getTopic(topicDefault, protocol), "Hello, world!"); + it("should be able to send and receive plain events", async () => { + const res = await httpServer.client.pubsub.publish(pubSubName, getTopic(topicDefault), "Hello, world!"); expect(res.error).toBeUndefined(); // Delay a bit for event to arrive await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); @@ -215,119 +284,93 @@ describe("common/server", () => { expect(mockSubscribeHandler.mock.calls[0][0]).toEqual("Hello, world!"); const headers = mockSubscribeHandler.mock.calls[0][1] as any; expect(headers["pubsubname"]).toEqual(pubSubName); - if (protocol === protocolHttp) { - expect(headers["content-type"]).toEqual("application/cloudevents+json"); - } + expect(headers["content-type"]).toEqual("application/cloudevents+json"); }); - runIt( - "should be able to publish multiple rawPayload messages and receive events using bulk subscribe", - async (server: DaprServer, protocol: string) => { - const res1 = await server.client.pubsub.publish( - pubSubName, - getTopic(bulkSubscribeTopic, protocol), - { - message: "Message 1!", - }, - { metadata: { rawPayload: "true" } }, - ); - const res2 = await server.client.pubsub.publish( - pubSubName, - getTopic(bulkSubscribeTopic, protocol), - { - message: "Message 2!", - }, - { metadata: { rawPayload: "true" } }, - ); - expect(res1.error).toBeUndefined(); - expect(res2.error).toBeUndefined(); - // Delay a bit for event to arrive - await new Promise((resolve, _reject) => setTimeout(resolve, 4000)); - expect(mockBulkSubscribeRawPayloadHandler.mock.calls.length).toBe(2); - expect(mockBulkSubscribeRawPayloadHandler.mock.calls[0][0]).toEqual({ message: "Message 1!" }); - expect(mockBulkSubscribeRawPayloadHandler.mock.calls[1][0]).toEqual({ message: "Message 2!" }); - }, - ); + it("should be able to publish multiple rawPayload messages and receive events using bulk subscribe", async () => { + const res1 = await httpServer.client.pubsub.publish( + pubSubName, + getTopic(bulkSubscribeTopic), + { message: "Message 1!" }, + { metadata: { rawPayload: "true" } }, + ); + const res2 = await httpServer.client.pubsub.publish( + pubSubName, + getTopic(bulkSubscribeTopic), + { message: "Message 2!" }, + { metadata: { rawPayload: "true" } }, + ); + expect(res1.error).toBeUndefined(); + expect(res2.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 4000)); + expect(mockBulkSubscribeRawPayloadHandler.mock.calls.length).toBe(2); + expect(mockBulkSubscribeRawPayloadHandler.mock.calls[0][0]).toEqual({ message: "Message 1!" }); + expect(mockBulkSubscribeRawPayloadHandler.mock.calls[1][0]).toEqual({ message: "Message 2!" }); + }); - runIt( - "should be able to publish multiple CloudEvents and receive events using bulk subscribe", - async (server: DaprServer, protocol: string) => { - const res1 = await server.client.pubsub.publish(pubSubName, getTopic(bulkSubscribeClodEventTopic, protocol), { - message: "Message 1!", - }); - const res2 = await server.client.pubsub.publish(pubSubName, getTopic(bulkSubscribeClodEventTopic, protocol), { - message: "Message 2!", - }); - expect(res1.error).toBeUndefined(); - expect(res2.error).toBeUndefined(); - // Delay a bit for event to arrive - await new Promise((resolve, _reject) => setTimeout(resolve, 4000)); - expect(mockBulkSubscribeCEHandler.mock.calls.length).toBe(2); - expect(mockBulkSubscribeCEHandler.mock.calls[0][0]).toEqual({ message: "Message 1!" }); - expect(mockBulkSubscribeCEHandler.mock.calls[1][0]).toEqual({ message: "Message 2!" }); - }, - ); + it("should be able to publish multiple CloudEvents and receive events using bulk subscribe", async () => { + const res1 = await httpServer.client.pubsub.publish(pubSubName, getTopic(bulkSubscribeClodEventTopic), { + message: "Message 1!", + }); + const res2 = await httpServer.client.pubsub.publish(pubSubName, getTopic(bulkSubscribeClodEventTopic), { + message: "Message 2!", + }); + expect(res1.error).toBeUndefined(); + expect(res2.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 4000)); + expect(mockBulkSubscribeCEHandler.mock.calls.length).toBe(2); + expect(mockBulkSubscribeCEHandler.mock.calls[0][0]).toEqual({ message: "Message 1!" }); + expect(mockBulkSubscribeCEHandler.mock.calls[1][0]).toEqual({ message: "Message 2!" }); + }); - runIt( - "should be able to publish multiple CloudEvents but receive rawPayload events using bulk subscribe", - async (server: DaprServer, protocol: string) => { - const res1 = await server.client.pubsub.publish( - pubSubName, - getTopic(bulkSubscribeCloudEventToRawPayloadTopic, protocol), - { - message: "Message 1!", - }, - ); - const res2 = await server.client.pubsub.publish( - pubSubName, - getTopic(bulkSubscribeCloudEventToRawPayloadTopic, protocol), - { - message: "Message 2!", - }, - ); - expect(res1.error).toBeUndefined(); - expect(res2.error).toBeUndefined(); - // Delay a bit for event to arrive - await new Promise((resolve, _reject) => setTimeout(resolve, 4000)); - expect(mockBulkSubscribeCloudEventToRawPayloadHandler.mock.calls.length).toBe(2); - expect(getDataFromCEObject(mockBulkSubscribeCloudEventToRawPayloadHandler.mock.calls[0][0])).toEqual({ - message: "Message 1!", - }); - expect(getDataFromCEObject(mockBulkSubscribeCloudEventToRawPayloadHandler.mock.calls[1][0])).toEqual({ - message: "Message 2!", - }); - }, - ); + it("should be able to publish multiple CloudEvents but receive rawPayload events using bulk subscribe", async () => { + const res1 = await httpServer.client.pubsub.publish( + pubSubName, + getTopic(bulkSubscribeCloudEventToRawPayloadTopic), + { message: "Message 1!" }, + ); + const res2 = await httpServer.client.pubsub.publish( + pubSubName, + getTopic(bulkSubscribeCloudEventToRawPayloadTopic), + { message: "Message 2!" }, + ); + expect(res1.error).toBeUndefined(); + expect(res2.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 4000)); + expect(mockBulkSubscribeCloudEventToRawPayloadHandler.mock.calls.length).toBe(2); + expect(getDataFromCEObject(mockBulkSubscribeCloudEventToRawPayloadHandler.mock.calls[0][0])).toEqual({ + message: "Message 1!", + }); + expect(getDataFromCEObject(mockBulkSubscribeCloudEventToRawPayloadHandler.mock.calls[1][0])).toEqual({ + message: "Message 2!", + }); + }); - runIt( - "should be able to publish multiple rawPayload messages and receive CloudEvents using bulk subscribe", - async (server: DaprServer, protocol: string) => { - const res1 = await server.client.pubsub.publish( - pubSubName, - getTopic(bulkSubscribeRawPayloadToClodEventTopic, protocol), - { - message: "Message 1!", - }, - { metadata: { rawPayload: "true" } }, - ); - const res2 = await server.client.pubsub.publish( - pubSubName, - getTopic(bulkSubscribeRawPayloadToClodEventTopic, protocol), - { - message: "Message 2!", - }, - { metadata: { rawPayload: "true" } }, - ); - expect(res1.error).toBeUndefined(); - expect(res2.error).toBeUndefined(); - // Delay a bit for event to arrive - await new Promise((resolve, _reject) => setTimeout(resolve, 4000)); - expect(mockBulkSubscribeRawPayloadToCloudEventHandler.mock.calls.length).toBe(2); - }, - ); + it("should be able to publish multiple rawPayload messages and receive CloudEvents using bulk subscribe", async () => { + const res1 = await httpServer.client.pubsub.publish( + pubSubName, + getTopic(bulkSubscribeRawPayloadToClodEventTopic), + { message: "Message 1!" }, + { metadata: { rawPayload: "true" } }, + ); + const res2 = await httpServer.client.pubsub.publish( + pubSubName, + getTopic(bulkSubscribeRawPayloadToClodEventTopic), + { message: "Message 2!" }, + { metadata: { rawPayload: "true" } }, + ); + expect(res1.error).toBeUndefined(); + expect(res2.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 4000)); + expect(mockBulkSubscribeRawPayloadToCloudEventHandler.mock.calls.length).toBe(2); + }); - runIt("should be able to send and receive JSON events", async (server: DaprServer, protocol: string) => { - const res = await server.client.pubsub.publish(pubSubName, getTopic(topicDefault, protocol), { + it("should be able to send and receive JSON events", async () => { + const res = await httpServer.client.pubsub.publish(pubSubName, getTopic(topicDefault), { message: "Hello, world!", }); expect(res.error).toBeUndefined(); @@ -337,7 +380,7 @@ describe("common/server", () => { expect(mockSubscribeHandler.mock.calls[0][0]).toEqual({ message: "Hello, world!" }); }); - runIt("should be able to send and receive cloudevents", async (server: DaprServer, protocol: string) => { + it("should be able to send and receive cloudevents", async () => { const ce = { specversion: "1.0", type: "com.github.pull.create", @@ -346,7 +389,7 @@ describe("common/server", () => { data: "Hello, world!", datacontenttype: "text/plain", }; - const res = await server.client.pubsub.publish(pubSubName, getTopic(topicDefault, protocol), ce); + const res = await httpServer.client.pubsub.publish(pubSubName, getTopic(topicDefault), ce); expect(res.error).toBeUndefined(); // Delay a bit for event to arrive await new Promise((resolve, _reject) => setTimeout(resolve, 500)); @@ -354,7 +397,7 @@ describe("common/server", () => { expect(mockSubscribeHandler.mock.calls[0][0]).toEqual("Hello, world!"); }); - runIt("should be able to send cloudevents as JSON and receive it", async (server: DaprServer, protocol: string) => { + it("should be able to send cloudevents as JSON and receive it", async () => { const ce = { specversion: "1.0", type: "com.github.pull.create", @@ -364,7 +407,7 @@ describe("common/server", () => { datacontenttype: "text/plain", }; const options = { contentType: "application/json" }; - const res = await server.client.pubsub.publish(pubSubName, getTopic(topicDefault, protocol), ce, options); + const res = await httpServer.client.pubsub.publish(pubSubName, getTopic(topicDefault), ce, options); expect(res.error).toBeUndefined(); // Delay a bit for event to arrive await new Promise((resolve, _reject) => setTimeout(resolve, 500)); @@ -374,100 +417,76 @@ describe("common/server", () => { expect(innerCe["data"]).toEqual("Hello, world!"); }); - runIt( - "should be able to send plain events and receive as raw payload", - async (server: DaprServer, protocol: string) => { - const res = await server.client.pubsub.publish( - pubSubName, - getTopic(topicRawPayload, protocol), - "Hello, world!", - ); - expect(res.error).toBeUndefined(); - // Delay a bit for event to arrive - await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); - expect(mockSubscribeHandler.mock.calls.length).toBe(1); - const rawData: any = mockSubscribeHandler.mock.calls[0][0]; - expect(rawData["data"]).toEqual("Hello, world!"); - }, - ); + it("should be able to send plain events and receive as raw payload", async () => { + const res = await httpServer.client.pubsub.publish(pubSubName, getTopic(topicRawPayload), "Hello, world!"); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + const rawData: any = mockSubscribeHandler.mock.calls[0][0]; + expect(rawData["data"]).toEqual("Hello, world!"); + }); - runIt( - "should be able to send JSON events and receive as raw payload", - async (server: DaprServer, protocol: string) => { - const res = await server.client.pubsub.publish(pubSubName, getTopic(topicRawPayload, protocol), { - message: "Hello, world!", - }); - expect(res.error).toBeUndefined(); - // Delay a bit for event to arrive - await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); - expect(mockSubscribeHandler.mock.calls.length).toBe(1); - const rawData: any = mockSubscribeHandler.mock.calls[0][0]; - expect(rawData["data"]).toEqual({ message: "Hello, world!" }); - }, - ); + it("should be able to send JSON events and receive as raw payload", async () => { + const res = await httpServer.client.pubsub.publish(pubSubName, getTopic(topicRawPayload), { + message: "Hello, world!", + }); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + const rawData: any = mockSubscribeHandler.mock.calls[0][0]; + expect(rawData["data"]).toEqual({ message: "Hello, world!" }); + }); - runIt( - "should be able to send and receive plain events as raw payload", - async (server: DaprServer, protocol: string) => { - const res = await server.client.pubsub.publish( - pubSubName, - getTopic(topicRawPayload, protocol), - "Hello, world!", - { metadata: { rawPayload: "true" } }, - ); - expect(res.error).toBeUndefined(); - // Delay a bit for event to arrive - await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); - expect(mockSubscribeHandler.mock.calls.length).toBe(1); - expect(mockSubscribeHandler.mock.calls[0][0]).toEqual("Hello, world!"); - }, - ); + it("should be able to send and receive plain events as raw payload", async () => { + const res = await httpServer.client.pubsub.publish(pubSubName, getTopic(topicRawPayload), "Hello, world!", { + metadata: { rawPayload: "true" }, + }); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + expect(mockSubscribeHandler.mock.calls[0][0]).toEqual("Hello, world!"); + }); - runIt( - "should be able to send and receive JSON events as raw payload", - async (server: DaprServer, protocol: string) => { - const res = await server.client.pubsub.publish( - pubSubName, - getTopic(topicRawPayload, protocol), - { message: "Hello, world!" }, - { metadata: { rawPayload: "true" } }, - ); - expect(res.error).toBeUndefined(); - // Delay a bit for event to arrive - await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); - expect(mockSubscribeHandler.mock.calls.length).toBe(1); - expect(mockSubscribeHandler.mock.calls[0][0]).toEqual({ message: "Hello, world!" }); - }, - ); + it("should be able to send and receive JSON events as raw payload", async () => { + const res = await httpServer.client.pubsub.publish( + pubSubName, + getTopic(topicRawPayload), + { message: "Hello, world!" }, + { metadata: { rawPayload: "true" } }, + ); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + expect(mockSubscribeHandler.mock.calls[0][0]).toEqual({ message: "Hello, world!" }); + }); - runIt( - "should be able to send and receive events when using options callback without a route", - async (server: DaprServer, protocol: string) => { - const res = await server.client.pubsub.publish(pubSubName, getTopic(topicOptionsWithCallback, protocol), { - message: "Hello, world!", - }); - expect(res.error).toBeUndefined(); - // Delay a bit for event to arrive - await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); - expect(mockSubscribeHandler.mock.calls.length).toBe(1); - expect(mockSubscribeHandler.mock.calls[0][0]).toEqual({ message: "Hello, world!" }); - }, - ); + it("should be able to send and receive events when using options callback without a route", async () => { + const res = await httpServer.client.pubsub.publish(pubSubName, getTopic(topicOptionsWithCallback), { + message: "Hello, world!", + }); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + expect(mockSubscribeHandler.mock.calls[0][0]).toEqual({ message: "Hello, world!" }); + }); - runIt("should only allow one subscription per topic", async (server: DaprServer, protocol: string) => { + it("should only allow one subscription per topic", async () => { const anotherMockHandler = jest.fn(async (_data: object) => null); - const anotherTopic = getTopic(topicDefault + "-another", protocol); - const port = protocol === protocolHttp ? daprHttpPort : daprGrpcPort; - const commProtocol = protocol === protocolHttp ? CommunicationProtocolEnum.HTTP : CommunicationProtocolEnum.GRPC; + const anotherTopic = getTopic(topicDefault + "-another"); let exceptionThrown = false; try { let anotherServer = new DaprServer({ - serverHost, - serverPort: customPort, - communicationProtocol: commProtocol, + serverHost: "127.0.0.1", + serverPort: "50002", + communicationProtocol: CommunicationProtocolEnum.HTTP, clientOptions: { - daprHost, - daprPort: port, + daprHost: daprContainer.getHost(), + daprPort: daprContainer.getHttpPort().toString(), }, }); await anotherServer.pubsub.subscribe(pubSubName, anotherTopic, anotherMockHandler); @@ -482,17 +501,13 @@ describe("common/server", () => { expect(exceptionThrown).toBe(true); }); - runIt("should create subscriptions for default and custom routes", async (server: DaprServer, protocol: string) => { + it("should create subscriptions for default and custom routes", async () => { const topicRouteEntry = (topic: string, route: string) => [ - getTopic(topic, protocol), - `/${pubSubName}--${getTopic(topic, protocol)}--${route}`, + getTopic(topic), + `/${pubSubName}--${getTopic(topic)}--${route}`, ]; - const topicRoutes = [ - topicRouteEntry(topicDefault, "default"), - // topicRouteEntry(topicSimpleRoute, routeSimple), - // topicRouteEntry(topicLeadingSlashRoute, routeWithLeadingSlash.substring(1)), - ]; - const subs = JSON.stringify(server.pubsub.getSubscriptions()); + const topicRoutes = [topicRouteEntry(topicDefault, "default")]; + const subs = JSON.stringify(httpServer.pubsub.getSubscriptions()); topicRoutes.forEach((topicRoute) => { expect(subs).toContain( JSON.stringify({ @@ -504,60 +519,54 @@ describe("common/server", () => { }); }); - runIt("should create subscriptions with rules and default route", async (server: DaprServer, protocol: string) => { + it("should create subscriptions with rules and default route", async () => { const getSubscriptionEntry = (topic: string) => { const rules = sampleRoutes.rules.map((rule) => { const path = rule.path.startsWith("/") ? rule.path.substring(1) : rule.path; return { match: rule.match, - path: `/${pubSubName}--${getTopic(topic, protocol)}--${path}`, + path: `/${pubSubName}--${getTopic(topic)}--${path}`, }; }); return { pubsubname: pubSubName, - topic: getTopic(topic, protocol), + topic: getTopic(topic), routes: { - default: `/${pubSubName}--${getTopic(topic, protocol)}--default`, + default: `/${pubSubName}--${getTopic(topic)}--default`, rules: rules, }, }; }; const expectedSubs = [getSubscriptionEntry(topicCustomRules), getSubscriptionEntry(topicCustomRulesInOptions)]; - const subs = JSON.stringify(server.pubsub.getSubscriptions()); + const subs = JSON.stringify(httpServer.pubsub.getSubscriptions()); expectedSubs.forEach((expectedSub) => { expect(subs).toContain(JSON.stringify(expectedSub)); }); }); - runIt( - "should allow to register a listener without event handler callback", - async (server: DaprServer, protocol: string) => { - const subs = JSON.stringify(server.pubsub.getSubscriptions()); - expect(subs).toContain( - JSON.stringify({ - pubsubname: pubSubName, - topic: getTopic(topicWithoutCallback, protocol), - route: `/${pubSubName}--${getTopic(topicWithoutCallback, protocol)}--default`, - }), - ); - }, - ); + it("should allow to register a listener without event handler callback", async () => { + const subs = JSON.stringify(httpServer.pubsub.getSubscriptions()); + expect(subs).toContain( + JSON.stringify({ + pubsubname: pubSubName, + topic: getTopic(topicWithoutCallback), + route: `/${pubSubName}--${getTopic(topicWithoutCallback)}--default`, + }), + ); + }); - runIt( - "should allow to register an event handler after the server has started", - async (server: DaprServer, protocol: string) => { - const topic = getTopic(topicWithoutCallback, protocol); - const count1 = server.pubsub.getSubscriptions()[pubSubName][topic].routes["default"].eventHandlers.length; - server.pubsub.subscribeToRoute(pubSubName, topic, "", async () => null); - const count2 = server.pubsub.getSubscriptions()[pubSubName][topic].routes["default"].eventHandlers.length; - expect(count2).toEqual(count1 + 1); - }, - ); + it("should allow to register an event handler after the server has started", async () => { + const topic = getTopic(topicWithoutCallback); + const count1 = httpServer.pubsub.getSubscriptions()[pubSubName][topic].routes["default"].eventHandlers.length; + httpServer.pubsub.subscribeToRoute(pubSubName, topic, "", async () => null); + const count2 = httpServer.pubsub.getSubscriptions()[pubSubName][topic].routes["default"].eventHandlers.length; + expect(count2).toEqual(count1 + 1); + }); - runIt("should allow to configure deadletter topic", async (server: DaprServer, protocol: string) => { - const topic = getTopic(topicWithDeadletter, protocol); - const topicDeadLetter = getTopic(deadLetterTopic, protocol); - const subs = JSON.stringify(server.pubsub.getSubscriptions()); + it("should allow to configure deadletter topic", async () => { + const topic = getTopic(topicWithDeadletter); + const topicDeadLetter = getTopic(deadLetterTopic); + const subs = JSON.stringify(httpServer.pubsub.getSubscriptions()); expect(subs).toContain( JSON.stringify({ pubsubname: pubSubName, @@ -568,337 +577,708 @@ describe("common/server", () => { ); }); - runIt("should allow to listen on the deadletter topic", async (server: DaprServer, protocol: string) => { - const topic = getTopic(topicWithDeadletter, protocol); - const topicDeadLetter = getTopic(deadLetterTopic, protocol); - const count1 = server.pubsub.getSubscriptions()[pubSubName][topic].routes[topicDeadLetter].eventHandlers.length; - server.pubsub.subscribeToRoute(pubSubName, topic, topicDeadLetter, async () => null); - const count2 = server.pubsub.getSubscriptions()[pubSubName][topic].routes[topicDeadLetter].eventHandlers.length; + it("should allow to listen on the deadletter topic", async () => { + const topic = getTopic(topicWithDeadletter); + const topicDeadLetter = getTopic(deadLetterTopic); + const count1 = + httpServer.pubsub.getSubscriptions()[pubSubName][topic].routes[topicDeadLetter].eventHandlers.length; + httpServer.pubsub.subscribeToRoute(pubSubName, topic, topicDeadLetter, async () => null); + const count2 = + httpServer.pubsub.getSubscriptions()[pubSubName][topic].routes[topicDeadLetter].eventHandlers.length; expect(count2).toEqual(count1 + 1); }); - runIt( - "should allow to configure deadletter topic through subscribeWithOptions", - async (server: DaprServer, protocol: string) => { - const topic = getTopic(topicWithDeadletterInOptions, protocol); - const topicDeadLetter = getTopic(deadLetterTopic, protocol); - const subs = JSON.stringify(server.pubsub.getSubscriptions()); - expect(subs).toContain( - JSON.stringify({ - pubsubname: pubSubName, - topic: topic, - route: `/${pubSubName}--${topic}--default`, - deadLetterTopic: topicDeadLetter, - }), - ); - // Ensure that it has an event handler bound to it. - const eventHandlers = server.pubsub.getSubscriptions()[pubSubName][topic].routes[topicDeadLetter].eventHandlers; - expect(eventHandlers.length).toEqual(1); - }, - ); + it("should allow to configure deadletter topic through subscribeWithOptions", async () => { + const topic = getTopic(topicWithDeadletterInOptions); + const topicDeadLetter = getTopic(deadLetterTopic); + const subs = JSON.stringify(httpServer.pubsub.getSubscriptions()); + expect(subs).toContain( + JSON.stringify({ + pubsubname: pubSubName, + topic: topic, + route: `/${pubSubName}--${topic}--default`, + deadLetterTopic: topicDeadLetter, + }), + ); + // Ensure that it has an event handler bound to it. + const eventHandlers = + httpServer.pubsub.getSubscriptions()[pubSubName][topic].routes[topicDeadLetter].eventHandlers; + expect(eventHandlers.length).toEqual(1); + }); - runIt( - "should allow to configure deadletter topic through subscribeWithOptions with a default deadletter topic if none was provided", - async (server: DaprServer, protocol: string) => { - const topic = getTopic(topicWithDeadletterInOptionsDefault, protocol); - const routes = server.pubsub.getSubscriptions()[pubSubName][topic].routes; - expect(Object.keys(routes)).toContain(defaultDeadLetterTopic); - expect(routes[defaultDeadLetterTopic].eventHandlers.length).toEqual(1); - }, - ); + it("should allow to configure deadletter topic through subscribeWithOptions with a default deadletter topic if none was provided", async () => { + const topic = getTopic(topicWithDeadletterInOptionsDefault); + const routes = httpServer.pubsub.getSubscriptions()[pubSubName][topic].routes; + expect(Object.keys(routes)).toContain(defaultDeadLetterTopic); + expect(routes[defaultDeadLetterTopic].eventHandlers.length).toEqual(1); + }); - runIt( - "should be able to send and receive events through deadletter", - async (server: DaprServer, protocol: string) => { - const res = await server.client.pubsub.publish(pubSubName, getTopic(topicWithDeadletterAndErrorCb, protocol), { - message: "Hello, world!", - }); - expect(res.error).toBeUndefined(); - // Delay a bit for event to arrive - await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); - expect(mockSubscribeErrorHandler).toHaveBeenCalledTimes(1); - expect(mockSubscribeErrorHandler.mock.calls[0][0]).toEqual({ message: "Hello, world!" }); - expect(mockSubscribeHandler).toHaveBeenCalledTimes(0); - }, - ); + it("should be able to send and receive events through deadletter", async () => { + const res = await httpServer.client.pubsub.publish(pubSubName, getTopic(topicWithDeadletterAndErrorCb), { + message: "Hello, world!", + }); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeErrorHandler).toHaveBeenCalledTimes(1); + expect(mockSubscribeErrorHandler.mock.calls[0][0]).toEqual({ message: "Hello, world!" }); + expect(mockSubscribeHandler).toHaveBeenCalledTimes(0); + }); - runIt( - "should be able to subscribe to wildcard topics with a # (e.g., myhome/groundfloor/#) - multi level wildcard", - async (server: DaprServer, protocol: string) => { - const topic = topicWildcardHash.replace("#", "foo/bar"); - await server.client.pubsub.publish(pubSubName, getTopic(topic, protocol), { - message: "Hello, world!", - }); - // Delay a bit for event to arrive - await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); - expect(mockSubscribeHandler.mock.calls.length).toBe(1); - expect(mockSubscribeHandler.mock.calls[0][0]).toEqual({ message: "Hello, world!" }); - }, - ); + it("should be able to subscribe to wildcard topics with a # (e.g., myhome/groundfloor/#) - multi level wildcard", async () => { + const topic = topicWildcardHash.replace("#", "foo/bar"); + await httpServer.client.pubsub.publish(pubSubName, getTopic(topic), { + message: "Hello, world!", + }); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + expect(mockSubscribeHandler.mock.calls[0][0]).toEqual({ message: "Hello, world!" }); + }); - runIt( - "should be able to subscribe to wildcard topics with a + (e.g., myhome/firstfloor/+/temperature) - multi level wildcard", - async (server: DaprServer, protocol: string) => { - const topic = topicWildcardPlus.replace("+", "foo"); - await server.client.pubsub.publish(pubSubName, getTopic(topic, protocol), { - message: "Hello, world!", - }); - // Delay a bit for event to arrive - await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); - expect(mockSubscribeHandler.mock.calls.length).toBe(1); - expect(mockSubscribeHandler.mock.calls[0][0]).toEqual({ message: "Hello, world!" }); - }, - ); + it("should be able to subscribe to wildcard topics with a + (e.g., myhome/firstfloor/+/temperature) - multi level wildcard", async () => { + const topic = topicWildcardPlus.replace("+", "foo"); + await httpServer.client.pubsub.publish(pubSubName, getTopic(topic), { + message: "Hello, world!", + }); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + expect(mockSubscribeHandler.mock.calls[0][0]).toEqual({ message: "Hello, world!" }); + }); - runIt( - "should be able to send and receive plain events with bulk publish", - async (server: DaprServer, protocol: string) => { - const messages = ["message1", "message2", "message3"]; - await server.client.pubsub.publishBulk(pubSubName, getTopic(topicWithBulk, protocol), messages); - // Delay a bit for events to arrive - await new Promise((resolve) => setTimeout(resolve, 1000)); - expect(mockSubscribeHandler.mock.calls.length).toBe(3); - // Check that the messages are present - mockSubscribeHandler.mock.calls.forEach((call) => { - expect(messages).toContain(call[0]); - }); - }, - ); + it("should be able to send and receive plain events with bulk publish", async () => { + const messages = ["message1", "message2", "message3"]; + await httpServer.client.pubsub.publishBulk(pubSubName, getTopic(topicWithBulk), messages); + // Delay a bit for events to arrive + await new Promise((resolve) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(3); + // Check that the messages are present + mockSubscribeHandler.mock.calls.forEach((call) => { + expect(messages).toContain(call[0]); + }); + }); - runIt( - "should not allow registering a topic if it is not subscribed to", - async (server: DaprServer, _protocol: string) => { - const eh = jest.fn(async (_data: object) => null); + it("should not allow registering a topic if it is not subscribed to", async () => { + const eh = jest.fn(async (_data: object) => null); + let isThrown = false; + try { + await httpServer.pubsub.subscribeToRoute(pubSubName, "my-unexisting-topic-subscription", "", eh); + } catch (e: any) { + isThrown = true; + expect(e.message).toEqual( + `The topic 'my-unexisting-topic-subscription' is not subscribed to PubSub '${pubSubName}', cannot add event handler.`, + ); + } + expect(isThrown).toBe(true); + }); + }); +}); - let isThrown = false; +describe("common/server/grpc", () => { + jest.setTimeout(30000); - // Register a subscription - try { - await server.pubsub.subscribeToRoute(pubSubName, "my-unexisting-topic-subscription", "", eh); - } catch (e: any) { - isThrown = true; + let network: StartedNetwork; + let redisContainer: StartedTestContainer; + let mqttContainer: StartedTestContainer; + let daprContainer: StartedGrpcDaprContainer; + let grpcServer: DaprServer; - expect(e.message).toEqual( - `The topic 'my-unexisting-topic-subscription' is not subscribed to PubSub '${pubSubName}', cannot add event handler.`, - ); - } + const protocol = "grpc"; + const appPort = 3001; + const getTopic = (topic: string) => protocol + "-" + topic; - expect(isThrown).toBe(true); - }, - ); + const mockSubscribeHandler = jest.fn(async (_data: object, _headers: object) => null); + const mockBulkSubscribeRawPayloadHandler = jest.fn(async (_data: object, _headers: object) => null); + const mockBulkSubscribeCEHandler = jest.fn(async (_data: object, _headers: object) => null); + const mockBulkSubscribeCloudEventToRawPayloadHandler = jest.fn(async (_data: object, _headers: object) => null); + const mockBulkSubscribeRawPayloadToCloudEventHandler = jest.fn(async (_data: object, _headers: object) => null); + const mockSubscribeDeadletterHandler = jest.fn(async (_data: object, _headers: object) => null); + const mockSubscribeErrorHandler = jest.fn(async (_data: object, _headers: object) => { + throw new Error("This will DROP the message!"); }); - const setupPubSubSubscriptions = async () => { - await httpServer.pubsub.subscribe(pubSubName, getTopic(topicDefault, protocolHttp), mockSubscribeHandler); - await grpcServer.pubsub.subscribe(pubSubName, getTopic(topicDefault, protocolGrpc), mockSubscribeHandler); + const statusHandlerVars = { counter: 0 }; + const mockSubscribeStatusHandler = jest.fn(async (_data: string, _headers: object) => { + switch (_data) { + case "DROP": + return DaprPubSubStatusEnum.DROP; + case "TEST_RETRY_TWICE": + if (statusHandlerVars.counter < 2) { + statusHandlerVars.counter++; + return DaprPubSubStatusEnum.RETRY; + } + // Once we reach SUCCESS, reset the counter + statusHandlerVars.counter = 0; + return DaprPubSubStatusEnum.SUCCESS; + default: + statusHandlerVars.counter = 0; + return DaprPubSubStatusEnum.SUCCESS; + } + }); - await grpcServer.pubsub.subscribeBulk( - pubSubName, - getTopic(bulkSubscribeTopic, protocolGrpc), - mockBulkSubscribeRawPayloadHandler, - { - metadata: { rawPayload: true }, - }, - ); + beforeAll(async () => { + network = await new Network().start(); + [redisContainer, mqttContainer] = await Promise.all([startRedisContainer(network), startMqttContainer(network)]); - await httpServer.pubsub.subscribeBulk( - pubSubName, - getTopic(bulkSubscribeTopic, protocolHttp), - mockBulkSubscribeRawPayloadHandler, - { - metadata: { rawPayload: true }, + await TestContainers.exposeHostPorts(appPort); + + grpcServer = new DaprServer({ + serverHost: "127.0.0.1", + serverPort: "3001", + communicationProtocol: CommunicationProtocolEnum.GRPC, + clientOptions: { + // Placeholder — replaced with real container ports after daprContainer starts below. + daprHost: "127.0.0.1", + daprPort: "50000", }, - ); + }); + + // Register all subscriptions before starting the server + await grpcServer.pubsub.subscribe(pubSubName, getTopic(topicDefault), mockSubscribeHandler); await grpcServer.pubsub.subscribeBulk( pubSubName, - getTopic(bulkSubscribeClodEventTopic, protocolGrpc), - mockBulkSubscribeCEHandler, + getTopic(bulkSubscribeTopic), + mockBulkSubscribeRawPayloadHandler, + { metadata: { rawPayload: true } }, ); - await httpServer.pubsub.subscribeBulk( + await grpcServer.pubsub.subscribeBulk( pubSubName, - getTopic(bulkSubscribeClodEventTopic, protocolHttp), + getTopic(bulkSubscribeClodEventTopic), mockBulkSubscribeCEHandler, ); await grpcServer.pubsub.subscribeBulk( pubSubName, - getTopic(bulkSubscribeCloudEventToRawPayloadTopic, protocolGrpc), + getTopic(bulkSubscribeCloudEventToRawPayloadTopic), mockBulkSubscribeCloudEventToRawPayloadHandler, - { - metadata: { rawPayload: true }, - }, - ); - - await httpServer.pubsub.subscribeBulk( - pubSubName, - getTopic(bulkSubscribeCloudEventToRawPayloadTopic, protocolHttp), - mockBulkSubscribeCloudEventToRawPayloadHandler, - { - metadata: { rawPayload: true }, - }, + { metadata: { rawPayload: true } }, ); await grpcServer.pubsub.subscribeBulk( pubSubName, - getTopic(bulkSubscribeRawPayloadToClodEventTopic, protocolGrpc), + getTopic(bulkSubscribeRawPayloadToClodEventTopic), mockBulkSubscribeRawPayloadToCloudEventHandler, ); - await httpServer.pubsub.subscribeBulk( - pubSubName, - getTopic(bulkSubscribeRawPayloadToClodEventTopic, protocolHttp), - mockBulkSubscribeRawPayloadToCloudEventHandler, - ); + await grpcServer.pubsub.subscribe(pubSubName, getTopic(topicRawPayload), mockSubscribeHandler, undefined, { + rawPayload: true, + }); - await httpServer.pubsub.subscribe( - pubSubName, - getTopic(topicRawPayload, protocolHttp), - mockSubscribeHandler, - undefined, - { rawPayload: true }, - ); + await grpcServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicOptionsWithCallback), { + callback: mockSubscribeHandler, + }); + + await grpcServer.pubsub.subscribe(pubSubName, getTopic(topicSimpleRoute), mockSubscribeHandler, routeSimple); await grpcServer.pubsub.subscribe( pubSubName, - getTopic(topicRawPayload, protocolGrpc), + getTopic(topicLeadingSlashRoute), mockSubscribeHandler, - undefined, - { rawPayload: true }, + routeWithLeadingSlash, ); - await httpServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicOptionsWithCallback, protocolHttp), { - callback: mockSubscribeHandler, + await grpcServer.pubsub.subscribe(pubSubName, getTopic(topicCustomRules), mockSubscribeHandler, sampleRoutes); + + await grpcServer.pubsub.subscribe(pubSubName, getTopic(topicWildcardHash), mockSubscribeHandler); + await grpcServer.pubsub.subscribe(pubSubName, getTopic(topicWildcardPlus), mockSubscribeHandler); + await grpcServer.pubsub.subscribe(pubSubName, getTopic(topicWithBulk), mockSubscribeHandler); + + await grpcServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicCustomRulesInOptions), { + route: sampleRoutes, }); - await grpcServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicOptionsWithCallback, protocolGrpc), { - callback: mockSubscribeHandler, + await grpcServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithoutCallback), {}); + + await grpcServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithDeadletter), { + deadLetterTopic: getTopic(deadLetterTopic), }); - await httpServer.pubsub.subscribe( - pubSubName, - getTopic(topicSimpleRoute, protocolHttp), - mockSubscribeHandler, - routeSimple, - ); + await grpcServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithDeadletterInOptions), { + deadLetterTopic: getTopic(deadLetterTopic), + deadLetterCallback: mockSubscribeHandler, + }); - await grpcServer.pubsub.subscribe( - pubSubName, - getTopic(topicSimpleRoute, protocolGrpc), - mockSubscribeHandler, - routeSimple, - ); + await grpcServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithDeadletterInOptionsDefault), { + deadLetterCallback: mockSubscribeHandler, + }); - await httpServer.pubsub.subscribe( - pubSubName, - getTopic(topicLeadingSlashRoute, protocolHttp), - mockSubscribeHandler, - routeWithLeadingSlash, - ); + await grpcServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithDeadletterAndErrorCb), { + deadLetterCallback: mockSubscribeHandler, + callback: mockSubscribeErrorHandler, + }); - await grpcServer.pubsub.subscribe( - pubSubName, - getTopic(topicLeadingSlashRoute, protocolGrpc), - mockSubscribeHandler, - routeWithLeadingSlash, - ); + // Configure subscriptions for Status Message testing + // to test, we use the message as the status (SUCCESS, RETRY, DROP, Other) + await grpcServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithStatusCb), { + deadLetterCallback: mockSubscribeDeadletterHandler, + callback: (_data, _headers) => mockSubscribeStatusHandler(_data as string, _headers), + }); - await httpServer.pubsub.subscribe( - pubSubName, - getTopic(topicCustomRules, protocolHttp), - mockSubscribeHandler, - sampleRoutes, - ); + // Start ONLY the gRPC listener (no sidecar wait) so the app is already + // listening when the Dapr container probes it during its own init. + await grpcServer.daprServer.start("127.0.0.1", "3001"); + + daprContainer = await new DaprGrpcAppContainer() + .withNetwork(network) + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(buildPubSubMqttComponent()) + .start(); + + // Patch the DaprClient with real container ports, then wait for sidecar. + (grpcServer as any).client = new DaprClient({ + daprHost: daprContainer.getHost(), + daprPort: daprContainer.getGrpcPort().toString(), + communicationProtocol: CommunicationProtocolEnum.GRPC, + }); + await grpcServer.client.start(); + }, 300 * 1000); - await grpcServer.pubsub.subscribe( - pubSubName, - getTopic(topicCustomRules, protocolGrpc), - mockSubscribeHandler, - sampleRoutes, - ); + beforeEach(() => { + jest.clearAllMocks(); + }); - await httpServer.pubsub.subscribe(pubSubName, getTopic(topicWildcardHash, protocolHttp), mockSubscribeHandler); - await grpcServer.pubsub.subscribe(pubSubName, getTopic(topicWildcardHash, protocolGrpc), mockSubscribeHandler); - await httpServer.pubsub.subscribe(pubSubName, getTopic(topicWildcardPlus, protocolHttp), mockSubscribeHandler); - await grpcServer.pubsub.subscribe(pubSubName, getTopic(topicWildcardPlus, protocolGrpc), mockSubscribeHandler); + afterAll(async () => { + await runWithCleanupErrorSuppression(async () => { + await grpcServer.stop(); + await daprContainer.stop(); + await mqttContainer.stop(); + await redisContainer.stop(); + await network.stop(); + }); + }, 60 * 1000); - await httpServer.pubsub.subscribe(pubSubName, getTopic(topicWithBulk, protocolHttp), mockSubscribeHandler); - await grpcServer.pubsub.subscribe(pubSubName, getTopic(topicWithBulk, protocolGrpc), mockSubscribeHandler); + describe("pubsub", () => { + it("should mark messages as processed successfully (SUCCESS) by-default, and the same message should not be received anymore", async () => { + const res = await grpcServer.client.pubsub.publish(pubSubName, getTopic(topicWithStatusCb), "SUCCESS"); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 2000)); + expect(mockSubscribeStatusHandler.mock.calls.length).toBe(1); + expect(mockSubscribeStatusHandler.mock.calls[0][0]).toEqual("SUCCESS"); + expect(mockSubscribeDeadletterHandler.mock.calls.length).toBe(0); + }); - await httpServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicCustomRulesInOptions, protocolHttp), { - route: sampleRoutes, + it("should mark messages as retried (RETRY), and the same message should be received again until we send SUCCESS", async () => { + const res = await grpcServer.client.pubsub.publish(pubSubName, getTopic(topicWithStatusCb), "TEST_RETRY_TWICE"); + expect(res.error).toBeUndefined(); + // Poll until all 3 deliveries arrive (EMQX QoS1 retry interval is 3 s; poll up to 45 s for CI headroom) + const deadline = Date.now() + 45_000; + while (mockSubscribeStatusHandler.mock.calls.length < 3 && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 500)); + } + // 3 as we retry twice + expect(mockSubscribeStatusHandler.mock.calls.length).toBe(3); + expect(mockSubscribeDeadletterHandler.mock.calls.length).toBe(0); + }, 60_000); + + it("should mark messages as dropped (DROP), and the message should be deadlettered", async () => { + const res = await grpcServer.client.pubsub.publish(pubSubName, getTopic(topicWithStatusCb), "DROP"); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeStatusHandler.mock.calls.length).toBe(1); + expect(mockSubscribeStatusHandler.mock.calls[0][0]).toEqual("DROP"); }); - await grpcServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicCustomRulesInOptions, protocolGrpc), { - route: sampleRoutes, + it("should be able to send and receive plain events", async () => { + const res = await grpcServer.client.pubsub.publish(pubSubName, getTopic(topicDefault), "Hello, world!"); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + expect(mockSubscribeHandler.mock.calls[0][0]).toEqual("Hello, world!"); + const headers = mockSubscribeHandler.mock.calls[0][1] as any; + expect(headers["pubsubname"]).toEqual(pubSubName); }); - await httpServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithoutCallback, protocolHttp), {}); - await grpcServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithoutCallback, protocolGrpc), {}); + it("should be able to publish multiple rawPayload messages and receive events using bulk subscribe", async () => { + const res1 = await grpcServer.client.pubsub.publish( + pubSubName, + getTopic(bulkSubscribeTopic), + { message: "Message 1!" }, + { metadata: { rawPayload: "true" } }, + ); + const res2 = await grpcServer.client.pubsub.publish( + pubSubName, + getTopic(bulkSubscribeTopic), + { message: "Message 2!" }, + { metadata: { rawPayload: "true" } }, + ); + expect(res1.error).toBeUndefined(); + expect(res2.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 4000)); + expect(mockBulkSubscribeRawPayloadHandler.mock.calls.length).toBe(2); + expect(mockBulkSubscribeRawPayloadHandler.mock.calls[0][0]).toEqual({ message: "Message 1!" }); + expect(mockBulkSubscribeRawPayloadHandler.mock.calls[1][0]).toEqual({ message: "Message 2!" }); + }); - await httpServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithDeadletter, protocolHttp), { - deadLetterTopic: getTopic(deadLetterTopic, protocolHttp), + it("should be able to publish multiple CloudEvents and receive events using bulk subscribe", async () => { + const res1 = await grpcServer.client.pubsub.publish(pubSubName, getTopic(bulkSubscribeClodEventTopic), { + message: "Message 1!", + }); + const res2 = await grpcServer.client.pubsub.publish(pubSubName, getTopic(bulkSubscribeClodEventTopic), { + message: "Message 2!", + }); + expect(res1.error).toBeUndefined(); + expect(res2.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 4000)); + expect(mockBulkSubscribeCEHandler.mock.calls.length).toBe(2); + expect(mockBulkSubscribeCEHandler.mock.calls[0][0]).toEqual({ message: "Message 1!" }); + expect(mockBulkSubscribeCEHandler.mock.calls[1][0]).toEqual({ message: "Message 2!" }); }); - await grpcServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithDeadletter, protocolGrpc), { - deadLetterTopic: getTopic(deadLetterTopic, protocolGrpc), + it("should be able to publish multiple CloudEvents but receive rawPayload events using bulk subscribe", async () => { + const res1 = await grpcServer.client.pubsub.publish( + pubSubName, + getTopic(bulkSubscribeCloudEventToRawPayloadTopic), + { message: "Message 1!" }, + ); + const res2 = await grpcServer.client.pubsub.publish( + pubSubName, + getTopic(bulkSubscribeCloudEventToRawPayloadTopic), + { message: "Message 2!" }, + ); + expect(res1.error).toBeUndefined(); + expect(res2.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 4000)); + expect(mockBulkSubscribeCloudEventToRawPayloadHandler.mock.calls.length).toBe(2); + expect(getDataFromCEObject(mockBulkSubscribeCloudEventToRawPayloadHandler.mock.calls[0][0])).toEqual({ + message: "Message 1!", + }); + expect(getDataFromCEObject(mockBulkSubscribeCloudEventToRawPayloadHandler.mock.calls[1][0])).toEqual({ + message: "Message 2!", + }); }); - await httpServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithDeadletterInOptions, protocolHttp), { - deadLetterTopic: getTopic(deadLetterTopic, protocolHttp), - deadLetterCallback: mockSubscribeHandler, + it("should be able to publish multiple rawPayload messages and receive CloudEvents using bulk subscribe", async () => { + const res1 = await grpcServer.client.pubsub.publish( + pubSubName, + getTopic(bulkSubscribeRawPayloadToClodEventTopic), + { message: "Message 1!" }, + { metadata: { rawPayload: "true" } }, + ); + const res2 = await grpcServer.client.pubsub.publish( + pubSubName, + getTopic(bulkSubscribeRawPayloadToClodEventTopic), + { message: "Message 2!" }, + { metadata: { rawPayload: "true" } }, + ); + expect(res1.error).toBeUndefined(); + expect(res2.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 4000)); + expect(mockBulkSubscribeRawPayloadToCloudEventHandler.mock.calls.length).toBe(2); }); - await grpcServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithDeadletterInOptions, protocolGrpc), { - deadLetterTopic: getTopic(deadLetterTopic, protocolGrpc), - deadLetterCallback: mockSubscribeHandler, + it("should be able to send and receive JSON events", async () => { + const res = await grpcServer.client.pubsub.publish(pubSubName, getTopic(topicDefault), { + message: "Hello, world!", + }); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + expect(mockSubscribeHandler.mock.calls[0][0]).toEqual({ message: "Hello, world!" }); }); - await httpServer.pubsub.subscribeWithOptions( - pubSubName, - getTopic(topicWithDeadletterInOptionsDefault, protocolHttp), - { - deadLetterCallback: mockSubscribeHandler, - }, - ); + it("should be able to send and receive cloudevents", async () => { + const ce = { + specversion: "1.0", + type: "com.github.pull.create", + source: "https://github.com/cloudevents/spec/pull", + id: "A234-1234-1234", + data: "Hello, world!", + datacontenttype: "text/plain", + }; + const res = await grpcServer.client.pubsub.publish(pubSubName, getTopic(topicDefault), ce); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 500)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + expect(mockSubscribeHandler.mock.calls[0][0]).toEqual("Hello, world!"); + }); - await grpcServer.pubsub.subscribeWithOptions( - pubSubName, - getTopic(topicWithDeadletterInOptionsDefault, protocolGrpc), - { - deadLetterCallback: mockSubscribeHandler, - }, - ); + it("should be able to send cloudevents as JSON and receive it", async () => { + const ce = { + specversion: "1.0", + type: "com.github.pull.create", + source: "https://github.com/cloudevents/spec/pull", + id: "A234-1234-1234", + data: "Hello, world!", + datacontenttype: "text/plain", + }; + const options = { contentType: "application/json" }; + const res = await grpcServer.client.pubsub.publish(pubSubName, getTopic(topicDefault), ce, options); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 500)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + // The cloudevent should contain an inner cloudevent since the content type was application/json + const innerCe: any = mockSubscribeHandler.mock.calls[0][0]; + expect(innerCe["data"]).toEqual("Hello, world!"); + }); - await httpServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithDeadletterAndErrorCb, protocolHttp), { - deadLetterCallback: mockSubscribeHandler, - callback: mockSubscribeErrorHandler, + it("should be able to send plain events and receive as raw payload", async () => { + const res = await grpcServer.client.pubsub.publish(pubSubName, getTopic(topicRawPayload), "Hello, world!"); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + const rawData: any = mockSubscribeHandler.mock.calls[0][0]; + expect(rawData["data"]).toEqual("Hello, world!"); }); - await grpcServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithDeadletterAndErrorCb, protocolGrpc), { - deadLetterCallback: mockSubscribeHandler, - callback: mockSubscribeErrorHandler, + it("should be able to send JSON events and receive as raw payload", async () => { + const res = await grpcServer.client.pubsub.publish(pubSubName, getTopic(topicRawPayload), { + message: "Hello, world!", + }); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + const rawData: any = mockSubscribeHandler.mock.calls[0][0]; + expect(rawData["data"]).toEqual({ message: "Hello, world!" }); }); - // Configure subscriptions for Status Message testing - // to test, we use the message as the status(SUCCESS, RETRY, DROP, Other) - // SUCCESS: Message is processed correctly - // RETRY: Message is retried and will thus call the callback again - // DROP: Message is dropped and will thus call the deadletter callback - await httpServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithStatusCb, protocolHttp), { - deadLetterCallback: mockSubscribeDeadletterHandler, - callback: (_data, _headers) => mockSubscribeStatusHandler(protocolHttp, _data, _headers), + it("should be able to send and receive plain events as raw payload", async () => { + const res = await grpcServer.client.pubsub.publish(pubSubName, getTopic(topicRawPayload), "Hello, world!", { + metadata: { rawPayload: "true" }, + }); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + expect(mockSubscribeHandler.mock.calls[0][0]).toEqual("Hello, world!"); }); - await grpcServer.pubsub.subscribeWithOptions(pubSubName, getTopic(topicWithStatusCb, protocolGrpc), { - deadLetterCallback: mockSubscribeDeadletterHandler, - callback: (_data, _headers) => mockSubscribeStatusHandler(protocolGrpc, _data, _headers), + it("should be able to send and receive JSON events as raw payload", async () => { + const res = await grpcServer.client.pubsub.publish( + pubSubName, + getTopic(topicRawPayload), + { message: "Hello, world!" }, + { metadata: { rawPayload: "true" } }, + ); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + expect(mockSubscribeHandler.mock.calls[0][0]).toEqual({ message: "Hello, world!" }); }); - }; -}); -function getDataFromCEObject(obj: object) { - const values = Object.values(obj); - return values[0]; -} + it("should be able to send and receive events when using options callback without a route", async () => { + const res = await grpcServer.client.pubsub.publish(pubSubName, getTopic(topicOptionsWithCallback), { + message: "Hello, world!", + }); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + expect(mockSubscribeHandler.mock.calls[0][0]).toEqual({ message: "Hello, world!" }); + }); + + it("should only allow one subscription per topic", async () => { + const anotherMockHandler = jest.fn(async (_data: object) => null); + const anotherTopic = getTopic(topicDefault + "-another"); + let exceptionThrown = false; + try { + let anotherServer = new DaprServer({ + serverHost: "127.0.0.1", + serverPort: "50003", + communicationProtocol: CommunicationProtocolEnum.GRPC, + clientOptions: { + daprHost: daprContainer.getHost(), + daprPort: daprContainer.getGrpcPort().toString(), + }, + }); + await anotherServer.pubsub.subscribe(pubSubName, anotherTopic, anotherMockHandler); + await anotherServer.pubsub.subscribe(pubSubName, anotherTopic, anotherMockHandler, "/another-route"); + anotherServer = undefined as any; // clean it up + } catch (e: any) { + exceptionThrown = true; + expect(e.message).toEqual( + `The topic '${anotherTopic}' is already subscribed to PubSub '${pubSubName}', there can be only one topic registered.`, + ); + } + expect(exceptionThrown).toBe(true); + }); + + it("should create subscriptions for default and custom routes", async () => { + const topicRouteEntry = (topic: string, route: string) => [ + getTopic(topic), + `/${pubSubName}--${getTopic(topic)}--${route}`, + ]; + const topicRoutes = [topicRouteEntry(topicDefault, "default")]; + const subs = JSON.stringify(grpcServer.pubsub.getSubscriptions()); + topicRoutes.forEach((topicRoute) => { + expect(subs).toContain( + JSON.stringify({ + pubsubname: pubSubName, + topic: topicRoute[0], + route: topicRoute[1], + }), + ); + }); + }); + + it("should create subscriptions with rules and default route", async () => { + const getSubscriptionEntry = (topic: string) => { + const rules = sampleRoutes.rules.map((rule) => { + const path = rule.path.startsWith("/") ? rule.path.substring(1) : rule.path; + return { + match: rule.match, + path: `/${pubSubName}--${getTopic(topic)}--${path}`, + }; + }); + return { + pubsubname: pubSubName, + topic: getTopic(topic), + routes: { + default: `/${pubSubName}--${getTopic(topic)}--default`, + rules: rules, + }, + }; + }; + const expectedSubs = [getSubscriptionEntry(topicCustomRules), getSubscriptionEntry(topicCustomRulesInOptions)]; + const subs = JSON.stringify(grpcServer.pubsub.getSubscriptions()); + expectedSubs.forEach((expectedSub) => { + expect(subs).toContain(JSON.stringify(expectedSub)); + }); + }); + + it("should allow to register a listener without event handler callback", async () => { + const subs = JSON.stringify(grpcServer.pubsub.getSubscriptions()); + expect(subs).toContain( + JSON.stringify({ + pubsubname: pubSubName, + topic: getTopic(topicWithoutCallback), + route: `/${pubSubName}--${getTopic(topicWithoutCallback)}--default`, + }), + ); + }); + + it("should allow to register an event handler after the server has started", async () => { + const topic = getTopic(topicWithoutCallback); + const count1 = grpcServer.pubsub.getSubscriptions()[pubSubName][topic].routes["default"].eventHandlers.length; + grpcServer.pubsub.subscribeToRoute(pubSubName, topic, "", async () => null); + const count2 = grpcServer.pubsub.getSubscriptions()[pubSubName][topic].routes["default"].eventHandlers.length; + expect(count2).toEqual(count1 + 1); + }); + + it("should allow to configure deadletter topic", async () => { + const topic = getTopic(topicWithDeadletter); + const topicDeadLetter = getTopic(deadLetterTopic); + const subs = JSON.stringify(grpcServer.pubsub.getSubscriptions()); + expect(subs).toContain( + JSON.stringify({ + pubsubname: pubSubName, + topic: topic, + route: `/${pubSubName}--${topic}--default`, + deadLetterTopic: topicDeadLetter, + }), + ); + }); + + it("should allow to listen on the deadletter topic", async () => { + const topic = getTopic(topicWithDeadletter); + const topicDeadLetter = getTopic(deadLetterTopic); + const count1 = + grpcServer.pubsub.getSubscriptions()[pubSubName][topic].routes[topicDeadLetter].eventHandlers.length; + grpcServer.pubsub.subscribeToRoute(pubSubName, topic, topicDeadLetter, async () => null); + const count2 = + grpcServer.pubsub.getSubscriptions()[pubSubName][topic].routes[topicDeadLetter].eventHandlers.length; + expect(count2).toEqual(count1 + 1); + }); + + it("should allow to configure deadletter topic through subscribeWithOptions", async () => { + const topic = getTopic(topicWithDeadletterInOptions); + const topicDeadLetter = getTopic(deadLetterTopic); + const subs = JSON.stringify(grpcServer.pubsub.getSubscriptions()); + expect(subs).toContain( + JSON.stringify({ + pubsubname: pubSubName, + topic: topic, + route: `/${pubSubName}--${topic}--default`, + deadLetterTopic: topicDeadLetter, + }), + ); + // Ensure that it has an event handler bound to it. + const eventHandlers = + grpcServer.pubsub.getSubscriptions()[pubSubName][topic].routes[topicDeadLetter].eventHandlers; + expect(eventHandlers.length).toEqual(1); + }); + + it("should allow to configure deadletter topic through subscribeWithOptions with a default deadletter topic if none was provided", async () => { + const topic = getTopic(topicWithDeadletterInOptionsDefault); + const routes = grpcServer.pubsub.getSubscriptions()[pubSubName][topic].routes; + expect(Object.keys(routes)).toContain(defaultDeadLetterTopic); + expect(routes[defaultDeadLetterTopic].eventHandlers.length).toEqual(1); + }); + + it("should be able to send and receive events through deadletter", async () => { + const res = await grpcServer.client.pubsub.publish(pubSubName, getTopic(topicWithDeadletterAndErrorCb), { + message: "Hello, world!", + }); + expect(res.error).toBeUndefined(); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeErrorHandler).toHaveBeenCalledTimes(1); + expect(mockSubscribeErrorHandler.mock.calls[0][0]).toEqual({ message: "Hello, world!" }); + expect(mockSubscribeHandler).toHaveBeenCalledTimes(0); + }); + + it("should be able to subscribe to wildcard topics with a # (e.g., myhome/groundfloor/#) - multi level wildcard", async () => { + const topic = topicWildcardHash.replace("#", "foo/bar"); + await grpcServer.client.pubsub.publish(pubSubName, getTopic(topic), { + message: "Hello, world!", + }); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + expect(mockSubscribeHandler.mock.calls[0][0]).toEqual({ message: "Hello, world!" }); + }); + + it("should be able to subscribe to wildcard topics with a + (e.g., myhome/firstfloor/+/temperature) - multi level wildcard", async () => { + const topic = topicWildcardPlus.replace("+", "foo"); + await grpcServer.client.pubsub.publish(pubSubName, getTopic(topic), { + message: "Hello, world!", + }); + // Delay a bit for event to arrive + await new Promise((resolve, _reject) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(1); + expect(mockSubscribeHandler.mock.calls[0][0]).toEqual({ message: "Hello, world!" }); + }); + + it("should be able to send and receive plain events with bulk publish", async () => { + const messages = ["message1", "message2", "message3"]; + await grpcServer.client.pubsub.publishBulk(pubSubName, getTopic(topicWithBulk), messages); + // Delay a bit for events to arrive + await new Promise((resolve) => setTimeout(resolve, 1000)); + expect(mockSubscribeHandler.mock.calls.length).toBe(3); + // Check that the messages are present + mockSubscribeHandler.mock.calls.forEach((call) => { + expect(messages).toContain(call[0]); + }); + }); + + it("should not allow registering a topic if it is not subscribed to", async () => { + const eh = jest.fn(async (_data: object) => null); + let isThrown = false; + try { + await grpcServer.pubsub.subscribeToRoute(pubSubName, "my-unexisting-topic-subscription", "", eh); + } catch (e: any) { + isThrown = true; + expect(e.message).toEqual( + `The topic 'my-unexisting-topic-subscription' is not subscribed to PubSub '${pubSubName}', cannot add event handler.`, + ); + } + expect(isThrown).toBe(true); + }); + }); +}); diff --git a/test/e2e/grpc/client.test.ts b/test/e2e/grpc/client.test.ts index a6aac5cf..311838b2 100644 --- a/test/e2e/grpc/client.test.ts +++ b/test/e2e/grpc/client.test.ts @@ -16,34 +16,68 @@ import { createReadStream } from "node:fs"; import { readFile } from "node:fs/promises"; import { Readable } from "node:stream"; import * as grpc from "@grpc/grpc-js"; +import { Network, StartedNetwork, StartedTestContainer } from "testcontainers"; +import { DaprContainer, StartedDaprContainer } from "@dapr/testcontainer-node"; import { CommunicationProtocolEnum, DaprClient, LogLevel } from "../../../src"; import { SubscribeConfigurationResponse } from "../../../src/types/configuration/SubscribeConfigurationResponse"; -import * as DockerUtils from "../../utils/DockerUtil"; import { DaprClient as DaprClientGrpc } from "../../../src/proto/dapr/proto/runtime/v1/dapr_grpc_pb"; import { NextCall } from "@grpc/grpc-js/build/src/client-interceptors"; import { GetMetadataRequest } from "../../../src/proto/dapr/proto/runtime/v1/metadata_pb"; - -const daprHost = "localhost"; -const daprPort = "50000"; // Dapr Sidecar Port of this Example Server +import { + startRedisContainer, + buildStateRedisComponent, + buildPubSubRedisComponent, + buildConfigRedisComponent, + buildLockRedisComponent, + buildSecretEnvvarsComponent, + buildCryptoLocalComponent, + DAPR_TEST_RUNTIME_IMAGE, + DAPR_TEST_PLACEMENT_IMAGE, + DAPR_TEST_SCHEDULER_IMAGE, + runWithCleanupErrorSuppression, +} from "../helpers/containers"; describe("grpc/client", () => { let client: DaprClient; + let network: StartedNetwork; + let redisContainer: StartedTestContainer; + let daprContainer: StartedDaprContainer; - // We need to start listening on some endpoints already - // this because Dapr is not dynamic and registers endpoints on boot beforeAll(async () => { + network = await new Network().start(); + redisContainer = await startRedisContainer(network); + + daprContainer = await new DaprContainer(DAPR_TEST_RUNTIME_IMAGE) + .withPlacementImage(DAPR_TEST_PLACEMENT_IMAGE) + .withSchedulerImage(DAPR_TEST_SCHEDULER_IMAGE) + .withNetwork(network) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(buildStateRedisComponent()) + .withComponent(buildPubSubRedisComponent()) + .withComponent(buildConfigRedisComponent()) + .withComponent(buildLockRedisComponent()) + .withComponent(buildSecretEnvvarsComponent()) + .withComponent(buildCryptoLocalComponent()) + .withEnvironment({ TEST_SECRET_1: "secret_val_1", TEST_SECRET_2: "secret_val_2" }) + .start(); + client = new DaprClient({ - daprHost, - daprPort, + daprHost: daprContainer.getHost(), + daprPort: daprContainer.getGrpcPort().toString(), communicationProtocol: CommunicationProtocolEnum.GRPC, logger: { level: LogLevel.Debug, }, }); - }, 10 * 1000); + }, 180 * 1000); afterAll(async () => { - await client.stop(); + await runWithCleanupErrorSuppression(async () => { + await client.stop(); + await daprContainer.stop(); + await redisContainer.stop(); + await network.stop(); + }); }); describe("client", () => { @@ -58,6 +92,9 @@ describe("grpc/client", () => { describe("proxy", () => { it("should allow to use a proxy builder to proxy a gRPC request", async () => { + const oldProcessAppId = process.env?.APP_ID; + process.env.APP_ID = "test-suite"; + let mockMetadataRes: grpc.Metadata = new grpc.Metadata(); const mockInterceptor = jest.fn((options: grpc.InterceptorOptions, nextCall: NextCall): grpc.InterceptingCall => { return new grpc.InterceptingCall(nextCall(options), { @@ -80,6 +117,8 @@ describe("grpc/client", () => { expect(mockInterceptor.mock.calls.length).toBe(1); expect(mockMetadataRes.get("dapr-app-id")[0]).toBe("test-suite"); + + process.env.APP_ID = oldProcessAppId; }); it("should allow to use a proxy builder that uses daprAppId by setting custom env variable to proxy a gRPC request", async () => { @@ -344,10 +383,10 @@ describe("grpc/client", () => { describe("configuration", () => { beforeEach(async () => { - // Reset the Configuration API - await DockerUtils.executeDockerCommand("dapr_redis redis-cli MSET myconfigkey1 key1_initialvalue||1"); - await DockerUtils.executeDockerCommand("dapr_redis redis-cli MSET myconfigkey2 key2_initialvalue||1"); - await DockerUtils.executeDockerCommand("dapr_redis redis-cli MSET myconfigkey3 key3_initialvalue||1"); + // Reset the Configuration API by writing directly to the Redis container + await redisContainer.exec(["redis-cli", "MSET", "myconfigkey1", "key1_initialvalue||1"]); + await redisContainer.exec(["redis-cli", "MSET", "myconfigkey2", "key2_initialvalue||1"]); + await redisContainer.exec(["redis-cli", "MSET", "myconfigkey3", "key3_initialvalue||1"]); }); it("should be able to get the configuration items", async () => { @@ -378,7 +417,7 @@ describe("grpc/client", () => { const stream = await client.configuration.subscribe("config-redis", m); // Update the configuration item - await DockerUtils.executeDockerCommand("dapr_redis redis-cli MSET myconfigkey3 mynewvalue||2"); + await redisContainer.exec(["redis-cli", "MSET", "myconfigkey3", "mynewvalue||2"]); expect(Object.keys(m.mock.calls[0][0].items).length).toEqual(1); expect("myconfigkey3" in m.mock.calls[0][0].items); @@ -393,7 +432,7 @@ describe("grpc/client", () => { }); const stream = await client.configuration.subscribeWithKeys("config-redis", ["myconfigkey1", "myconfigkey2"], m); - await DockerUtils.executeDockerCommand("dapr_redis redis-cli MSET myconfigkey1 key1_mynewvalue||1"); + await redisContainer.exec(["redis-cli", "MSET", "myconfigkey1", "key1_mynewvalue||1"]); expect(Object.keys(m.mock.calls[0][0].items).length).toEqual(1); expect("myconfigkey1" in m.mock.calls[0][0].items); @@ -413,7 +452,7 @@ describe("grpc/client", () => { { hello: "world" }, m, ); - await DockerUtils.executeDockerCommand("dapr_redis redis-cli MSET myconfigkey1 key1_mynewvalue||1"); + await redisContainer.exec(["redis-cli", "MSET", "myconfigkey1", "key1_mynewvalue||1"]); expect(Object.keys(m.mock.calls[0][0].items).length).toEqual(1); expect("myconfigkey1" in m.mock.calls[0][0].items); @@ -433,7 +472,7 @@ describe("grpc/client", () => { { hello: "world" }, m, ); - await DockerUtils.executeDockerCommand("dapr_redis redis-cli MSET myconfigkey1 key1_mynewvalue||1"); + await redisContainer.exec(["redis-cli", "MSET", "myconfigkey1", "key1_mynewvalue||1"]); expect(Object.keys(m.mock.calls[0][0].items).length).toEqual(1); expect("myconfigkey1" in m.mock.calls[0][0].items); @@ -441,7 +480,7 @@ describe("grpc/client", () => { stream.stop(); - await DockerUtils.executeDockerCommand("dapr_redis redis-cli MSET myconfigkey1 key1_mynewvalue2||1"); + await redisContainer.exec(["redis-cli", "MSET", "myconfigkey1", "key1_mynewvalue2||1"]); // Expect no change after stop expect(Object.keys(m.mock.calls[0][0].items).length).toEqual(1); @@ -460,7 +499,7 @@ describe("grpc/client", () => { const stream1 = await client.configuration.subscribeWithKeys("config-redis", ["myconfigkey1"], m1); const stream2 = await client.configuration.subscribeWithKeys("config-redis", ["myconfigkey1"], m2); - await DockerUtils.executeDockerCommand("dapr_redis redis-cli MSET myconfigkey1 key1_mynewvalue||1"); + await redisContainer.exec(["redis-cli", "MSET", "myconfigkey1", "key1_mynewvalue||1"]); expect(Object.keys(m1.mock.calls[0][0].items).length).toEqual(1); expect("myconfigkey1" in m1.mock.calls[0][0].items); diff --git a/test/e2e/grpc/clientWithApiToken.test.ts b/test/e2e/grpc/clientWithApiToken.test.ts index b0e8c5f4..3af4cef2 100644 --- a/test/e2e/grpc/clientWithApiToken.test.ts +++ b/test/e2e/grpc/clientWithApiToken.test.ts @@ -12,19 +12,43 @@ limitations under the License. */ import * as grpc from "@grpc/grpc-js"; +import { Network, StartedNetwork } from "testcontainers"; +import { DaprContainer, StartedDaprContainer } from "@dapr/testcontainer-node"; import { CommunicationProtocolEnum, DaprClient, LogLevel } from "../../../src"; import { DaprClient as DaprClientGrpc } from "../../../src/proto/dapr/proto/runtime/v1/dapr_grpc_pb"; import { NextCall } from "@grpc/grpc-js/build/src/client-interceptors"; import { GetMetadataRequest } from "../../../src/proto/dapr/proto/runtime/v1/metadata_pb"; - -const daprHost = "localhost"; -const daprPort = "50000"; // Dapr Sidecar Port of this Example Server +import { buildInMemoryPubSubComponent, DAPR_TEST_RUNTIME_IMAGE, DAPR_TEST_PLACEMENT_IMAGE, DAPR_TEST_SCHEDULER_IMAGE, runWithCleanupErrorSuppression } from "../helpers/containers"; describe("grpc/client with api token", () => { + let network: StartedNetwork; + let daprContainer: StartedDaprContainer; + + beforeAll(async () => { + network = await new Network().start(); + + // Configure the Dapr sidecar to require an API token. + daprContainer = await new DaprContainer(DAPR_TEST_RUNTIME_IMAGE) + .withPlacementImage(DAPR_TEST_PLACEMENT_IMAGE) + .withSchedulerImage(DAPR_TEST_SCHEDULER_IMAGE) + .withNetwork(network) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(buildInMemoryPubSubComponent()) + .withEnvironment({ DAPR_API_TOKEN: "test" }) + .start(); + }, 180 * 1000); + + afterAll(async () => { + await runWithCleanupErrorSuppression(async () => { + await daprContainer.stop(); + await network.stop(); + }); + }); + it("should send api token as metadata when present", async () => { const clientWithToken = new DaprClient({ - daprHost, - daprPort, + daprHost: daprContainer.getHost(), + daprPort: daprContainer.getGrpcPort().toString(), communicationProtocol: CommunicationProtocolEnum.GRPC, daprApiToken: "test", logger: { diff --git a/test/e2e/grpc/server.test.ts b/test/e2e/grpc/server.test.ts index ea0dd81f..8e22c82e 100644 --- a/test/e2e/grpc/server.test.ts +++ b/test/e2e/grpc/server.test.ts @@ -11,29 +11,54 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { CommunicationProtocolEnum, DaprServer, HttpMethod, LogLevel } from "../../../src"; +import { Network, StartedNetwork, StartedTestContainer, TestContainers } from "testcontainers"; +import { CommunicationProtocolEnum, DaprClient, DaprServer, HttpMethod, LogLevel } from "../../../src"; +import { DaprInvokerCallbackContent } from "../../../src/types/DaprInvokerCallback.type"; +import { DaprGrpcAppContainer, StartedGrpcDaprContainer } from "../helpers/DaprGrpcAppContainer"; +import { + startRedisContainer, + startMqttContainer, + buildBindingMqttComponent, + buildBindingRedisComponent, + buildConfigRedisComponent, + runWithCleanupErrorSuppression, +} from "../helpers/containers"; const serverHost = "127.0.0.1"; -const serverPort = "50001"; -const daprHost = "127.0.0.1"; -const daprPort = "50000"; // Dapr Sidecar Port of this Example Server -const daprAppId = "test-suite"; +const serverPort = "3001"; +const daprAppId = "test-suite-grpc-server"; describe("grpc/server", () => { let server: DaprServer; + let network: StartedNetwork; + let redisContainer: StartedTestContainer; + let mqttContainer: StartedTestContainer; + let daprContainer: StartedGrpcDaprContainer; + const mockInvoker = jest.fn(async (_data: object) => _data); const mockBindingReceive = jest.fn(async (_data: object) => null); - // We need to start listening on some endpoints already - // this because Dapr is not dynamic and registers endpoints on boot beforeAll(async () => { + network = await new Network().start(); + [redisContainer, mqttContainer] = await Promise.all([ + startRedisContainer(network), + startMqttContainer(network), + ]); + + // The Dapr container calls back to the gRPC app server on the host. + await TestContainers.exposeHostPorts(parseInt(serverPort)); + + // Create the server with placeholder Dapr client options (real ports patched after + // the container starts). Must be done BEFORE DaprContainer.start() so the gRPC app + // listener is already running when Dapr probes it for input binding subscriptions. server = new DaprServer({ serverHost, serverPort, communicationProtocol: CommunicationProtocolEnum.GRPC, clientOptions: { - daprHost, - daprPort, + daprHost: "127.0.0.1", + daprPort: "50001", // placeholder – patched below with the real mapped port + communicationProtocol: CommunicationProtocolEnum.GRPC, maxBodySizeMb: 20, // we set sending larger than receiving to test the error handling logger: { level: LogLevel.Debug, @@ -45,18 +70,53 @@ describe("grpc/server", () => { await server.binding.receive("binding-mqtt", mockBindingReceive); await server.invoker.listen("test-invoker", mockInvoker, { method: HttpMethod.POST }); - // Start server - await server.start(); + // Start ONLY the gRPC listener (no sidecar wait) so the app is already listening + // when the Dapr container probes it for input binding subscriptions during its init. + await server.daprServer.start(serverHost, serverPort); + + // Now start the Dapr container — it will call back to the running gRPC app and + // register the MQTT input binding. + daprContainer = await new DaprGrpcAppContainer() + .withNetwork(network) + .withAppId(daprAppId) + .withAppPort(parseInt(serverPort)) + .withAppChannelAddress("host.testcontainers.internal") + .withDaprLogLevel("info") + .withMaxRequestSizeMb(10) + .withComponent(buildBindingMqttComponent()) + .withComponent(buildBindingRedisComponent()) + .withComponent(buildConfigRedisComponent()) + .start(); + + // Patch the server's internal DaprClient with the real container ports. + (server as any).client = new DaprClient({ + daprHost: daprContainer.getHost(), + daprPort: daprContainer.getGrpcPort().toString(), + communicationProtocol: CommunicationProtocolEnum.GRPC, + maxBodySizeMb: 20, + logger: { + level: LogLevel.Debug, + }, + }); + + // Wait for the sidecar (which is already running) to be fully ready. + await server.client.start(); await new Promise((resolve, _reject) => setTimeout(resolve, 2500)); - }, 10 * 1000); + }, 300 * 1000); beforeEach(() => { mockBindingReceive.mockClear(); }); afterAll(async () => { - await server.stop(); + await runWithCleanupErrorSuppression(async () => { + await server.stop(); + await daprContainer.stop(); + await mqttContainer.stop(); + await redisContainer.stop(); + await network.stop(); + }); }); describe("server", () => { @@ -66,7 +126,7 @@ describe("grpc/server", () => { try { await server.client.invoker.invoke(daprAppId, "test-invoker", HttpMethod.POST, payload); } catch (e: any) { - expect(e?.details).toEqual(`grpc: received message larger than max (11534407 vs. ${10 * 1024 * 1024})`); + expect(e?.details).toContain(`vs. ${10 * 1024 * 1024}`); } }); @@ -114,9 +174,6 @@ describe("grpc/server", () => { await server.invoker.listen("hello-world", mock, { method: HttpMethod.GET }); const res = await server.client.invoker.invoke(daprAppId, "hello-world", HttpMethod.GET); - // Delay a bit for event to arrive - // await new Promise((resolve, reject) => setTimeout(resolve, 250)); - expect(mock.mock.calls.length).toBe(1); expect(JSON.stringify(res)).toEqual(`{"hello":"world"}`); }); @@ -129,11 +186,24 @@ describe("grpc/server", () => { hello: "world", }); - // Delay a bit for event to arrive - // await new Promise((resolve, reject) => setTimeout(resolve, 250)); - expect(mock.mock.calls.length).toBe(1); expect(JSON.stringify(res)).toEqual(`{"hello":"world"}`); }); + + it("should be able to listen and invoke a service with headers", async () => { + // Return a serializable object so the gRPC response is not empty (an empty + // response body causes the gRPC client to throw COULD_NOT_PARSE_RESULT). + const mock = jest.fn(async (data: DaprInvokerCallbackContent) => ({ receivedHeaders: data.headers ?? null })); + + await server.invoker.listen("hello-world-headers", mock, { method: HttpMethod.GET }); + const res = await server.client.invoker.invoke(daprAppId, "hello-world-headers", HttpMethod.GET, undefined, { + headers: { "x-foo": "bar-baz" }, + }); + + expect(mock.mock.calls.length).toBe(1); + // Headers are NOT forwarded to the gRPC app callback (documented in DaprInvokerCallbackContent + // and noted in GRPCServerImpl: "@TODO add call.metadata"). + expect(res).toEqual({ receivedHeaders: null }); + }); }); }); diff --git a/test/e2e/helpers/CustomNodeEnvironment.cjs b/test/e2e/helpers/CustomNodeEnvironment.cjs new file mode 100644 index 00000000..ca8ca8bc --- /dev/null +++ b/test/e2e/helpers/CustomNodeEnvironment.cjs @@ -0,0 +1,117 @@ +/* +Copyright 2025 The Dapr Authors +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. +*/ + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const NodeEnvironment = require("jest-environment-node"); + +/** + * Custom Jest test environment that installs a filter on the process object + * used by jest-circus to suppress spurious empty AggregateErrors emitted by + * testcontainers' ssh2/SubtleCrypto handles during container teardown. + * + * ## Background + * + * testcontainers uses ssh2 which creates SubtleCrypto key handles at + * module-load time. When these handles are garbage-collected during container + * teardown they abort their pending operations and emit unhandled promise + * rejections in the form of empty-message AggregateErrors. Jest-circus + * catches these via its `unhandledRejection` listener and surfaces them as + * "Test suite failed to run" even though every individual test passes. + * + * ## Why patching the real `process` doesn't help + * + * In Jest 27, `jest-circus/runner.js` is loaded by `runtime.requireModule()` + * inside the VM sandbox that jest-environment-node creates. Inside that + * sandbox, `process` resolves to `this.global.process` — the object that + * jest-environment-node installed when it constructed the environment — which + * is a **copy** of the real process created by jest-util's + * `createProcessObject()`. That copy may have its own `on` / `addListener` + * properties (bound to the real EventEmitter but as separate function + * references), so patching the real `process.on` has no effect on what + * jest-circus calls. + * + * `jest.setup.js` (via `setupFiles`) also runs inside the sandbox, so it + * suffers from the same problem. + * + * ## Mechanism + * + * `environment.setup()` runs in the outer Node.js context *before* + * `testFramework()` is invoked, and `this.global` already holds the fully + * initialised VM sandbox global. `this.global.process` is therefore the + * exact object that jest-circus/runner.js will see as its module-level + * `process` and pass as `parentProcess` to `injectGlobalErrorHandlers`. + * + * We patch `this.global.process.on` (and its EventEmitter aliases) here so + * that when jest-circus subsequently calls + * `parentProcess.on('unhandledRejection', uncaught)`, the registered handler + * is our *wrapped* version that silently drops empty-message AggregateErrors + * while forwarding everything else unchanged. + * + * We also patch the real Node.js `process` as a belt-and-suspenders measure + * for any listeners registered outside the VM sandbox (e.g. from + * globalSetup or from code that requires process directly). + * + * The `FILTER_FLAG` symbol guard prevents double-wrapping when multiple test + * files share the same worker process (e.g. with `--runInBand`). + */ + +function isEmptyAggregateError(reason) { + if (reason === null || typeof reason !== "object") return false; + return Array.isArray(reason.errors) && !reason.message; +} + +function wrapUnhandledRejectionHandler(handler) { + return function filteredUnhandledRejectionHandler(reason, promise) { + if (!isEmptyAggregateError(reason)) { + return Reflect.apply(handler, this, [reason, promise]); + } + // Suppress empty AggregateErrors silently; don't forward to jest-circus. + }; +} + +const FILTER_FLAG = Symbol.for("dapr.test.unhandledRejectionFiltered"); + +function patchProcessListeners(proc) { + if (proc[FILTER_FLAG]) return; // already patched + proc[FILTER_FLAG] = true; + for (const method of ["on", "addListener", "prependListener", "once"]) { + // Resolve the function to call — may be own property or inherited. + const fn = proc[method]; + if (typeof fn !== "function") continue; + const original = fn.bind(proc); + proc[method] = function filteredProcessOn(event, listener, ...rest) { + if (event === "unhandledRejection") { + return original(event, wrapUnhandledRejectionHandler(listener), ...rest); + } + return original(event, listener, ...rest); + }; + } +} + +class CustomNodeEnvironment extends NodeEnvironment { + async setup() { + await super.setup(); + + // Patch the process object that jest-circus will use as parentProcess. + // In Jest 27's VM sandbox, this.global.process is the object injected by + // jest-environment-node (potentially a createProcessObject() copy), which + // is the exact object that jest-circus/runner.js sees as its module-level + // `process` variable when loaded via runtime.requireModule(). + patchProcessListeners(this.global.process); + + // Also patch the real Node.js process as a belt-and-suspenders measure. + patchProcessListeners(process); + } +} + +module.exports = CustomNodeEnvironment; diff --git a/test/e2e/helpers/DaprGrpcAppContainer.ts b/test/e2e/helpers/DaprGrpcAppContainer.ts new file mode 100644 index 00000000..d02a0df6 --- /dev/null +++ b/test/e2e/helpers/DaprGrpcAppContainer.ts @@ -0,0 +1,225 @@ +/* +Copyright 2025 The Dapr Authors +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. +*/ + +/** + * DaprGrpcAppContainer + * + * A Dapr sidecar container that uses `--app-protocol grpc` so that Dapr + * communicates with the application via gRPC callbacks (bindings, subscriptions, + * invoker). This mirrors `DaprContainer` from `@dapr/testcontainer-node` but + * targets the gRPC app-channel protocol which is not yet configurable in the + * upstream package. + */ + +import { + AbstractStartedContainer, + GenericContainer, + StartedNetwork, + StartedTestContainer, + StopOptions, + StoppedTestContainer, + Wait, +} from "testcontainers"; +import { + Component, + DaprPlacementContainer, + DaprSchedulerContainer, +} from "@dapr/testcontainer-node"; +import { + DAPR_TEST_RUNTIME_IMAGE, + DAPR_TEST_PLACEMENT_IMAGE, + DAPR_TEST_SCHEDULER_IMAGE, +} from "./containers"; + +const DAPRD_HTTP_PORT = 3500; +const DAPRD_GRPC_PORT = 50001; + +export class DaprGrpcAppContainer extends GenericContainer { + private daprLogLevel = "info"; + private appId = "dapr-grpc-app"; + private appChannelAddress?: string; + private appPort?: number; + private maxRequestSizeMb?: number; + // Use distinct aliases so this container does not conflict with a DaprContainer + // running on the same network (DaprContainer uses "placement" / "scheduler"). + private placementAlias = "placement-grpc"; + private schedulerAlias = "scheduler-grpc"; + private startedNetwork?: StartedNetwork; + private components: Component[] = []; + private containerEnv: Record = {}; + + // Populated during start() before beforeContainerCreated() is called. + private placementContainer?: DaprPlacementContainer; + private schedulerContainer?: DaprSchedulerContainer; + + constructor(daprRuntimeImage = DAPR_TEST_RUNTIME_IMAGE) { + super(daprRuntimeImage); + this.withExposedPorts(DAPRD_HTTP_PORT, DAPRD_GRPC_PORT) + .withWaitStrategy( + Wait.forHttp("/v1.0/healthz/outbound", DAPRD_HTTP_PORT).forStatusCodeMatching( + (statusCode) => statusCode >= 200 && statusCode <= 399, + ), + ) + .withStartupTimeout(120_000); + } + + /** Must be called before start(). */ + withNetwork(network: StartedNetwork): this { + this.startedNetwork = network; + return super.withNetwork(network); + } + + withAppId(appId: string): this { + this.appId = appId; + return this; + } + + withAppPort(port: number): this { + this.appPort = port; + return this; + } + + withAppChannelAddress(address: string): this { + this.appChannelAddress = address; + return this; + } + + withDaprLogLevel(level: string): this { + this.daprLogLevel = level; + return this; + } + + withComponent(component: Component): this { + this.components.push(component); + return this; + } + + withPlacementAlias(alias: string): this { + this.placementAlias = alias; + return this; + } + + withSchedulerAlias(alias: string): this { + this.schedulerAlias = alias; + return this; + } + + withContainerEnvironment(env: Record): this { + this.containerEnv = { ...this.containerEnv, ...env }; + return this; + } + + withMaxRequestSizeMb(mb: number): this { + this.maxRequestSizeMb = mb; + return this; + } + + async start(): Promise { + if (!this.startedNetwork) { + throw new Error("Network must be provided before starting DaprGrpcAppContainer"); + } + + // Start placement and scheduler before the main container so that + // beforeContainerCreated() can reference their internal ports. + this.placementContainer = new DaprPlacementContainer(DAPR_TEST_PLACEMENT_IMAGE) + .withNetwork(this.startedNetwork) + .withNetworkAliases(this.placementAlias); + + this.schedulerContainer = new DaprSchedulerContainer(DAPR_TEST_SCHEDULER_IMAGE) + .withNetwork(this.startedNetwork) + .withNetworkAliases(this.schedulerAlias); + + const [startedPlacement, startedScheduler] = await Promise.all([ + this.placementContainer.start(), + this.schedulerContainer.start(), + ]); + + if (Object.keys(this.containerEnv).length > 0) { + this.withEnvironment(this.containerEnv); + } + + return new StartedGrpcDaprContainer(await super.start(), [startedPlacement, startedScheduler]); + } + + protected override async beforeContainerCreated(): Promise { + if (!this.placementContainer || !this.schedulerContainer) { + throw new Error("Placement and scheduler containers must be started before the Dapr container"); + } + + const cmds = [ + "./daprd", + "--app-id", + this.appId, + "--dapr-listen-addresses=0.0.0.0", + "--app-protocol", + "grpc", // <-- key difference from DaprContainer + "--placement-host-address", + `${this.placementAlias}:${this.placementContainer.getPort()}`, + "--scheduler-host-address", + `${this.schedulerAlias}:${this.schedulerContainer.getPort()}`, + "--log-level", + this.daprLogLevel, + "--resources-path", + "/dapr-resources", + ]; + + if (this.appChannelAddress) { + cmds.push("--app-channel-address", this.appChannelAddress); + } + if (this.appPort) { + cmds.push("--app-port", this.appPort.toString()); + } + if (this.maxRequestSizeMb !== undefined) { + cmds.push("--dapr-http-max-request-size", this.maxRequestSizeMb.toString()); + } + + this.withCommand(cmds); + + for (const component of this.components) { + this.withCopyContentToContainer([ + { content: component.toYaml(), target: `/dapr-resources/${component.name}.yaml` }, + ]); + } + } +} + +export class StartedGrpcDaprContainer extends AbstractStartedContainer { + constructor( + startedTestContainer: StartedTestContainer, + private readonly supportingContainers: StartedTestContainer[], + ) { + super(startedTestContainer); + } + + async stop(options?: Partial): Promise { + const stopped = await super.stop(options); + await Promise.all(this.supportingContainers.map((c) => c.stop(options))); + return stopped; + } + + getHttpPort(): number { + return this.getMappedPort(DAPRD_HTTP_PORT); + } + + getHttpEndpoint(): string { + return `http://${this.getHost()}:${this.getMappedPort(DAPRD_HTTP_PORT)}`; + } + + getGrpcPort(): number { + return this.getMappedPort(DAPRD_GRPC_PORT); + } + + getGrpcEndpoint(): string { + return `:${this.getMappedPort(DAPRD_GRPC_PORT)}`; + } +} diff --git a/test/e2e/helpers/containers.ts b/test/e2e/helpers/containers.ts new file mode 100644 index 00000000..40bcdf65 --- /dev/null +++ b/test/e2e/helpers/containers.ts @@ -0,0 +1,235 @@ +/* +Copyright 2025 The Dapr Authors +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. +*/ + +import { GenericContainer, StartedTestContainer, StartedNetwork, Wait } from "testcontainers"; +import { Component, DaprContainer, DAPR_VERSION } from "@dapr/testcontainer-node"; + +// ------------------------------------------------------------------ +// Version resolution +// +// Set the DAPR_RUNTIME_VER environment variable to test against a specific +// Dapr version (e.g. an RC or N-1 stable). When unset, the version pinned +// in @dapr/testcontainer-node is used. +// +// All three images (daprd, placement, scheduler) always use the same version +// so that the sidecar and its supporting services are always in sync. +// +// Example: +// DAPR_RUNTIME_VER=1.15.0 npm run test:e2e:all +// ------------------------------------------------------------------ + +const DAPR_TEST_VERSION = process.env.DAPR_RUNTIME_VER || DAPR_VERSION; + +/** daprd image at the configured test version. */ +export const DAPR_TEST_RUNTIME_IMAGE = `daprio/daprd:${DAPR_TEST_VERSION}`; + +/** placement image – always matches the runtime version. */ +export const DAPR_TEST_PLACEMENT_IMAGE = `daprio/placement:${DAPR_TEST_VERSION}`; + +/** scheduler image – always matches the runtime version. */ +export const DAPR_TEST_SCHEDULER_IMAGE = `daprio/scheduler:${DAPR_TEST_VERSION}`; + +// ------------------------------------------------------------------ +// Container starters +// ------------------------------------------------------------------ + +/** + * Starts a Redis container on the given network with the alias "redis". + */ +export async function startRedisContainer(network: StartedNetwork): Promise { + return new GenericContainer("redis:7") + .withNetwork(network) + .withNetworkAliases("redis") + .withExposedPorts(6379) + .withWaitStrategy(Wait.forLogMessage(/Ready to accept connections/)) + .withStartupTimeout(60_000) + .start(); +} + +/** + * Starts a MongoDB container on the given network with the alias "mongodb". + */ +export async function startMongoDbContainer(network: StartedNetwork): Promise { + return new GenericContainer("mongo:7") + .withNetwork(network) + .withNetworkAliases("mongodb") + .withExposedPorts(27017) + .withWaitStrategy(Wait.forLogMessage(/Waiting for connections/)) + .withStartupTimeout(60_000) + .start(); +} + +/** + * Starts an EMQX MQTT broker on the given network with the alias "mqtt". + */ +export async function startMqttContainer(network: StartedNetwork): Promise { + return new GenericContainer("emqx/emqx:5.10.3") + .withNetwork(network) + .withNetworkAliases("mqtt") + .withExposedPorts(1883, 18083) + // Shorten the QoS1 re-delivery interval so RETRY pubsub tests don't have to wait 30 s per retry + .withEnvironment({ EMQX_MQTT__RETRY_INTERVAL: "3s" }) + .withWaitStrategy(Wait.forLogMessage(/EMQX.*is running now/)) + .withStartupTimeout(120_000) + .start(); +} + +// ------------------------------------------------------------------ +// Component builders (all hosts use Docker network aliases) +// ------------------------------------------------------------------ + +/** state.redis component backed by the "redis" network alias. */ +export function buildStateRedisComponent(actorStateStore = false): Component { + return new Component("state-redis", "state.redis", "v1", [ + { name: "redisHost", value: "redis:6379" }, + { name: "redisPassword", value: "" }, + { name: "enableTLS", value: "false" }, + { name: "failover", value: "false" }, + { name: "actorStateStore", value: actorStateStore ? "true" : "false" }, + ]); +} + +/** pubsub.redis component backed by the "redis" network alias. */ +export function buildPubSubRedisComponent(): Component { + return new Component("pubsub-redis", "pubsub.redis", "v1", [ + { name: "redisHost", value: "redis:6379" }, + { name: "redisPassword", value: "" }, + { name: "enableTLS", value: "false" }, + ]); +} + +/** configuration.redis component backed by the "redis" network alias. */ +export function buildConfigRedisComponent(): Component { + return new Component("config-redis", "configuration.redis", "v1", [ + { name: "redisHost", value: "redis:6379" }, + { name: "redisPassword", value: "" }, + ]); +} + +/** lock.redis component backed by the "redis" network alias. */ +export function buildLockRedisComponent(): Component { + return new Component("redislock", "lock.redis", "v1", [ + { name: "redisHost", value: "redis:6379" }, + { name: "redisPassword", value: "" }, + ]); +} + +/** bindings.redis component backed by the "redis" network alias. */ +export function buildBindingRedisComponent(): Component { + return new Component("binding-redis", "bindings.redis", "v1", [ + { name: "redisHost", value: "redis:6379" }, + { name: "redisPassword", value: "" }, + ]); +} + +/** state.mongodb component backed by the "mongodb" network alias. */ +export function buildStateMongoDbComponent(): Component { + return new Component("state-mongodb", "state.mongodb", "v1", [{ name: "host", value: "mongodb:27017" }]); +} + +/** + * bindings.mqtt3 component backed by the "mqtt" network alias. + * Uses the default EMQX credentials (admin/public). + */ +export function buildBindingMqttComponent(): Component { + return new Component("binding-mqtt", "bindings.mqtt3", "v1", [ + { name: "consumerID", value: "e2e" }, + { name: "url", value: "tcp://admin:public@mqtt:1883" }, + { name: "topic", value: "topic-testing" }, + { name: "retain", value: "false" }, + { name: "cleanSession", value: "false" }, + ]); +} + +/** + * pubsub.mqtt3 component backed by the "mqtt" network alias. + * Uses the default EMQX credentials (admin/public). + */ +export function buildPubSubMqttComponent(): Component { + return new Component("pubsub-mqtt", "pubsub.mqtt3", "v1", [ + { name: "url", value: "tcp://admin:public@mqtt:1883" }, + { name: "qos", value: "1" }, + { name: "cleanSession", value: "true" }, + { name: "backOffMaxRetries", value: "0" }, + ]); +} + +/** secretstores.local.env component (reads env vars from the daprd process). */ +export function buildSecretEnvvarsComponent(): Component { + return new Component("secret-envvars", "secretstores.local.env", "v1", []); +} + +/** + * crypto.dapr.jwks component with an embedded symmetric key for testing. + */ +export function buildCryptoLocalComponent(): Component { + return new Component("crypto-local", "crypto.dapr.jwks", "v1", [ + { + name: "jwks", + value: JSON.stringify({ + keys: [{ kid: "symmetric256", kty: "oct", k: "RjJPhQzsDB5dvjQZ-85l_D_SBXWCBFx7IVsesenVvts" }], + }), + }, + ]); +} + +/** pubsub.in-memory component — used as a fallback so Dapr starts cleanly. */ +export function buildInMemoryPubSubComponent(name = "pubsub"): Component { + return new Component(name, "pubsub.in-memory", "v1", []); +} + +/** + * Runs a cleanup function, then briefly yields the microtask queue so that any + * lingering testcontainers/ssh2 AggregateErrors can be dispatched and suppressed + * by the global process.emit filter installed in jest.setup.js. + * + * Usage in `afterAll`: + * ```ts + * afterAll(() => runWithCleanupErrorSuppression(async () => { + * await server.stop(); + * await daprContainer.stop(); + * await network.stop(); + * }), 60_000); + * ``` + */ +export async function runWithCleanupErrorSuppression(fn: () => Promise): Promise { + await fn(); + // Brief yield so that any micro-tasks queued by cleanup can be dispatched and + // filtered by the global process.emit override in jest.setup.js before this + // function returns. + await new Promise((resolve) => setTimeout(resolve, 300)); +} + +/** + * DaprContainer extension that appends `--dapr-http-max-request-size ` to + * the daprd command built by the base class. Required for tests that send or + * receive payloads larger than Dapr's default 4 MB HTTP body limit. + */ +export class DaprContainerWithLargeBody extends DaprContainer { + private readonly maxRequestSizeMb: number; + + constructor(image: string, maxRequestSizeMb: number) { + super(image); + this.maxRequestSizeMb = maxRequestSizeMb; + } + + protected override async beforeContainerCreated(): Promise { + await super.beforeContainerCreated(); + // createOpts.Cmd is set by DaprContainer.beforeContainerCreated(); append our flag. + this.createOpts.Cmd = [ + ...(this.createOpts.Cmd ?? []), + "--dapr-http-max-request-size", + this.maxRequestSizeMb.toString(), + ]; + } +} diff --git a/test/e2e/helpers/filterAggregateErrors.cjs b/test/e2e/helpers/filterAggregateErrors.cjs new file mode 100644 index 00000000..1bddbb81 --- /dev/null +++ b/test/e2e/helpers/filterAggregateErrors.cjs @@ -0,0 +1,159 @@ +/* +Copyright 2025 The Dapr Authors +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. +*/ + +/** + * Jest `testResultsProcessor` that removes spurious "Test suite failed to run" + * entries caused by empty-message AggregateErrors emitted by testcontainers' + * ssh2/SubtleCrypto handles during container teardown (GC). + * + * These errors fire as unhandled promise rejections after the test file's + * `afterAll` block completes. Jest-circus catches them via its + * `unhandledRejection` listener, stores them in `state.unhandledErrors`, and + * converts them into a `testExecError` on the suite result even though every + * individual test passed. + * + * ## Why the simple `suite.status === "failed"` check does NOT work + * + * `runAndTransformResultsToJestFormat` (jest-circus) builds the suite result + * from `createEmptyTestResult()` which has NO `status` field. The `status` + * field on a suite result is `undefined` – it is only defined on individual + * test-case results. Checking `suite.status !== "failed"` always skips the + * suite even when it has a `testExecError`. + * + * ## How jest-circus builds testExecError + * + * In `jestAdapterInit.js`, when `runResult.unhandledErrors.length > 0`: + * - `testExecError.message` is **always set to `''`** (hard-coded empty string) + * - `testExecError.stack` = `unhandledErrors.join('\n')` + * + * Each element of `unhandledErrors` is the result of `getErrorStack(error)`: + * - `error.stack` if it is a string (e.g. `"AggregateError\n at ...\n at ..."`) + * - otherwise `error.message` + * + * So when jest-circus accumulates 13 ssh2/SubtleCrypto GC AggregateErrors, + * `testExecError.stack` contains all 13 full stack traces joined with '\n' — + * each one looking like `"AggregateError\n at processTicksAndMicrotasks ..."`. + * `testExecError.message` is always `''` regardless. + * + * ## Correct detection + * + * A suite result spoiled solely by ssh2/SubtleCrypto empty-message AggregateErrors: + * 1. `suite.testExecError` is non-null. + * 2. `suite.testExecError.message` is `''` (always true for jest-circus unhandled errors). + * 3. `suite.testExecError.stack` contains only: + * - blank lines + * - `"AggregateError"` or `"AggregateError:"` header lines (no message) + * - `"at ..."` stack-frame lines (allowed — real AggregateErrors always have frames) + * Any line of the form `"AggregateError: "` marks the suite + * as having a real error and prevents suppression. + * 4. `suite.numFailingTests === 0` — every individual test passed. + * + * ## What we fix + * + * We clear `testExecError` and `failureMessage` on the matching suite(s) and + * decrement both `numFailedTestSuites` and `numRuntimeErrorTestSuites` (the + * latter is what drives `anyTestFailures` → `results.success = false`). + * Finally we set `results.success = true` so Jest exits with code 0. + */ + +/** + * Returns true when every line in `str` is either: + * - blank / whitespace-only + * - an `AggregateError` header with no message: `"AggregateError"` or `"AggregateError:"` + * - a stack-frame line that starts with `"at "` after trimming + * + * Returns false when any line contains a real error message + * (e.g. `"AggregateError: some meaningful message"`) or an unrecognised error type. + * + * Accepts null/undefined/empty (absent content is considered all-clear). + * + * @param {string|null|undefined} str + * @returns {boolean} + */ +function isAggregateErrorOnlyContent(str) { + if (str === null || str === undefined || str === "") return true; + return str.split("\n").every((line) => { + const trimmed = line.trim(); + if (trimmed === "") return true; + // Stack-frame lines produced by Node.js error.stack (always present in real AggregateErrors) + if (trimmed.startsWith("at ")) return true; + // AggregateError header with no message + if (trimmed === "AggregateError") return true; + if (trimmed === "AggregateError:") return true; + // Anything else (AggregateError with a real message, other error types, …) → real error + return false; + }); +} + +/** + * Returns true if the suite result was spoiled solely by empty-message + * AggregateErrors from testcontainers/ssh2 handle GC. + * + * @param {import('@jest/test-result').TestResult} suite + */ +function isSpuriousAggregateErrorSuite(suite) { + // Must have a testExecError (this is how jest-circus records unhandled errors) + if (!suite.testExecError) return false; + // jest-circus always sets testExecError.message = '' for unhandled rejections; + // any truthy message (even just "AggregateError\nAggregateError") indicates a + // real error accumulated across multiple rejections — but we still verify with + // isAggregateErrorOnlyContent in case the internal format ever changes. + if (!isAggregateErrorOnlyContent(suite.testExecError.message)) return false; + // testExecError.stack = unhandledErrors.join('\n') where each element is + // error.stack (full stack trace including "at …" frames) or error.message. + // We must allow "at …" lines — all real AggregateErrors have them. + if (!isAggregateErrorOnlyContent(suite.testExecError.stack)) return false; + // All individual tests must have passed (no genuine test failures) + if (suite.numFailingTests !== 0) return false; + return true; +} + +/** + * Jest testResultsProcessor entry point. + * @param {import('@jest/test-result').AggregatedResult} results + * @returns {import('@jest/test-result').AggregatedResult} + */ +module.exports = function filterAggregateErrors(results) { + let fixedCount = 0; + + results.testResults = results.testResults.map((suite) => { + if (!isSpuriousAggregateErrorSuite(suite)) return suite; + + fixedCount++; + return { + ...suite, + testExecError: null, + failureMessage: null, + }; + }); + + if (fixedCount > 0) { + results.numFailedTestSuites = Math.max(0, (results.numFailedTestSuites || 0) - fixedCount); + results.numPassedTestSuites = (results.numPassedTestSuites || 0) + fixedCount; + // numRuntimeErrorTestSuites is what drives anyTestFailures → results.success = false + results.numRuntimeErrorTestSuites = Math.max( + 0, + (results.numRuntimeErrorTestSuites || 0) - fixedCount + ); + // Update top-level success flag so Jest exits with code 0 + if ( + results.numFailedTestSuites === 0 && + (results.numFailedTests || 0) === 0 && + results.numRuntimeErrorTestSuites === 0 + ) { + results.success = true; + } + } + + return results; +}; diff --git a/test/e2e/http/actors.test.ts b/test/e2e/http/actors.test.ts index 6780d972..a42837a6 100644 --- a/test/e2e/http/actors.test.ts +++ b/test/e2e/http/actors.test.ts @@ -11,12 +11,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { CommunicationProtocolEnum, DaprClient, DaprClientOptions, DaprServer } from "../../../src"; +import { CommunicationProtocolEnum, DaprClient, DaprServer } from "../../../src"; import fetch from "node-fetch"; +import { Network, StartedNetwork, StartedTestContainer, TestContainers } from "testcontainers"; +import { DaprContainer, StartedDaprContainer } from "@dapr/testcontainer-node"; import * as NodeJSUtil from "../../../src/utils/NodeJS.util"; import ActorId from "../../../src/actors/ActorId"; import ActorProxyBuilder from "../../../src/actors/client/ActorProxyBuilder"; +import ActorRuntime from "../../../src/actors/runtime/ActorRuntime"; +import AbstractActor from "../../../src/actors/runtime/AbstractActor"; +import Class from "../../../src/types/Class"; import DemoActorActivateImpl from "../../actor/DemoActorActivateImpl"; import DemoActorCounterImpl from "../../actor/DemoActorCounterImpl"; import DemoActorCounterInterface from "../../actor/DemoActorCounterInterface"; @@ -33,38 +38,64 @@ import DemoActorTimerTtlImpl from "../../actor/DemoActorTimerTtlImpl"; import DemoActorReminderTtlImpl from "../../actor/DemoActorReminderTtlImpl"; import DemoActorDeleteStateImpl from "../../actor/DemoActorDeleteStateImpl"; import DemoActorDeleteStateInterface from "../../actor/DemoActorDeleteStateInterface"; +import { + startRedisContainer, + buildStateRedisComponent, + DAPR_TEST_RUNTIME_IMAGE, + DAPR_TEST_PLACEMENT_IMAGE, + DAPR_TEST_SCHEDULER_IMAGE, + runWithCleanupErrorSuppression, +} from "../helpers/containers"; const serverHost = "127.0.0.1"; -const serverPort = "50001"; -const sidecarHost = "127.0.0.1"; -const sidecarPort = "50000"; -const serverStartWaitTimeMs = 5 * 1000; - -const daprClientOptions: DaprClientOptions = { - daprHost: sidecarHost, - daprPort: sidecarPort, - communicationProtocol: CommunicationProtocolEnum.HTTP, - isKeepAlive: false, - actor: { - actorIdleTimeout: "1h", - actorScanInterval: "30s", - drainOngoingCallTimeout: "1m", - drainRebalancedActors: true, - reentrancy: { - enabled: true, - maxStackDepth: 32, - }, - remindersStoragePartitions: 0, +const serverPort = "3001"; +// Dapr actor placement tables can take up to ~20 seconds to propagate in CI after the sidecar +// reports healthy. We wait here (inside beforeAll which has a 300 second budget) to avoid +// "actor.init()" hanging inside individual tests and consuming their timeout budget. +const serverStartWaitTimeMs = 20 * 1000; + +const actorOptions = { + actorIdleTimeout: "1h", + actorScanInterval: "30s", + drainOngoingCallTimeout: "1m", + drainRebalancedActors: true, + reentrancy: { + enabled: true, + maxStackDepth: 32, }, + remindersStoragePartitions: 0, }; +// Actor implementations registered with every test-run server instance. +const DEMO_ACTORS: Class[] = [ + DemoActorCounterImpl, + DemoActorSayImpl, + DemoActorReminderImpl, + DemoActorReminder2Impl, + DemoActorReminderOnceImpl, + DemoActorTimerImpl, + DemoActorTimerOnceImpl, + DemoActorActivateImpl, + DemoActorTimerTtlImpl, + DemoActorReminderTtlImpl, + DemoActorDeleteStateImpl, +]; + describe("http/actors", () => { let server: DaprServer; let client: DaprClient; + let network: StartedNetwork; + let redisContainer: StartedTestContainer; + let daprContainer: StartedDaprContainer; - // We need to start listening on some endpoints already - // this because Dapr is not dynamic and registers endpoints on boot beforeAll(async () => { + network = await new Network().start(); + redisContainer = await startRedisContainer(network); + + // Allow the Dapr container to call back to the app server on the host. + await TestContainers.exposeHostPorts(parseInt(serverPort)); + + // Create server with placeholder dapr client options (real ports patched after container starts). // Start server and client with keepAlive on the client set to false. // this means that we won't re-use connections here which is necessary for the tests // since it will keep handles open else it has to be initialized before the server starts! @@ -72,39 +103,88 @@ describe("http/actors", () => { serverHost, serverPort, communicationProtocol: CommunicationProtocolEnum.HTTP, - clientOptions: daprClientOptions, + clientOptions: { + daprHost: "127.0.0.1", + daprPort: "3500", + communicationProtocol: CommunicationProtocolEnum.HTTP, + isKeepAlive: false, + actor: actorOptions, + }, }); - client = new DaprClient(daprClientOptions); - // This will initialize the actor routes. // Actors themselves can be initialized later await server.actor.init(); - await server.actor.registerActor(DemoActorCounterImpl); - await server.actor.registerActor(DemoActorSayImpl); - await server.actor.registerActor(DemoActorReminderImpl); - await server.actor.registerActor(DemoActorReminder2Impl); - await server.actor.registerActor(DemoActorReminderOnceImpl); - await server.actor.registerActor(DemoActorTimerImpl); - await server.actor.registerActor(DemoActorTimerOnceImpl); - await server.actor.registerActor(DemoActorActivateImpl); - await server.actor.registerActor(DemoActorTimerTtlImpl); - await server.actor.registerActor(DemoActorReminderTtlImpl); - await server.actor.registerActor(DemoActorDeleteStateImpl); - - // Start server - await server.start(); // Start the general server, this can take a while + await Promise.all(DEMO_ACTORS.map((cls) => server.actor.registerActor(cls))); + + // Start ONLY the HTTP listener (no sidecar wait) so the app is already + // listening when the Dapr container probes /dapr/config during its own init. + // This ensures actor types are registered with the placement service. + await server.daprServer.start(serverHost, serverPort); + + // Now start the Dapr container — it will call /dapr/config on the app and + // register the actor types with the placement service. + daprContainer = await new DaprContainer(DAPR_TEST_RUNTIME_IMAGE) + .withPlacementImage(DAPR_TEST_PLACEMENT_IMAGE) + .withSchedulerImage(DAPR_TEST_SCHEDULER_IMAGE) + .withNetwork(network) + .withAppPort(parseInt(serverPort)) + .withAppChannelAddress("host.testcontainers.internal") + // actorStateStore must be "true" for actor support + .withComponent(buildStateRedisComponent(true)) + .start(); + + // Build a DaprClient pointing at the real container ports. + const realClientOptions = { + daprHost: daprContainer.getHost(), + daprPort: daprContainer.getHttpPort().toString(), + communicationProtocol: CommunicationProtocolEnum.HTTP, + isKeepAlive: false, + actor: actorOptions, + }; + + // Patch the server's DaprClient reference so server.client.start() connects to the + // real sidecar port and so that the server actor router uses the real Dapr HTTP port + // for the ActorRuntime singleton (used when actor methods call back to Dapr for + // timer/reminder registration and state operations). + const realServerClient = new DaprClient(realClientOptions); + (server as any).client = realServerClient; + + // HTTPServerActor holds its own private `client` reference that was set in the + // constructor (before the container started). Patch it so that calls to + // ActorRuntime.getInstance(this.client.daprClient) inside every actor HTTP handler + // pass the correct Dapr port. Also reset the ActorRuntime singleton so it is + // re-created with the real DaprClient on the next getInstance() call. + (server.actor as any).client = realServerClient; + ActorRuntime.resetForTesting(); + + // Re-register all actors against the new singleton (which will be lazily created + // with realServerClient on the first registerActor / getInstance call). + await Promise.all(DEMO_ACTORS.map((cls) => server.actor.registerActor(cls))); + + // Create the standalone client used for actor proxy tests. + client = new DaprClient(realClientOptions); + + // Explicitly start both clients so that the sidecar health check completes here + // rather than lazily inside individual tests (which would consume their timeout budget). + await server.client.start(); + await client.start(); // Wait for actor placement tables to fully start up // TODO: Remove this once healthz is fixed (https://github.com/dapr/dapr/issues/3451) await NodeJSUtil.sleep(serverStartWaitTimeMs); - }, 30 * 1000); + }, 300 * 1000); // We need to stop the server after all tests are done // Note: it can take > 5s so increase timeout as we are testing reminders and timers afterAll(async () => { - await server.stop(); - }, 30 * 1000); + await runWithCleanupErrorSuppression(async () => { + await server.stop(); + await daprContainer.stop(); + await redisContainer.stop(); + await network.stop(); + }); + }, 60 * 1000); describe("configuration", () => { it("actor configuration endpoint should contain the correct parameters", async () => { @@ -167,7 +247,7 @@ describe("http/actors", () => { const actorId = ActorId.createRandomId(); builder.build(actorId); - const baseActorUrl = `http://${sidecarHost}:${sidecarPort}/v1.0/actors/DemoActorCounterImpl/${actorId.toString()}/method`; + const baseActorUrl = `http://${daprContainer.getHost()}:${daprContainer.getHttpPort()}/v1.0/actors/DemoActorCounterImpl/${actorId.toString()}/method`; const validFunc = await fetch(`${baseActorUrl}/getCounter`); expect(validFunc.status).toBe(200); @@ -183,7 +263,7 @@ describe("http/actors", () => { it("should be able to delete actor state", async () => { const builder = new ActorProxyBuilder(DemoActorDeleteStateImpl, client); const actor = builder.build(ActorId.createRandomId()); - await actor.init(); + await actor.init(); // actor.init() can take a few seconds if placement tables are still propagating const res = await actor.tryGetState(); expect(res).toEqual(true); @@ -193,7 +273,7 @@ describe("http/actors", () => { const deletedRes = await actor.tryGetState(); console.log(deletedRes); expect(deletedRes).toEqual(false); - }); + }, 30000); }); describe("invoke", () => { it("should register actors correctly", async () => { @@ -282,7 +362,7 @@ describe("http/actors", () => { await new Promise((resolve) => setTimeout(resolve, 1000)); const res4 = await actor.getCounter(); expect(res4).toEqual(300); - }, 10000); + }, 60000); it("should apply the ttl when it is set (expected execution time > 5s)", async () => { const builder = new ActorProxyBuilder(DemoActorTimerTtlImpl, client); @@ -319,7 +399,7 @@ describe("http/actors", () => { await new Promise((resolve) => setTimeout(resolve, 1000)); const res4 = await actor.getCounter(); expect(res4).toEqual(200); - }, 10000); + }, 60000); it("should only fire once when period is not set to a timer", async () => { const builder = new ActorProxyBuilder(DemoActorTimerOnceImpl, client); @@ -345,7 +425,7 @@ describe("http/actors", () => { // Make sure the counter didn't change const res2 = await actor.getCounter(); expect(res2).toEqual(100); - }, 5000); + }, 30000); }); describe("reminders", () => { @@ -376,7 +456,7 @@ describe("http/actors", () => { // Make sure the counter didn't change since we removed the reminder const res2 = await actor.getCounter(); expect(res2).toEqual(res1); - }); + }, 30000); it("should fire a reminder but with a warning if it's not implemented correctly", async () => { const builder = new ActorProxyBuilder(DemoActorReminder2Impl, client); @@ -404,7 +484,7 @@ describe("http/actors", () => { // Unregister the reminder await actor.removeReminder(); - }); + }, 30000); it("should apply the ttl when it is set to a reminder", async () => { const builder = new ActorProxyBuilder(DemoActorReminderTtlImpl, client); @@ -432,7 +512,7 @@ describe("http/actors", () => { // Make sure the counter didn't change const res2 = await actor.getCounter(); expect(res2).toEqual(123); - }); + }, 30000); it("should only fire once when period is not set to a reminder", async () => { const builder = new ActorProxyBuilder(DemoActorReminderOnceImpl, client); @@ -458,6 +538,6 @@ describe("http/actors", () => { // Make sure the counter didn't change const res2 = await actor.getCounter(); expect(res2).toEqual(100); - }, 5000); + }, 30000); }); }); diff --git a/test/e2e/http/client.test.ts b/test/e2e/http/client.test.ts index c12e6a69..29cfe388 100644 --- a/test/e2e/http/client.test.ts +++ b/test/e2e/http/client.test.ts @@ -11,28 +11,56 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { Network, StartedNetwork, StartedTestContainer } from "testcontainers"; +import { DaprContainer, StartedDaprContainer } from "@dapr/testcontainer-node"; import { CommunicationProtocolEnum, DaprClient } from "../../../src"; - -const daprHost = "127.0.0.1"; -const daprPort = "50000"; // Dapr Sidecar Port of this Example Server +import { + startRedisContainer, + buildStateRedisComponent, + buildSecretEnvvarsComponent, + buildInMemoryPubSubComponent, + DAPR_TEST_RUNTIME_IMAGE, + DAPR_TEST_PLACEMENT_IMAGE, + DAPR_TEST_SCHEDULER_IMAGE, + runWithCleanupErrorSuppression, +} from "../helpers/containers"; describe("http/client", () => { let client: DaprClient; + let network: StartedNetwork; + let redisContainer: StartedTestContainer; + let daprContainer: StartedDaprContainer; - // We need to start listening on some endpoints already - // this because Dapr is not dynamic and registers endpoints on boot - // we put a timeout of 10s since it takes around 4s for Dapr to boot up beforeAll(async () => { + network = await new Network().start(); + redisContainer = await startRedisContainer(network); + + daprContainer = await new DaprContainer(DAPR_TEST_RUNTIME_IMAGE) + .withPlacementImage(DAPR_TEST_PLACEMENT_IMAGE) + .withSchedulerImage(DAPR_TEST_SCHEDULER_IMAGE) + .withNetwork(network) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(buildStateRedisComponent()) + .withComponent(buildSecretEnvvarsComponent()) + .withComponent(buildInMemoryPubSubComponent()) + .withEnvironment({ TEST_SECRET_1: "secret_val_1", TEST_SECRET_2: "secret_val_2" }) + .start(); + client = new DaprClient({ - daprHost, - daprPort, + daprHost: daprContainer.getHost(), + daprPort: daprContainer.getHttpPort().toString(), communicationProtocol: CommunicationProtocolEnum.HTTP, isKeepAlive: false, }); - }, 10 * 1000); + }, 180 * 1000); afterAll(async () => { - await client.stop(); + await runWithCleanupErrorSuppression(async () => { + await client.stop(); + await daprContainer.stop(); + await redisContainer.stop(); + await network.stop(); + }); }); describe("sidecar", () => { diff --git a/test/e2e/http/server.test.ts b/test/e2e/http/server.test.ts index d431c9d3..9094d223 100644 --- a/test/e2e/http/server.test.ts +++ b/test/e2e/http/server.test.ts @@ -13,32 +13,55 @@ limitations under the License. import express from "express"; import fetch from "node-fetch"; -import { CommunicationProtocolEnum, DaprServer, HttpMethod } from "../../../src"; +import { Network, StartedNetwork, StartedTestContainer, TestContainers } from "testcontainers"; +import { StartedDaprContainer } from "@dapr/testcontainer-node"; +import { CommunicationProtocolEnum, DaprClient, DaprServer, HttpMethod } from "../../../src"; import { DaprInvokerCallbackContent } from "../../../src/types/DaprInvokerCallback.type"; import { KeyValueType } from "../../../src/types/KeyValue.type"; +import { + startRedisContainer, + startMqttContainer, + buildBindingMqttComponent, + buildInMemoryPubSubComponent, + DAPR_TEST_RUNTIME_IMAGE, + DAPR_TEST_PLACEMENT_IMAGE, + DAPR_TEST_SCHEDULER_IMAGE, + DaprContainerWithLargeBody, + runWithCleanupErrorSuppression, +} from "../helpers/containers"; const serverHost = "127.0.0.1"; -const serverPort = "50001"; -const daprHost = "127.0.0.1"; -const daprPort = "50000"; // Dapr Sidecar Port of this Example Server -const daprAppId = "test-suite"; +const serverPort = "3001"; +const daprAppId = "test-suite-http-server"; describe("http/server", () => { let server: DaprServer; + let network: StartedNetwork; + let redisContainer: StartedTestContainer; + let mqttContainer: StartedTestContainer; + let daprContainer: StartedDaprContainer; + const mockBindingReceive = jest.fn(async (_data: object) => null); const mockInvoke = jest.fn(async (_data: object) => null); - // We need to start listening on some endpoints already - // this because Dapr is not dynamic and registers endpoints on boot - // we put a timeout of 10s since it takes around 4s for Dapr to boot up beforeAll(async () => { + network = await new Network().start(); + [redisContainer, mqttContainer] = await Promise.all([ + startRedisContainer(network), + startMqttContainer(network), + ]); + + // Allow the Dapr container to call back to the app server on the host. + await TestContainers.exposeHostPorts(parseInt(serverPort)); + + // Create the server with placeholder dapr client options. server = new DaprServer({ serverHost, serverPort, communicationProtocol: CommunicationProtocolEnum.HTTP, clientOptions: { - daprHost, - daprPort, + daprHost: "127.0.0.1", + daprPort: "3500", maxBodySizeMb: 20, // we set sending larger than receiving to test the error handling }, maxBodySizeMb: 10, @@ -46,9 +69,31 @@ describe("http/server", () => { await server.binding.receive("binding-mqtt", mockBindingReceive); - // Start server - await server.start(); - }, 10 * 1000); + // Start ONLY the HTTP listener (no sidecar wait) so the app is already + // listening when the Dapr container probes it during its own init. + await server.daprServer.start(serverHost, serverPort); + + daprContainer = await new DaprContainerWithLargeBody(DAPR_TEST_RUNTIME_IMAGE, 20) + .withPlacementImage(DAPR_TEST_PLACEMENT_IMAGE) + .withSchedulerImage(DAPR_TEST_SCHEDULER_IMAGE) + .withNetwork(network) + .withAppName(daprAppId) + .withAppPort(parseInt(serverPort)) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(buildBindingMqttComponent()) + .withComponent(buildInMemoryPubSubComponent()) + .start(); + + // Patch the DaprClient with the real container ports, then wait for the + // sidecar (which is already running by this point). + (server as any).client = new DaprClient({ + daprHost: daprContainer.getHost(), + daprPort: daprContainer.getHttpPort().toString(), + communicationProtocol: CommunicationProtocolEnum.HTTP, + maxBodySizeMb: 20, + }); + await server.client.start(); + }, 300 * 1000); beforeEach(() => { mockBindingReceive.mockClear(); @@ -56,7 +101,13 @@ describe("http/server", () => { }); afterAll(async () => { - await server.stop(); + await runWithCleanupErrorSuppression(async () => { + await server.stop(); + await daprContainer.stop(); + await mqttContainer.stop(); + await redisContainer.stop(); + await network.stop(); + }); }); describe("server", () => { @@ -73,8 +124,8 @@ describe("http/server", () => { communicationProtocol: CommunicationProtocolEnum.HTTP, serverHttp: myApp, clientOptions: { - daprHost, - daprPort, + daprHost: daprContainer.getHost(), + daprPort: daprContainer.getHttpPort().toString(), }, }); diff --git a/test/e2e/workflow/workflow.test.ts b/test/e2e/workflow/workflow.test.ts index 7bf5383d..4b1d41f0 100644 --- a/test/e2e/workflow/workflow.test.ts +++ b/test/e2e/workflow/workflow.test.ts @@ -11,6 +11,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { Network, StartedNetwork, StartedTestContainer } from "testcontainers"; +import { DaprContainer, StartedDaprContainer } from "@dapr/testcontainer-node"; import DaprWorkflowClient from "../../../src/workflow/client/DaprWorkflowClient"; import WorkflowContext from "../../../src/workflow/runtime/WorkflowContext"; import WorkflowRuntime from "../../../src/workflow/runtime/WorkflowRuntime"; @@ -19,23 +21,47 @@ import { getFunctionName } from "../../../src/workflow/internal"; import { WorkflowRuntimeStatus } from "../../../src/workflow/runtime/WorkflowRuntimeStatus"; import WorkflowActivityContext from "../../../src/workflow/runtime/WorkflowActivityContext"; import { Task } from "../../../src/workflow/internal/durabletask/task/task"; - -const clientHost = "localhost"; -const clientPort = "4001"; - -describe("Workflow", () => { +import { + startRedisContainer, + buildStateRedisComponent, + DAPR_TEST_RUNTIME_IMAGE, + DAPR_TEST_PLACEMENT_IMAGE, + DAPR_TEST_SCHEDULER_IMAGE, + runWithCleanupErrorSuppression, +} from "../helpers/containers"; + +describe("workflow", () => { + let network: StartedNetwork; + let redisContainer: StartedTestContainer; + let daprContainer: StartedDaprContainer; let workflowClient: DaprWorkflowClient; let workflowRuntime: WorkflowRuntime; + beforeAll(async () => { + network = await new Network().start(); + redisContainer = await startRedisContainer(network); + + // Workflows require an actor state store (actorStateStore: true) and use the + // placement / scheduler services that DaprContainer starts automatically. + daprContainer = await new DaprContainer(DAPR_TEST_RUNTIME_IMAGE) + .withPlacementImage(DAPR_TEST_PLACEMENT_IMAGE) + .withSchedulerImage(DAPR_TEST_SCHEDULER_IMAGE) + .withNetwork(network) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(buildStateRedisComponent(true /* actorStateStore */)) + .start(); + }, 180 * 1000); + beforeEach(async () => { - // Start a worker, which will connect to the sidecar in a background thread + // Each test registers different workflows/activities so we create fresh + // client and runtime instances that connect to the shared Dapr container. workflowClient = new DaprWorkflowClient({ - daprHost: clientHost, - daprPort: clientPort, + daprHost: daprContainer.getHost(), + daprPort: daprContainer.getGrpcPort().toString(), }); workflowRuntime = new WorkflowRuntime({ - daprHost: clientHost, - daprPort: clientPort, + daprHost: daprContainer.getHost(), + daprPort: daprContainer.getGrpcPort().toString(), }); }); @@ -44,6 +70,14 @@ describe("Workflow", () => { await workflowClient.stop(); }); + afterAll(async () => { + await runWithCleanupErrorSuppression(async () => { + await daprContainer.stop(); + await redisContainer.stop(); + await network.stop(); + }); + }); + it("should be able to run an empty orchestration", async () => { let invoked = false; const emptyWorkflow: TWorkflow = async (_: WorkflowContext, __: any) => { @@ -285,6 +319,40 @@ describe("Workflow", () => { } }, 31000); + it("should be able to suspend and resume an orchestration", async () => { + const workflow: TWorkflow = async function* (ctx: WorkflowContext, _: any): any { + const res = yield ctx.waitForExternalEvent("my_event"); + return res; + }; + + workflowRuntime.registerWorkflow(workflow); + await workflowRuntime.start(); + + const id = await workflowClient.scheduleNewWorkflow(workflow); + let state = await workflowClient.waitForWorkflowStart(id, undefined, 30); + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(WorkflowRuntimeStatus.RUNNING); + + // Suspend the workflow and confirm it enters the SUSPENDED state. + await workflowClient.suspendWorkflow(id); + state = await workflowClient.getWorkflowState(id, false); + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(WorkflowRuntimeStatus.SUSPENDED); + + // Resume the workflow and confirm it returns to the RUNNING state. + await workflowClient.resumeWorkflow(id); + state = await workflowClient.getWorkflowState(id, false); + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(WorkflowRuntimeStatus.RUNNING); + + // Unblock the workflow by raising the awaited event. + await workflowClient.raiseEvent(id, "my_event", "hello"); + state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30); + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(WorkflowRuntimeStatus.COMPLETED); + expect(state?.serializedOutput).toEqual(JSON.stringify("hello")); + }, 31000); + it("should be able to terminate an orchestration", async () => { const workflow: TWorkflow = async function* (ctx: WorkflowContext, _: any): any { const res = yield ctx.waitForExternalEvent("my_event");