diff --git a/README.md b/README.md index f237cca..015dd76 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,25 @@ npm test npm run test:watch ``` +#### Integration tests + +`npm run test:integration` needs an S3-compatible server. By default it starts a Minio +[testcontainer](https://testcontainers.com/) automatically. + The image is pinned to the release deployed in our Azure environment; bump it in +`tests/integration/helpers/minioContainer.ts` when that environment moves. + +To run against an already-running Minio instead, set `TEST_MINIO_ENDPOINT`: + +| Variable | Default | Purpose | +| ------------------------ | ------------ | ------------------------------------------------------------------ | +| `TEST_MINIO_ENDPOINT` | _(unset)_ | Point the suite at an existing Minio. Unset means start a container. | +| `TEST_MINIO_ACCESS_KEY` | `minioadmin` | Access key for that server. | +| `TEST_MINIO_SECRET_KEY` | `minioadmin` | Secret key for that server. | + +> **The suite creates and deletes buckets on whichever endpoint it is given.** Never point +> `TEST_MINIO_ENDPOINT` at a shared or deployed environment, and beware of leaving it exported in a +> shell profile. Each run prints which mode it selected and against which endpoint. + ## Customizing the Boilerplate This boilerplate includes example "logistics" code to demonstrate task handling. Follow these steps to adapt it to your use case: diff --git a/eslint.config.mjs b/eslint.config.mjs index c7cea9d..100e66d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,4 +1,4 @@ import tsBaseConfig from '@map-colonies/eslint-config/ts-base'; import { config } from '@map-colonies/eslint-config/helpers'; -export default config(tsBaseConfig, { ignores: ['vitest.config.mts'] }); +export default config(tsBaseConfig, { ignores: ['vitest.config.mts', 'vitest.integration.config.mts'] }); diff --git a/helm/values.yaml b/helm/values.yaml index 376c021..70f01e9 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -35,6 +35,7 @@ mclabels: component: backend partOf: core owner: raster + gisDomain: raster prometheus: enabled: true diff --git a/package-lock.json b/package-lock.json index d8ded2d..d97d035 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +14,8 @@ "@map-colonies/config": "^4.0.1", "@map-colonies/jobnik-sdk": "^0.1.0", "@map-colonies/js-logger": "^5.0.0", - "@map-colonies/mc-priority-queue": "^9.1.0", - "@map-colonies/mc-utils": "^5.1.0", + "@map-colonies/mc-priority-queue": "^9.1.2", + "@map-colonies/mc-utils": "^6.0.1", "@map-colonies/raster-shared": "^9.0.0-alpha.0", "@map-colonies/read-pkg": "^1.0.0", "@map-colonies/schemas": "^1.20.0", @@ -47,6 +47,7 @@ "prettier": "^3.5.3", "pretty-quick": "^4.1.1", "rimraf": "^6.0.1", + "testcontainers": "^11.14.0", "tsc-alias": "^1.8.11", "type-fest": "^5.1.0", "typescript": "^5.8.2", @@ -1030,6 +1031,13 @@ "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": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", @@ -2263,6 +2271,16 @@ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "license": "MIT" }, + "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/@map-colonies/commitlint-config": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@map-colonies/commitlint-config/-/commitlint-config-1.1.1.tgz", @@ -2499,9 +2517,9 @@ } }, "node_modules/@map-colonies/mc-priority-queue": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@map-colonies/mc-priority-queue/-/mc-priority-queue-9.1.0.tgz", - "integrity": "sha512-e7SpeIGQ7O7qIgazMbgubeaWL91i4IC+l5YfbogHkyfvgCluKZEdZAXYtIWXSal44g31XEGp89LoyY+UfpIcSQ==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@map-colonies/mc-priority-queue/-/mc-priority-queue-9.1.2.tgz", + "integrity": "sha512-5peCKqA6qscAYWIlA2sWjUMcF7uR1H8oiN4mi4mIF3qwyKJg9zysGgfxyNkgWG9Of7rIaK5EEN1PsvXyNI3sXw==", "license": "ISC", "dependencies": { "@map-colonies/types": "^1.9.0" @@ -2510,13 +2528,13 @@ "node": ">=24.0.0" }, "peerDependencies": { - "@map-colonies/mc-utils": "^5.1.0" + "@map-colonies/mc-utils": "^6.0.1" } }, "node_modules/@map-colonies/mc-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@map-colonies/mc-utils/-/mc-utils-5.1.0.tgz", - "integrity": "sha512-4eL4ykgJFrFSPP18AbeJUJklwmzJieormWIvEc8c8PFRXU5eN1lRUal60xAhghpmzEQnudRG1kV3NWIibUjw2w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@map-colonies/mc-utils/-/mc-utils-6.0.1.tgz", + "integrity": "sha512-aHPeigSA/rl88LzXiah9CVd2gzVTXV6zlk2rt34rjwqgLpBa3vBadwOgBun/w9/1jOXrS/hGz9FhMKmQL7qYGQ==", "license": "ISC", "dependencies": { "@map-colonies/types": "^1.9.0", @@ -7115,6 +7133,29 @@ "dev": true, "license": "MIT" }, + "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/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -7286,6 +7327,43 @@ "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", "license": "MIT" }, + "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/ssh2/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/@types/tedious": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", @@ -7993,6 +8071,19 @@ "url": "https://opencollective.com/vitest" } }, + "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", @@ -8140,6 +8231,170 @@ "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.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "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/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/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/archiver-utils/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/archiver-utils/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/archiver-utils/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/archiver/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/archiver/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/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -8169,6 +8424,16 @@ "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/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -8198,6 +8463,20 @@ "dev": true, "license": "MIT" }, + "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", @@ -8236,12 +8515,143 @@ "axios": "0.x || 1.x" } }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.0.tgz", + "integrity": "sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==", + "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.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "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.5.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.2.tgz", + "integrity": "sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==", + "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/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -8270,10 +8680,72 @@ "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", "license": "MIT" }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "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/bl/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/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -8337,12 +8809,67 @@ "node": ">=8" } }, + "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==", "license": "MIT" }, + "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", @@ -8497,6 +9024,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/cjs-module-lexer": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", @@ -8590,6 +9124,50 @@ "dot-prop": "^5.1.0" } }, + "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/compress-commons/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/compress-commons/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/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -8869,6 +9447,75 @@ "typescript": ">=5" } }, + "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/crc32-stream/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/crc32-stream/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/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -9100,6 +9747,168 @@ "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/docker-modem/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/dockerode": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.12.tgz", + "integrity": "sha512-/bCZd6KlGcjZO8Buqmi/vXuqEGVEZ0PNjx/biBNqJD3MhK9DmdiAuKxqfNhflgDESDIiBz3qF+0e55+CpnrUcw==", + "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/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "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/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/dockerode/node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "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/dockerode/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -9778,6 +10587,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/expect-type": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", @@ -9883,6 +10722,13 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "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.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -10173,6 +11019,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", @@ -10321,6 +11174,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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", @@ -10457,6 +11323,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -10601,6 +11474,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.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -11108,6 +12002,59 @@ "json-buffer": "3.0.1" } }, + "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/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/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", @@ -11439,6 +12386,13 @@ "node": ">=10" } }, + "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/module-details-from-path": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", @@ -11485,6 +12439,14 @@ "url": "https://github.com/sponsors/raouldeheer" } }, + "node_modules/nan": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -12568,6 +13530,16 @@ "dev": true, "license": "MIT" }, + "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", @@ -12603,6 +13575,59 @@ "node": "^16 || ^18 || >=20" } }, + "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/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "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/properties-reader/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/protobufjs": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", @@ -12771,6 +13796,39 @@ "string_decoder": "~0.10.x" } }, + "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.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "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", @@ -12891,6 +13949,16 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "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.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -13389,6 +14457,13 @@ "node": ">=18.20 || >=20" } }, + "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/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -13398,6 +14473,46 @@ "node": ">= 10.x" } }, + "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/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -13448,6 +14563,18 @@ "npm": ">=6" } }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "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": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", @@ -13592,6 +14719,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "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.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "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/tdigest": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", @@ -13601,6 +14756,16 @@ "bintrees": "1.0.2" } }, + "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/test-exclude": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", @@ -13687,6 +14852,40 @@ "url": "https://github.com/sponsors/isaacs" } }, + "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-extensions": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", @@ -13866,6 +15065,16 @@ "node": ">=14.0.0" } }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -14016,6 +15225,13 @@ "integrity": "sha512-Ja03QIJlPuHt4IQ2FfGex4F4JAr8m3jpaHbFbQrgwr7s7L6U8ocrHiF3J1+wf9jzhGKxvDeaCAnGDot8OjGFyA==", "license": "(EDL-1.0 OR EPL-1.0)" }, + "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", @@ -14096,9 +15312,9 @@ } }, "node_modules/undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -14625,6 +15841,48 @@ "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" + } + }, + "node_modules/zip-stream/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/zip-stream/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/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index ac6cbfd..61e1672 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,11 @@ "description": "This is template for map colonies jobnik worker service", "main": "./src/index.ts", "scripts": { - "test": "vitest run", + "test": "npm run test:unit && npm run test:integration", + "test:unit": "vitest run", "test:watch": "vitest watch", "test:ui": "vitest --ui", + "test:integration": "vitest run --config vitest.integration.config.mts", "format": "prettier --check .", "format:fix": "prettier --write .", "prelint:fix": "npm run format:fix", @@ -35,8 +37,8 @@ "@map-colonies/config": "^4.0.1", "@map-colonies/jobnik-sdk": "^0.1.0", "@map-colonies/js-logger": "^5.0.0", - "@map-colonies/mc-priority-queue": "^9.1.0", - "@map-colonies/mc-utils": "^5.1.0", + "@map-colonies/mc-priority-queue": "^9.1.2", + "@map-colonies/mc-utils": "^6.0.1", "@map-colonies/raster-shared": "^9.0.0-alpha.0", "@map-colonies/read-pkg": "^1.0.0", "@map-colonies/schemas": "^1.20.0", @@ -68,6 +70,7 @@ "prettier": "^3.5.3", "pretty-quick": "^4.1.1", "rimraf": "^6.0.1", + "testcontainers": "^11.14.0", "tsc-alias": "^1.8.11", "type-fest": "^5.1.0", "typescript": "^5.8.2", diff --git a/tests/helpers/fakes/index.ts b/tests/helpers/fakes/index.ts index cc5b372..adb0c82 100644 --- a/tests/helpers/fakes/index.ts +++ b/tests/helpers/fakes/index.ts @@ -1,2 +1,3 @@ export * from './taskFakes'; export * from './pairFakes'; +export * from './tilesDeletionFakes'; diff --git a/tests/helpers/fakes/tilesDeletionFakes.ts b/tests/helpers/fakes/tilesDeletionFakes.ts new file mode 100644 index 0000000..a290e1b --- /dev/null +++ b/tests/helpers/fakes/tilesDeletionFakes.ts @@ -0,0 +1,53 @@ +import { faker } from '@faker-js/faker'; +import { StorageProvider, type FsTilesDeletionParams, type S3TilesDeletionParams, type TileRange } from '@map-colonies/raster-shared'; + +const EXTENSIONS = ['png', 'jpeg'] as const; + +/** Fields every path-addressed tiles-deletion member carries, whatever its storage locator. */ +type TilesDeletionCommon = Pick; +const MIN_FAKE_ZOOM = 10; +const MAX_FAKE_ZOOM = 18; + +function buildTileRange(overrides: Partial = {}): TileRange { + const minX = faker.number.int({ min: 0, max: 50 }); + const minY = faker.number.int({ min: 0, max: 50 }); + return { + zoom: faker.number.int({ min: MIN_FAKE_ZOOM, max: MAX_FAKE_ZOOM }), + minX, + maxX: minX + faker.number.int({ min: 0, max: 5 }), + minY, + maxY: minY + faker.number.int({ min: 0, max: 5 }), + ...overrides, + }; +} + +function buildTilesRelativePath(): string { + return `${faker.word.noun().toLowerCase()}/${faker.string.alphanumeric({ length: 6, casing: 'lower' })}`; +} + +function buildTilesDeletionCommon(overrides: Partial): TilesDeletionCommon { + return { + tilesRelativePath: overrides.tilesRelativePath ?? buildTilesRelativePath(), + fileExtension: overrides.fileExtension ?? faker.helpers.arrayElement(EXTENSIONS), + ranges: overrides.ranges ?? [buildTileRange()], + }; +} + +function buildS3TilesDeletionParams(overrides: Partial = {}): S3TilesDeletionParams { + return { + storageProvider: StorageProvider.S3, + bucket: overrides.bucket ?? `test-${faker.string.alphanumeric({ length: 16, casing: 'lower' })}`, + ...buildTilesDeletionCommon(overrides), + }; +} + +function buildFsTilesDeletionParams(overrides: Partial = {}): FsTilesDeletionParams { + return { + storageProvider: StorageProvider.FS, + subPath: overrides.subPath ?? buildTilesRelativePath(), + ...buildTilesDeletionCommon(overrides), + }; +} + +export { buildTileRange, buildTilesRelativePath, buildS3TilesDeletionParams, buildFsTilesDeletionParams }; +export type { TilesDeletionCommon }; diff --git a/tests/integration/helpers/backendFixtures.ts b/tests/integration/helpers/backendFixtures.ts new file mode 100644 index 0000000..1206b35 --- /dev/null +++ b/tests/integration/helpers/backendFixtures.ts @@ -0,0 +1,143 @@ +import { join } from 'node:path'; +import { faker } from '@faker-js/faker'; +import { container } from 'tsyringe'; +import { StorageProvider, type FsTilesDeletionParams, type S3TilesDeletionParams } from '@map-colonies/raster-shared'; +import type { S3Client } from '@aws-sdk/client-s3'; +import { S3StorageProvider, FsStorageProvider, type FsStorageConfig, type StorageProviders } from '@src/cleaner/storageProviders'; +import { buildFsTilesDeletionParams, buildS3TilesDeletionParams, type TilesDeletionCommon } from '../../helpers/fakes/tilesDeletionFakes'; +import { createMockLogger } from '../../helpers/mocks'; +import { startMinio, type MinioHandle } from './minioContainer'; +import { buildS3StorageConfigForMinio, createTestS3Client, deleteBucket, ensureBucket, listAllKeys, putManyTiles } from './s3TestKit'; +import { listAllFiles, makeTempFsBase, rmBase, writeManyTiles } from './fsTestKit'; + +/** The storage providers a tiles-deletion task can actually be routed to today (REDIS is not implemented). */ +type PathAddressedProvider = Exclude; +type PathAddressedParams = S3TilesDeletionParams | FsTilesDeletionParams; + +/** + * The only sub path FS deletion is allowed under, mirroring `storage.fs.subPaths` in config. + * The task's own `subPath` must live inside it or `FsStorageProvider` rejects the paths outright. + */ +const FS_ALLOWED_SUB_PATH = 'artifacts/tiles'; +const FS_DELETE_BATCH_SIZE = 100; + +interface BackendHandles { + minio: MinioHandle; + s3Client: S3Client; +} + +interface TestStorageContext { + providers: StorageProviders; + /** Bucket the S3 task targets — travels in the task params, not in config. */ + bucket: string; + /** Mounted root the FS provider is configured with; never referenced by the task. */ + fsBasePath: string; + /** Sub path of `fsBasePath` the FS task targets — travels in the task params. */ + fsSubPath: string; +} + +async function startBackends(): Promise { + const minio = await startMinio(); + const s3Client = createTestS3Client(minio); + return { minio, s3Client }; +} + +async function stopBackends({ minio, s3Client }: BackendHandles): Promise { + s3Client.destroy(); + await minio.stop(); +} + +/** Per-provider delete batch sizes; each falls back to the production-shaped default. */ +interface ProviderBatchSizes { + /** Keys per `DeleteObjects` call the S3 provider chunks its input into. */ + s3?: number; + /** + * Only reaches `FsStorageProvider.deleteResources` — `delete` fans every path out at once + * instead of chunking, so tiles deletion is unaffected by it. + */ + fs?: number; +} + +function buildProviders(minio: MinioHandle, fsBasePath: string, batchSizes: ProviderBatchSizes = {}): StorageProviders { + const fsStorageConfig: FsStorageConfig = { + basePath: fsBasePath, + subPaths: [FS_ALLOWED_SUB_PATH], + batchSize: batchSizes.fs ?? FS_DELETE_BATCH_SIZE, + }; + const s3StorageConfig = buildS3StorageConfigForMinio(minio); + + return { + [StorageProvider.S3]: new S3StorageProvider({ ...s3StorageConfig, batchSize: batchSizes.s3 ?? s3StorageConfig.batchSize }, createMockLogger()), + [StorageProvider.FS]: new FsStorageProvider(fsStorageConfig, createMockLogger()), + }; +} + +async function setupTestStorageContext({ minio, s3Client }: BackendHandles): Promise { + const bucket = `test-${faker.string.alphanumeric({ length: 16, casing: 'lower' })}`; + await ensureBucket(s3Client, bucket); + const fsBasePath = await makeTempFsBase(); + + return { providers: buildProviders(minio, fsBasePath), bucket, fsBasePath, fsSubPath: FS_ALLOWED_SUB_PATH }; +} + +/** + * Rebuilds the current context's providers with different delete batch sizes, so a test can + * cross a provider's own chunking boundary without seeding thousands of tiles. The returned + * providers point at the same bucket and base path, so seeding stays unchanged. + */ +function providersWithBatchSizes(handles: BackendHandles, storageContext: TestStorageContext, batchSizes: ProviderBatchSizes): StorageProviders { + return buildProviders(handles.minio, storageContext.fsBasePath, batchSizes); +} + +async function teardownTestStorageContext(handles: BackendHandles, storageContext: TestStorageContext): Promise { + try { + await Promise.all([deleteBucket(handles.s3Client, storageContext.bucket), rmBase(storageContext.fsBasePath)]); + } finally { + container.reset(); + } +} + +interface ProviderBackend { + storageProvider: PathAddressedProvider; + /** + * Task params carrying this backend's own storage locator — bucket for S3, sub path for FS. + * `overrides` pins the otherwise-faked tile fields, e.g. explicit `ranges`. + */ + buildParams: (tilesRelativePath: string, overrides?: Partial) => PathAddressedParams; + /** Seeds tiles at paths relative to the storage target. */ + seed: (paths: string[]) => Promise; + /** Lists surviving tiles as paths relative to the storage target. */ + list: (prefix: string) => Promise; +} + +function s3Backend(handles: () => BackendHandles, perTest: () => TestStorageContext): ProviderBackend { + return { + storageProvider: StorageProvider.S3, + buildParams: (tilesRelativePath, overrides) => buildS3TilesDeletionParams({ ...overrides, bucket: perTest().bucket, tilesRelativePath }), + seed: async (paths) => putManyTiles(handles().s3Client, perTest().bucket, paths), + list: async (prefix) => listAllKeys(handles().s3Client, perTest().bucket, prefix), + }; +} + +function fsBackend(perTest: () => TestStorageContext): ProviderBackend { + // The provider joins base path + sub path itself, so the test seeds and reads the same root. + const targetRoot = (): string => join(perTest().fsBasePath, perTest().fsSubPath); + return { + storageProvider: StorageProvider.FS, + buildParams: (tilesRelativePath, overrides) => buildFsTilesDeletionParams({ ...overrides, subPath: perTest().fsSubPath, tilesRelativePath }), + seed: async (paths) => writeManyTiles(targetRoot(), paths), + list: async (prefix) => (await listAllFiles(targetRoot())).filter((path) => path.startsWith(prefix)), + }; +} + +export { + FS_ALLOWED_SUB_PATH, + startBackends, + stopBackends, + setupTestStorageContext, + teardownTestStorageContext, + providersWithBatchSizes, + s3Backend, + fsBackend, +}; +export type { BackendHandles, TestStorageContext, ProviderBackend, PathAddressedParams, ProviderBatchSizes }; diff --git a/tests/integration/helpers/fsTestKit.ts b/tests/integration/helpers/fsTestKit.ts new file mode 100644 index 0000000..b4af196 --- /dev/null +++ b/tests/integration/helpers/fsTestKit.ts @@ -0,0 +1,57 @@ +import { mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises'; +import { join, relative, sep, dirname } from 'node:path'; +import { tmpdir } from 'node:os'; +import { TINY_TILE_BODY } from './tileFixtures'; + +async function makeTempFsBase(): Promise { + return mkdtemp(join(tmpdir(), 'test-dir')); +} + +async function writeTile(basePath: string, relativePath: string): Promise { + const absolute = join(basePath, relativePath); + await mkdir(dirname(absolute), { recursive: true }); + await writeFile(absolute, TINY_TILE_BODY); +} + +async function writeManyTiles(basePath: string, relativePaths: string[]): Promise { + const concurrency = 32; + for (let i = 0; i < relativePaths.length; i += concurrency) { + await Promise.all(relativePaths.slice(i, i + concurrency).map(async (path) => writeTile(basePath, path))); + } +} + +function toForwardSlashes(value: string): string { + return sep === '/' ? value : value.split(sep).join('/'); +} + +async function listAllFiles(basePath: string): Promise { + const entries = await readdir(basePath, { recursive: true, withFileTypes: true }); + const files: string[] = []; + for (const entry of entries) { + if (entry.isFile()) { + const parentPath = (entry as unknown as { parentPath?: string; path?: string }).parentPath ?? (entry as unknown as { path: string }).path; + const abs = join(parentPath, entry.name); + files.push(toForwardSlashes(relative(basePath, abs))); + } + } + return files.sort(); +} + +async function listAllDirs(basePath: string): Promise { + const entries = await readdir(basePath, { recursive: true, withFileTypes: true }); + const dirs: string[] = []; + for (const entry of entries) { + if (entry.isDirectory()) { + const parentPath = (entry as unknown as { parentPath?: string; path?: string }).parentPath ?? (entry as unknown as { path: string }).path; + const abs = join(parentPath, entry.name); + dirs.push(toForwardSlashes(relative(basePath, abs))); + } + } + return dirs.sort(); +} + +async function rmBase(basePath: string): Promise { + await rm(basePath, { recursive: true, force: true }); +} + +export { makeTempFsBase, writeTile, writeManyTiles, listAllFiles, listAllDirs, rmBase }; diff --git a/tests/integration/helpers/minioContainer.ts b/tests/integration/helpers/minioContainer.ts new file mode 100644 index 0000000..97772be --- /dev/null +++ b/tests/integration/helpers/minioContainer.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { GenericContainer, type StartedTestContainer } from 'testcontainers'; + +interface MinioHandle { + endpoint: string; + accessKeyId: string; + secretAccessKey: string; + stop: () => Promise; +} + +/** + * Pinned to the release running on our Azure deployment, so the suite exercises the same server + * behavior we deploy against. + */ +const MINIO_IMAGE = 'minio/minio:RELEASE.2025-07-23T15-54-02Z'; +const MINIO_PORT = 9000; +const DEFAULT_USER = 'minioadmin'; +const DEFAULT_PASSWORD = 'minioadmin'; + +async function startMinio(): Promise { + const externalEndpoint = process.env.TEST_MINIO_ENDPOINT; + if (externalEndpoint !== undefined && externalEndpoint !== '') { + console.warn(`Minio: using EXTERNAL server at ${externalEndpoint} (TEST_MINIO_ENDPOINT is set).`); + console.warn('Minio: this suite CREATES AND DELETES buckets on that server. Unset TEST_MINIO_ENDPOINT to use a testcontainer.'); + return { + endpoint: externalEndpoint, + accessKeyId: process.env.TEST_MINIO_ACCESS_KEY ?? DEFAULT_USER, + secretAccessKey: process.env.TEST_MINIO_SECRET_KEY ?? DEFAULT_PASSWORD, + stop: async (): Promise => Promise.resolve(), + }; + } + console.log(`Minio: TEST_MINIO_ENDPOINT is not set, starting testcontainer from ${MINIO_IMAGE}`); + const container: StartedTestContainer = await new GenericContainer(MINIO_IMAGE) + .withCommand(['server', '/data']) + .withEnvironment({ + MINIO_ROOT_USER: DEFAULT_USER, + MINIO_ROOT_PASSWORD: DEFAULT_PASSWORD, + }) + .withExposedPorts(MINIO_PORT) + .start(); + + return { + endpoint: `http://${container.getHost()}:${container.getMappedPort(MINIO_PORT)}`, + accessKeyId: DEFAULT_USER, + secretAccessKey: DEFAULT_PASSWORD, + stop: async (): Promise => { + try { + await container.stop(); + } catch (error) { + // Non-fatal: Ryuk reaps the container on test process exit, unless it is disabled. + console.warn('Failed to stop Minio container; relying on Ryuk to reap it', error); + } + }, + }; +} + +export { startMinio, type MinioHandle }; diff --git a/tests/integration/helpers/s3TestKit.ts b/tests/integration/helpers/s3TestKit.ts new file mode 100644 index 0000000..2fb72fe --- /dev/null +++ b/tests/integration/helpers/s3TestKit.ts @@ -0,0 +1,106 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { + S3Client, + CreateBucketCommand, + DeleteBucketCommand, + DeleteObjectsCommand, + ListObjectsV2Command, + PutObjectCommand, + BucketAlreadyOwnedByYou, + BucketAlreadyExists, +} from '@aws-sdk/client-s3'; +import type { S3StorageConfig } from '@src/cleaner/storageProviders'; +import type { MinioHandle } from './minioContainer'; +import { TINY_TILE_BODY } from './tileFixtures'; + +// S3/MinIO reject a DeleteObjects request carrying more than 1000 keys. Only bucket +// teardown below batches against it — the provider caps itself via its own config. +const S3_DELETE_OBJECTS_MAX_KEYS = 1000; +const TEST_REGION = 'us-east-1'; + +function createTestS3Client(handle: MinioHandle): S3Client { + return new S3Client({ + endpoint: handle.endpoint, + credentials: { + accessKeyId: handle.accessKeyId, + secretAccessKey: handle.secretAccessKey, + }, + region: TEST_REGION, + forcePathStyle: true, + tls: false, + }); +} + +/** + * The already-validated shape `S3StorageProvider` is injected with in production — + * built here straight from the container handle, so the test skips `buildS3StorageConfig` + * (and with it the `storage.s3` config lookup) but exercises the real provider. + */ +function buildS3StorageConfigForMinio(handle: MinioHandle): S3StorageConfig { + return { + endpoint: handle.endpoint, + accessKeyId: handle.accessKeyId, + secretAccessKey: handle.secretAccessKey, + sslEnabled: false, + forcePathStyle: true, + region: TEST_REGION, + batchSize: S3_DELETE_OBJECTS_MAX_KEYS, + }; +} + +async function ensureBucket(client: S3Client, bucket: string): Promise { + try { + await client.send(new CreateBucketCommand({ Bucket: bucket })); + } catch (err) { + if (err instanceof BucketAlreadyOwnedByYou || err instanceof BucketAlreadyExists) { + return; + } + throw err; + } +} + +async function listAllKeys(client: S3Client, bucket: string, prefix?: string): Promise { + const keys: string[] = []; + let continuationToken: string | undefined; + do { + const response = await client.send(new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix, ContinuationToken: continuationToken })); + for (const obj of response.Contents ?? []) { + if (obj.Key !== undefined) { + keys.push(obj.Key); + } + } + continuationToken = response.IsTruncated === true ? response.NextContinuationToken : undefined; + } while (continuationToken !== undefined); + return keys.sort(); +} + +async function emptyBucket(client: S3Client, bucket: string): Promise { + const keys = await listAllKeys(client, bucket); + for (let i = 0; i < keys.length; i += S3_DELETE_OBJECTS_MAX_KEYS) { + const chunk = keys.slice(i, i + S3_DELETE_OBJECTS_MAX_KEYS); + await client.send( + new DeleteObjectsCommand({ + Bucket: bucket, + Delete: { Objects: chunk.map((Key) => ({ Key })) }, + }) + ); + } +} + +async function deleteBucket(client: S3Client, bucket: string): Promise { + await emptyBucket(client, bucket); + await client.send(new DeleteBucketCommand({ Bucket: bucket })); +} + +async function putTile(client: S3Client, bucket: string, key: string): Promise { + await client.send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: TINY_TILE_BODY })); +} + +async function putManyTiles(client: S3Client, bucket: string, keys: string[]): Promise { + const concurrency = 20; + for (let i = 0; i < keys.length; i += concurrency) { + await Promise.all(keys.slice(i, i + concurrency).map(async (key) => putTile(client, bucket, key))); + } +} + +export { createTestS3Client, buildS3StorageConfigForMinio, ensureBucket, deleteBucket, emptyBucket, putTile, putManyTiles, listAllKeys }; diff --git a/tests/integration/helpers/testPoller.ts b/tests/integration/helpers/testPoller.ts new file mode 100644 index 0000000..c0cd0f0 --- /dev/null +++ b/tests/integration/helpers/testPoller.ts @@ -0,0 +1,103 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import { vi } from 'vitest'; +import { container } from 'tsyringe'; +import type { ITaskResponse, TaskHandler as QueueClient } from '@map-colonies/mc-priority-queue'; +import { SERVICES } from '@src/common/constants'; +import { getJobAndTaskToken } from '@src/common/dependencyRegistration'; +import { TaskPoller } from '@src/worker/taskPoller'; +import { ErrorHandler } from '@src/cleaner/errors'; +import { StrategyFactory, TilesDeletionStrategy } from '@src/cleaner/strategies'; +import type { StorageProviders } from '@src/cleaner/storageProviders'; +import type { JobTrackerClient } from '@src/cleaner/httpClients'; +import type { PollingPairConfig } from '@src/cleaner/types'; +import { createMockLogger, createMockQueueClient, createMockStrategyConfig, createMockJobTrackerClient } from '../../helpers/mocks'; + +const TASK_TYPE = 'tiles-deletion'; +const JOB_TYPE = 'Ingestion_Update'; +const POLLING_PAIR: PollingPairConfig = { jobType: JOB_TYPE, taskType: TASK_TYPE, maxAttempts: 3 }; +const POLLER_WATCHDOG_MS = 30_000; + +/** + * Returns a queue client whose `dequeue` hands out `task` exactly once for the + * matching pair, then null forever. `ack` / `reject` invoke `onTerminal()` so + * the caller can stop the poller and assert. + */ +function buildSingleShotQueue(task: ITaskResponse, onTerminal: () => void): QueueClient { + const queue = createMockQueueClient(); + let handedOut = false; + + vi.mocked(queue.dequeue).mockImplementation(((jobType: string, taskType: string) => { + if (handedOut || jobType !== JOB_TYPE || taskType !== TASK_TYPE) { + return null; + } + handedOut = true; + return task; + }) as unknown as QueueClient['dequeue']); + + vi.mocked(queue.ack).mockImplementation((() => { + onTerminal(); + }) as unknown as QueueClient['ack']); + + vi.mocked(queue.reject).mockImplementation((() => { + onTerminal(); + }) as unknown as QueueClient['reject']); + + return queue; +} + +interface TestPollerParams { + providers: StorageProviders; + task: ITaskResponse; + /** Config keys merged over the strategy defaults, e.g. the batching knobs. */ + configOverrides?: Record; +} + +interface TestPoller { + queueClient: QueueClient; + jobTrackerClient: JobTrackerClient; + runSingleTask: () => Promise; +} + +/** + * Wires the real TaskPoller → StrategyFactory → TilesDeletionStrategy pipeline, + * with real S3/FS providers (supplied by the caller) and a single-shot fake + * QueueClient. The poller terminates as soon as ack/reject fires. + * + * The strategy reads nothing but batching knobs from config — every storage locator + * travels in the task params — so the mock config carries no bucket or base path. + */ +function buildPoller({ providers, task, configOverrides = {} }: TestPollerParams): TestPoller { + const config = createMockStrategyConfig({ 'queue.dequeueIntervalMs': 0, ...configOverrides }); + + container.register(SERVICES.LOGGER, { useValue: createMockLogger() }); + container.register(SERVICES.CONFIG, { useValue: config }); + container.register(SERVICES.STORAGE_PROVIDERS, { useValue: providers }); + + const queueClient = buildSingleShotQueue(task, () => { + void poller.stop(); + }); + container.register(SERVICES.QUEUE_CLIENT, { useValue: queueClient }); + // StrategyFactory resolves strategies by the combined job+task token, not the task type alone. + container.register(getJobAndTaskToken(POLLING_PAIR), { useClass: TilesDeletionStrategy }); + + const jobTrackerClient = createMockJobTrackerClient(); + container.register(SERVICES.JOB_TRACKER_CLIENT, { useValue: jobTrackerClient }); + + const strategyFactory = container.resolve(StrategyFactory); + const errorHandler = container.resolve(ErrorHandler); + const poller = new TaskPoller(createMockLogger(), config, queueClient, strategyFactory, errorHandler, [POLLING_PAIR], jobTrackerClient); + + const runSingleTask = async (): Promise => { + const startPromise = poller.start(); + // Watchdog so a regression doesn't hang the whole suite. + const watchdog = new Promise((_, reject) => + setTimeout(() => reject(new Error('Poller did not terminate after task lifecycle')), POLLER_WATCHDOG_MS).unref() + ); + await Promise.race([startPromise, watchdog]); + }; + + return { queueClient, jobTrackerClient, runSingleTask }; +} + +export { TASK_TYPE, JOB_TYPE, POLLING_PAIR, buildSingleShotQueue, buildPoller }; +export type { TestPoller, TestPollerParams }; diff --git a/tests/integration/helpers/tileFixtures.ts b/tests/integration/helpers/tileFixtures.ts new file mode 100644 index 0000000..105d485 --- /dev/null +++ b/tests/integration/helpers/tileFixtures.ts @@ -0,0 +1,83 @@ +import type { TileRange } from '@map-colonies/raster-shared'; +import type { TilesDeletionCommon } from '../../helpers/fakes/tilesDeletionFakes'; + +// First 4 bytes of a PNG file header (\x89 P N G). Content is arbitrary; +// eslint-disable-next-line @typescript-eslint/no-magic-numbers +const TINY_TILE_BODY = Buffer.from([0x89, 0x50, 0x4e, 0x47]); + +/** + * Every path returned here is relative to the storage target — the bucket for S3, + * the task's sub path for FS — exactly as `TilesDeletionStrategy` generates them. + */ +function tilePathsForRange(range: TileRange, tilesRelativePath: string, fileExtension: string): string[] { + const paths: string[] = []; + for (let x = range.minX; x <= range.maxX; x++) { + for (let y = range.minY; y <= range.maxY; y++) { + paths.push(`${tilesRelativePath}/${range.zoom}/${x}/${y}.${fileExtension}`); + } + } + return paths; +} + +function tilePathsForRanges(params: TilesDeletionCommon): string[] { + return params.ranges.flatMap((range) => tilePathsForRange(range, params.tilesRelativePath, params.fileExtension)); +} + +function paddedRange(range: TileRange, padding: number): TileRange { + return { + zoom: range.zoom, + minX: Math.max(0, range.minX - padding), + maxX: range.maxX + padding, + minY: Math.max(0, range.minY - padding), + maxY: range.maxY + padding, + }; +} + +/** + * Builds the set of tiles inside the `paddedRange` but **outside** the original + * `params.ranges`. These tiles are seeded into the backend before the strategy + * runs and MUST survive — they prove "only the supplied ranges are deleted". + * + * Deduped, because with several ranges the padded regions can overlap and yield + * the same bystander path twice. + */ +function extraTilePathsAroundRanges(params: TilesDeletionCommon, padding: number): string[] { + const targetSet = new Set(tilePathsForRanges(params)); + const extras = new Set(); + for (const range of params.ranges) { + const wider = paddedRange(range, padding); + for (const path of tilePathsForRange(wider, params.tilesRelativePath, params.fileExtension)) { + if (!targetSet.has(path)) { + extras.add(path); + } + } + } + return [...extras]; +} + +/** + * Returns one tile path per (range × zoomDelta) pair, placed at a zoom level + * adjacent to each range's zoom. + * + * Like `extraTilePathsAroundRanges`, the returned paths are seeded before the + * test and must survive after the strategy runs. + * */ +function extraTilePathsAtAdjacentZooms(params: TilesDeletionCommon, zoomDeltas: number[]): string[] { + const targetSet = new Set(tilePathsForRanges(params)); + const extras = new Set(); + for (const range of params.ranges) { + for (const delta of zoomDeltas) { + const zoom = range.zoom + delta; + if (zoom < 0) continue; + const path = `${params.tilesRelativePath}/${zoom}/${range.minX}/${range.minY}.${params.fileExtension}`; + // With several ranges, one range's neighbouring zoom can be another range's own zoom, + // where the path is a deletion target rather than a bystander that must survive. + if (!targetSet.has(path)) { + extras.add(path); + } + } + } + return [...extras]; +} + +export { tilePathsForRange, tilePathsForRanges, paddedRange, extraTilePathsAroundRanges, extraTilePathsAtAdjacentZooms, TINY_TILE_BODY }; diff --git a/tests/integration/helpers/tilesDeletionStrategyScenarios.ts b/tests/integration/helpers/tilesDeletionStrategyScenarios.ts new file mode 100644 index 0000000..3d8d615 --- /dev/null +++ b/tests/integration/helpers/tilesDeletionStrategyScenarios.ts @@ -0,0 +1,36 @@ +import type { TileRange } from '@map-colonies/raster-shared'; + +/** + * Sized so one run crosses every batching boundary: 180 tiles at `strategyBatchSize` 40 is + * four full batches flushed in two groups of `strategyConcurrency`, followed by a 20-tile + * trailing partial batch. Each 40-key batch then re-chunks into 25 + 15 inside the S3 + * provider's own delete loop, which a production-sized batch never reaches — its cap is 1000 + * keys, well above the strategy's batch size. + */ +const BATCHING_SCENARIO = { + range: { zoom: 12, minX: 0, maxX: 11, minY: 0, maxY: 14 } satisfies TileRange, + tileCount: 180, + strategyBatchSize: 40, + strategyConcurrency: 2, + s3ChunkSize: 25, + /** One `updateProgress` call per flush of `strategyConcurrency` full batches. */ + expectedFlushCount: 2, + /** Math.round((80 / 180) * 100) and Math.round((160 / 180) * 100) — one per flush. */ + firstFlushPercentage: 44, + secondFlushPercentage: 89, +}; + +/** + * Two ranges sharing a zoom but disjoint in X, plus one at a zoom far enough away that no + * padded or adjacent-zoom bystander tile collides with another range's targets. + */ +const MULTIPLE_RANGES_SCENARIO = { + ranges: [ + { zoom: 10, minX: 0, maxX: 2, minY: 0, maxY: 2 }, + { zoom: 10, minX: 10, maxX: 12, minY: 10, maxY: 12 }, + { zoom: 14, minX: 5, maxX: 6, minY: 5, maxY: 6 }, + ] satisfies TileRange[], + tileCount: 22, +}; + +export { BATCHING_SCENARIO, MULTIPLE_RANGES_SCENARIO }; diff --git a/tests/integration/tilesDeletionStrategy.integration.spec.ts b/tests/integration/tilesDeletionStrategy.integration.spec.ts new file mode 100644 index 0000000..8c679f5 --- /dev/null +++ b/tests/integration/tilesDeletionStrategy.integration.spec.ts @@ -0,0 +1,166 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { faker } from '@faker-js/faker'; +import { buildTask } from '../helpers/fakes/taskFakes'; +import { + fsBackend, + providersWithBatchSizes, + s3Backend, + setupTestStorageContext, + startBackends, + stopBackends, + teardownTestStorageContext, + type BackendHandles, + type PathAddressedParams, + type TestStorageContext, +} from './helpers/backendFixtures'; +import { buildPoller, TASK_TYPE } from './helpers/testPoller'; +import { extraTilePathsAroundRanges, extraTilePathsAtAdjacentZooms, tilePathsForRanges } from './helpers/tileFixtures'; +import { BATCHING_SCENARIO, MULTIPLE_RANGES_SCENARIO } from './helpers/tilesDeletionStrategyScenarios'; + +describe('tiles deletion E2E (polling → strategy → real provider → ack)', () => { + let handles: BackendHandles; + let storageContext: TestStorageContext; + + beforeAll(async () => { + handles = await startBackends(); + }); + + afterAll(async () => { + await stopBackends(handles); + }); + + beforeEach(async () => { + storageContext = await setupTestStorageContext(handles); + }); + + afterEach(async () => { + await teardownTestStorageContext(handles, storageContext); + }); + + describe.each([ + s3Backend( + () => handles, + () => storageContext + ), + fsBackend(() => storageContext), + ])('$storageProvider provider', (backend) => { + describe('happy path', () => { + it('polls the task, runs the strategy against the chosen provider, and acks after deleting only the requested tiles', async () => { + const tilesRelativePath = `${faker.string.uuid()}/${faker.string.uuid()}`; + const params: PathAddressedParams = backend.buildParams(tilesRelativePath); + const target = tilePathsForRanges(params); + const extras = [...extraTilePathsAroundRanges(params, 2), ...extraTilePathsAtAdjacentZooms(params, [-1, 1])]; + await backend.seed([...target, ...extras]); + const task = buildTask({ type: TASK_TYPE, parameters: params }); + + const { runSingleTask, queueClient, jobTrackerClient } = buildPoller({ providers: storageContext.providers, task }); + await runSingleTask(); + const remaining = await backend.list(`${tilesRelativePath}/`); + + expect(remaining.sort()).toEqual([...extras].sort()); + expect(queueClient.ack).toHaveBeenCalledWith(task.jobId, task.id); + expect(queueClient.reject).not.toHaveBeenCalled(); + expect(jobTrackerClient.notify).toHaveBeenCalledWith(task.id); + }); + + it('acks a redelivered task whose tiles are already gone, leaving unrelated tiles intact', async () => { + const tilesRelativePath = `${faker.string.uuid()}/${faker.string.uuid()}`; + const params: PathAddressedParams = backend.buildParams(tilesRelativePath); + const extras = [...extraTilePathsAroundRanges(params, 2), ...extraTilePathsAtAdjacentZooms(params, [-1, 1])]; + await backend.seed([...tilePathsForRanges(params), ...extras]); + + const firstTask = buildTask({ type: TASK_TYPE, parameters: params }); + await buildPoller({ providers: storageContext.providers, task: firstTask }).runSingleTask(); + + // The queue delivers at least once, so the same task can come back after a crash or a + // visibility timeout. Deleting an already-deleted tile must still be a success. + const redeliveredTask = buildTask({ type: TASK_TYPE, parameters: params }); + const { runSingleTask, queueClient, jobTrackerClient } = buildPoller({ providers: storageContext.providers, task: redeliveredTask }); + await runSingleTask(); + const remaining = await backend.list(`${tilesRelativePath}/`); + + expect(queueClient.ack).toHaveBeenCalledWith(redeliveredTask.jobId, redeliveredTask.id); + expect(queueClient.reject).not.toHaveBeenCalled(); + expect(jobTrackerClient.notify).toHaveBeenCalledWith(redeliveredTask.id); + expect(remaining.sort()).toEqual([...extras].sort()); + }); + }); + + describe('batching', () => { + it('flushes full batches concurrently, chunks each inside the provider, and reports progress per flush', async () => { + const tilesRelativePath = `${faker.string.uuid()}/${faker.string.uuid()}`; + const params: PathAddressedParams = backend.buildParams(tilesRelativePath, { ranges: [BATCHING_SCENARIO.range] }); + const target = tilePathsForRanges(params); + // Guards the arithmetic the progress percentages below are derived from. + expect(target).toHaveLength(BATCHING_SCENARIO.tileCount); + + const extras = extraTilePathsAroundRanges(params, 1); + await backend.seed([...target, ...extras]); + const task = buildTask({ type: TASK_TYPE, parameters: params }); + + const { runSingleTask, queueClient, jobTrackerClient } = buildPoller({ + providers: providersWithBatchSizes(handles, storageContext, { s3: BATCHING_SCENARIO.s3ChunkSize }), + task, + configOverrides: { + 'strategies.tilesDeletion.batchSize': BATCHING_SCENARIO.strategyBatchSize, + 'strategies.tilesDeletion.concurrency': BATCHING_SCENARIO.strategyConcurrency, + }, + }); + await runSingleTask(); + const remaining = await backend.list(`${tilesRelativePath}/`); + + expect(remaining.sort()).toEqual([...extras].sort()); + expect(queueClient.ack).toHaveBeenCalledWith(task.jobId, task.id); + expect(queueClient.reject).not.toHaveBeenCalled(); + expect(jobTrackerClient.notify).toHaveBeenCalledWith(task.id); + // One update per flush of `concurrency` full batches. The trailing partial batch reports + // nothing, and the terminal 100% is the queue's task-ack rather than the strategy's job. + expect(queueClient.updateProgress).toHaveBeenCalledTimes(BATCHING_SCENARIO.expectedFlushCount); + expect(queueClient.updateProgress).toHaveBeenNthCalledWith(1, task.jobId, task.id, BATCHING_SCENARIO.firstFlushPercentage); + expect(queueClient.updateProgress).toHaveBeenNthCalledWith(2, task.jobId, task.id, BATCHING_SCENARIO.secondFlushPercentage); + }); + }); + + describe('multiple ranges', () => { + it('deletes every tile of every range, leaving neighbouring tiles and zooms of each intact', async () => { + const tilesRelativePath = `${faker.string.uuid()}/${faker.string.uuid()}`; + const params: PathAddressedParams = backend.buildParams(tilesRelativePath, { ranges: MULTIPLE_RANGES_SCENARIO.ranges }); + const target = tilePathsForRanges(params); + // Guards against a range set that silently collapses to fewer tiles than intended. + expect(target).toHaveLength(MULTIPLE_RANGES_SCENARIO.tileCount); + + const extras = [...new Set([...extraTilePathsAroundRanges(params, 1), ...extraTilePathsAtAdjacentZooms(params, [-1, 1])])]; + await backend.seed([...target, ...extras]); + const task = buildTask({ type: TASK_TYPE, parameters: params }); + + const { runSingleTask, queueClient, jobTrackerClient } = buildPoller({ providers: storageContext.providers, task }); + await runSingleTask(); + const remaining = await backend.list(`${tilesRelativePath}/`); + + // Every range contributes deletions, and nothing outside them is touched — so a + // regression that only walked the first range would leave survivors here. + expect(remaining.sort()).toEqual([...extras].sort()); + expect(queueClient.ack).toHaveBeenCalledWith(task.jobId, task.id); + expect(queueClient.reject).not.toHaveBeenCalled(); + expect(jobTrackerClient.notify).toHaveBeenCalledWith(task.id); + }); + }); + + describe('sad path', () => { + it('rejects the task as unrecoverable without acking when the storage target does not exist', async () => { + // Nothing is seeded, so the path the task points at was never created on the backend. + const params: PathAddressedParams = backend.buildParams(`${faker.string.uuid()}/${faker.string.uuid()}`); + const task = buildTask({ type: TASK_TYPE, parameters: params }); + + const { runSingleTask, queueClient, jobTrackerClient } = buildPoller({ providers: storageContext.providers, task }); + await runSingleTask(); + + // shouldRetry=false is the whole point: a missing target is a producer bug, not a blip. + expect(queueClient.reject).toHaveBeenCalledWith(task.jobId, task.id, false, expect.stringContaining('storage target does not exist')); + expect(queueClient.ack).not.toHaveBeenCalled(); + expect(jobTrackerClient.notify).toHaveBeenCalledWith(task.id); + }); + }); + }); +}); diff --git a/vitest.integration.config.mts b/vitest.integration.config.mts new file mode 100644 index 0000000..28104d1 --- /dev/null +++ b/vitest.integration.config.mts @@ -0,0 +1,29 @@ +import { defineConfig, type ViteUserConfig } from 'vitest/config'; +import path from 'path'; +import tsconfig from './tsconfig.json'; + +const pathAlias = Object.fromEntries( + Object.entries(tsconfig.compilerOptions.paths).map(([key, [value]]) => [key.replace('/*', ''), path.resolve(__dirname, value.replace('/*', ''))]) +); + +const reporters: Exclude['reporters'] = ['default']; +if (process.env.GITHUB_ACTIONS) { + reporters.push('github-actions'); +} + +export default defineConfig({ + resolve: { + alias: { + ...pathAlias, + '@map-colonies/raster-shared': path.resolve(__dirname, 'node_modules/@map-colonies/raster-shared/dist/index.js'), + }, + }, + test: { + setupFiles: ['./tests/setup/vite.setup.ts'], + include: ['tests/integration/**/*.integration.spec.ts'], + environment: 'node', + reporters, + testTimeout: 120_000, + hookTimeout: 120_000, + }, +});