diff --git a/.gitea/workflows/run-tests.yaml b/.gitea/workflows/run-tests.yaml index 4e54e60..6af0fc3 100644 --- a/.gitea/workflows/run-tests.yaml +++ b/.gitea/workflows/run-tests.yaml @@ -1,16 +1,14 @@ # Gitea Actions – Server Tests # # Two jobs run on every push to any branch: -# 1. jest-integration – Jest tests against the server's data methods (needs MongoDB) -# 2. mocha-unit – Existing Mocha/Chai tests in tests/ and tests/utils/ +# 1. jest-integration – Jest integration tests using an in-memory MongoDB +# instance (mongodb-memory-server). No external database +# or repository secrets are required. +# 2. mocha-unit – Existing Mocha/Chai unit tests in tests/ and tests/utils/ # -# Prerequisites (Gitea repository secrets): -# DB_HOSTS – MongoDB host(s), e.g. "127.0.0.1:27017" -# DB_NAME – Must contain "test", e.g. "agmission_test" -# DB_USR – MongoDB username -# DB_PWD – MongoDB password -# DB_AUTH_SRC – MongoDB auth source (default: "admin") -# TOKEN_SECRET – JWT secret used by the server's auth helpers +# Optional repository secret: +# TOKEN_SECRET – JWT secret used by the server's auth helpers. +# A safe default is used automatically when absent. name: Server Tests @@ -19,10 +17,69 @@ on: branches: - '**' -# ── Shared env-file step (inline, re-used by both jobs via heredoc) ────────── +# ───────────────────────────────────────────────────────────────────────────── jobs: # ══════════════════════════════════════════════════════════════════════════ + # Job 1: Jest integration tests (in-memory MongoDB via mongodb-memory-server) + # ══════════════════════════════════════════════════════════════════════════ + jest-integration: + name: Jest – Integration Tests + runs-on: self-hosted + + defaults: + run: + working-directory: Development/server + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + cache-dependency-path: Development/server/package-lock.json + + - name: Install dependencies + run: npm ci --prefer-offline + + - name: Write test environment file + run: | + cat > environment.test.env <<'EOF' + NODE_ENV=test + TOKEN_SECRET=${{ secrets.TOKEN_SECRET || 'ci-test-secret-not-for-production' }} + PRODUCTION=false + NO_EMAIL_MODE=true + ENABLE_SUBSCRIPTION=false + INV_IMG_VIR_DIR=/tmp/inv-img + INV_UPLOAD_DIR=/tmp/inv-upload + INV_MAX_UPLOAD_SIZE_MB=5 + PAGINATION_DEFAULT_LIMIT=1000 + PAGINATION_MAX_LIMIT=14000 + STRIPE_SEC_KEY=sk_test_placeholder + STRIPE_API_VERSION=2022-11-15 + EOF + + - name: Enforce controller integration contract + env: + TEST_ENV_FILE: ./environment.test.env + NODE_ENV: test + run: npm run test:integration:contract + + - name: Run Jest integration tests + env: + TEST_ENV_FILE: ./environment.test.env + NODE_ENV: test + run: npm run test:integration:jest -- --ci + + - name: Upload Jest results + if: always() + uses: actions/upload-artifact@v4 + with: + name: jest-integration-results + # ══════════════════════════════════════════════════════════════════════════ # Job 2: Mocha/Chai tests – tests/ and tests/utils/ # These tests are self-contained unit tests that do not require MongoDB. # ══════════════════════════════════════════════════════════════════════════ diff --git a/Development/package-lock.json b/Development/package-lock.json new file mode 100644 index 0000000..4b93e56 --- /dev/null +++ b/Development/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "Development", + "lockfileVersion": 2, + "requires": true, + "packages": {} +} diff --git a/Development/server/controllers/api_export.js b/Development/server/controllers/api_export.js index 6d60f62..cd5e8d1 100644 --- a/Development/server/controllers/api_export.js +++ b/Development/server/controllers/api_export.js @@ -361,7 +361,11 @@ async function generateExport(exportJobId) { } catch (err) { exportJob.status = ExportJobStatus.ERROR; exportJob.errorMsg = err.message; - await exportJob.save(); + try { + await exportJob.save(); + } catch (saveErr) { + // ExportJob may have been deleted before we could update it – ignore + } console.error('[export] generation failed', err); } } diff --git a/Development/server/jest.integration.config.js b/Development/server/jest.integration.config.js new file mode 100644 index 0000000..5841a87 --- /dev/null +++ b/Development/server/jest.integration.config.js @@ -0,0 +1,17 @@ +'use strict'; + +/** @type {import('jest').Config} */ +module.exports = { + testMatch: ['**/tests/integration/**/*.integration.test.js'], + testEnvironment: 'node', + // Allow extra time on first run when mongodb-memory-server downloads its binary. + testTimeout: 60000, + // Each test file spins up its own in-memory MongoDB instance so files can + // run in parallel safely. Keep maxWorkers=1 to avoid hammering the CI + // machine; increase if your machine has spare cores. + maxWorkers: 1, + // CommonJS project – no Babel/ESM transform needed. + transform: {}, + // Silence the mongodb-memory-server download progress logs during tests. + silent: false, +}; diff --git a/Development/server/package-lock.json b/Development/server/package-lock.json index b7b83e3..6ce0707 100644 --- a/Development/server/package-lock.json +++ b/Development/server/package-lock.json @@ -32,7 +32,7 @@ "debug": "^4.1.1", "dotenv": "^16.4.5", "email-templates": "11.0.3", - "error-handler": "file:../../../../@agn/error-handler", + "error-handler": "file:../../../@agn/error-handler", "exceljs": "^4.2.1", "express": "^4.18.1", "express-async-errors": "^3.1.1", @@ -84,7 +84,9 @@ }, "devDependencies": { "chai": "^4.3.10", + "jest": "^29.7.0", "mocha": "^10.2.0", + "mongodb-memory-server": "^9.5.0", "nyc": "^15.1.0" }, "engines": { @@ -94,6 +96,16 @@ }, "../../../../@agn/error-handler": { "name": "error-handler", + "version": "2.0.0", + "extraneous": true, + "license": "Proprietary", + "dependencies": { + "debug": "^4.4.0", + "key-file-storage": "^2.3.3", + "mailer": "file:../mailer" + } + }, + "../../../@agn/error-handler": { "version": "2.0.0", "license": "Proprietary", "dependencies": { @@ -901,6 +913,15 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -953,6 +974,228 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -997,6 +1240,12 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, "node_modules/@fast-csv/format": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", @@ -1189,6 +1438,477 @@ "node": ">=8" } }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@jest/core/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@jest/core/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@jest/transform/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@jest/transform/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@jest/transform/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@jest/transform/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1586,6 +2306,39 @@ "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/commons/node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, "node_modules/@smithy/config-resolver": { "version": "4.4.15", "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.15.tgz", @@ -3968,6 +4721,47 @@ "url": "https://opencollective.com/turf" } }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -3981,11 +4775,44 @@ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==" }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/is-valid-path": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/is-valid-path/-/is-valid-path-0.1.2.tgz", "integrity": "sha512-BsZtkfiPpnzDWFjSZanYllttVW7/46ayPZkcHBCSFBkBqIO9rWrflUvEmT2tF///hnPLwBJU3TJPzbBxpUEqCg==" }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -3999,6 +4826,12 @@ "undici-types": "~7.19.0" } }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -4013,6 +4846,21 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -4309,6 +5157,33 @@ "node": ">=0.10.0" } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-gray": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", @@ -4763,6 +5638,15 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" }, + "node_modules/async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4826,6 +5710,139 @@ "proxy-from-env": "^2.1.0" } }, + "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, + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/babel-walk": { "version": "3.0.0-canary-5", "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", @@ -4842,6 +5859,97 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "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, + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "dev": true, + "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.9.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.0.tgz", + "integrity": "sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q==", + "dev": true, + "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, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", + "dev": true, + "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.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.2.tgz", + "integrity": "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==", + "dev": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", @@ -5112,6 +6220,15 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, "node_modules/bson": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz", @@ -5430,6 +6547,15 @@ "node": ">=8" } }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/character-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", @@ -5586,6 +6712,12 @@ "node": ">=8" } }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true + }, "node_modules/class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -5708,6 +6840,16 @@ "node": ">=0.10.0" } }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, "node_modules/code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -5716,6 +6858,12 @@ "node": ">=0.10.0" } }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true + }, "node_modules/collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -6026,6 +7174,27 @@ "node": ">= 0.4" } }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/cross-fetch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", @@ -6177,6 +7346,20 @@ "node": ">=0.10" } }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -6406,6 +7589,15 @@ "node": ">=0.3.1" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/display-notification": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/display-notification/-/display-notification-3.0.0.tgz", @@ -6588,6 +7780,18 @@ "node": ">=14" } }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -6661,7 +7865,7 @@ } }, "node_modules/error-handler": { - "resolved": "../../../../@agn/error-handler", + "resolved": "../../../@agn/error-handler", "link": true }, "node_modules/error-symbol": { @@ -6794,6 +7998,15 @@ "node": ">= 0.6" } }, + "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, + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/exceljs": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", @@ -6885,6 +8098,15 @@ "util-extend": "^1.0.1" } }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -6938,6 +8160,22 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -7152,6 +8390,12 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "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 + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -7205,6 +8449,15 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -8456,6 +9709,25 @@ "node": ">=4" } }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -8702,6 +9974,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -9062,6 +10343,805 @@ "resolved": "https://registry.npmjs.org/jdataview/-/jdataview-2.5.0.tgz", "integrity": "sha512-ZJop3D5nyDcWPBPv4NPnhCvx3HgQNsCXMfw8gpNKY16BobgxmVF+kJ08aHuqk6bJQVeL2mkf6nDCcZPMompalw==" }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-config/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/jest-config/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-haste-map/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-haste-map/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-haste-map/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-haste-map/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/jest-haste-map/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-message-util/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/jest-message-util/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/joi": { "version": "17.13.3", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", @@ -9354,6 +11434,15 @@ "node": ">=0.10.0" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/lazy-cache": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", @@ -9416,6 +11505,15 @@ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/libbase64": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.2.1.tgz", @@ -9765,6 +11863,15 @@ "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-7.5.0.tgz", "integrity": "sha512-0booA+aVYyVFoR67JBHdfVk0U08HmrBH2FrtmBqBa+NldlqXv/G2Z9VQuQq6Wgp2jDWdybEWGfBkk1cq5264WA==" }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -10183,6 +12290,152 @@ "whatwg-url": "^11.0.0" } }, + "node_modules/mongodb-memory-server": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-9.5.0.tgz", + "integrity": "sha512-In3zRT40cLlVtpy7FK6b96Lby6JBAdXj8Kf9YrH4p1Aa2X4ptojq7SmiRR3x47Lo0/UCXXIwhJpkdbYY8kRZAw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "mongodb-memory-server-core": "9.5.0", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongodb-memory-server-core": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-9.5.0.tgz", + "integrity": "sha512-Jb/V80JeYAKWaF4bPFme7SmTR6ew1PWgkpPUepLDfRraeN49i1cruxICeA4zz4T33W/o31N+zazP8wI8ebf7yw==", + "dev": true, + "dependencies": { + "async-mutex": "^0.4.1", + "camelcase": "^6.3.0", + "debug": "^4.3.7", + "find-cache-dir": "^3.3.2", + "follow-redirects": "^1.15.9", + "https-proxy-agent": "^7.0.5", + "mongodb": "^5.9.2", + "new-find-package-json": "^2.0.0", + "semver": "^7.6.3", + "tar-stream": "^3.1.7", + "tslib": "^2.6.3", + "yauzl": "^3.1.3" + }, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/bson": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", + "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==", + "dev": true, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/mongodb": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", + "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==", + "dev": true, + "dependencies": { + "bson": "^5.5.0", + "mongodb-connection-string-url": "^2.6.0", + "socks": "^2.7.1" + }, + "engines": { + "node": ">=14.20.1" + }, + "optionalDependencies": { + "@mongodb-js/saslprep": "^1.1.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.0.0", + "kerberos": "^1.0.0 || ^2.0.0", + "mongodb-client-encryption": ">=2.3.0 <3", + "snappy": "^7.2.2" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + } + } + }, + "node_modules/mongodb-memory-server-core/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, + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/yauzl": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.0.tgz", + "integrity": "sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/mongoose": { "version": "6.13.9", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.13.9.tgz", @@ -10395,6 +12648,12 @@ "node": ">=0.10.0" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "node_modules/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", @@ -10408,6 +12667,18 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/new-find-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz", + "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">=12.22.0" + } + }, "node_modules/node-abi": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", @@ -10519,6 +12790,12 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, "node_modules/node-libxml": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/node-libxml/-/node-libxml-4.1.2.tgz", @@ -12130,6 +14407,15 @@ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==" }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -12246,6 +14532,32 @@ "prebuildify": "bin.js" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/preview-email": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/preview-email/-/preview-email-3.1.3.tgz", @@ -12347,6 +14659,19 @@ "asap": "~2.0.3" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -12598,6 +14923,22 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, "node_modules/qs": { "version": "6.15.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", @@ -12712,6 +15053,12 @@ "node": ">=0.10.0" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -13071,6 +15418,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -13086,6 +15445,15 @@ "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", "deprecated": "https://github.com/lydell/resolve-url#deprecated" }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -13518,11 +15886,26 @@ "resolved": "https://registry.npmjs.org/simplify-path/-/simplify-path-1.1.0.tgz", "integrity": "sha512-qvEyrV36pP6YjRoEAe7ymSqFwurrTQXltcmZaQXFVh8cTWUfHYGeobbMK8V7WHlT9+ysW3GNVkCd6TF8aM0ydw==" }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, "node_modules/skmeans": { "version": "0.9.7", "resolved": "https://registry.npmjs.org/skmeans/-/skmeans-0.9.7.tgz", "integrity": "sha512-hNj1/oZ7ygsfmPZ7ZfN5MUBRoGg1gtpnImuJBgLO0ljQ67DtJuiQaiYdS4lUA6s0KCwnPhGivtC/WRwIZLkHyg==" }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/slice-source": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/slice-source/-/slice-source-0.4.1.tgz", @@ -13684,6 +16067,16 @@ "urix": "^0.1.0" } }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/source-map-url": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", @@ -13795,6 +16188,27 @@ "node": ">=0.10.0" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -13864,6 +16278,17 @@ "node": ">=0.8.0" } }, + "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, + "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", @@ -13877,6 +16302,40 @@ "resolved": "https://registry.npmjs.org/string-format-js/-/string-format-js-1.0.0.tgz", "integrity": "sha512-huaT7ujQTFhhMmFnJOjwrZQvUu3vgRKrrzcmOF8Ls6iKIg1EHvyQCaGDOrKV1sRnOku4CTI+iJbXBAmWkeVhTQ==" }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -14057,6 +16516,15 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "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, + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -14071,6 +16539,15 @@ "node": ">=8" } }, + "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, + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-encoding": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", @@ -14163,6 +16640,12 @@ "node": ">=14.14" } }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, "node_modules/to-gfm-code-block": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/to-gfm-code-block/-/to-gfm-code-block-0.1.1.tgz", @@ -14715,6 +17198,26 @@ "uuid": "dist-node/bin/uuid" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/valid-data-url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", @@ -14760,6 +17263,15 @@ "node": ">=0.10.0" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/warning-symbol": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/warning-symbol/-/warning-symbol-0.1.0.tgz", @@ -15900,6 +18412,12 @@ "@babel/traverse": "^7.28.6" } }, + "@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true + }, "@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -15934,6 +18452,159 @@ "@babel/types": "^7.29.0" } }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.28.6" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.28.6" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.28.6" + } + }, "@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -15969,6 +18640,12 @@ "@babel/helper-validator-identifier": "^7.28.5" } }, + "@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 + }, "@fast-csv/format": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", @@ -16134,6 +18811,377 @@ "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true }, + "@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, + "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, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "requires": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + } + }, + "@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "requires": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + } + }, + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3" + } + }, + "@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + } + }, + "@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "requires": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + } + }, + "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, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + } + }, + "@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "dependencies": { + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + } + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, "@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -16466,6 +19514,38 @@ "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" }, + "@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true + }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + }, + "dependencies": { + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + } + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, "@smithy/config-resolver": { "version": "4.4.15", "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.15.tgz", @@ -18392,6 +21472,47 @@ "d3-voronoi": "1.1.2" } }, + "@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "requires": { + "@babel/types": "^7.28.2" + } + }, "@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -18405,11 +21526,44 @@ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==" }, + "@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/is-valid-path": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/is-valid-path/-/is-valid-path-0.1.2.tgz", "integrity": "sha512-BsZtkfiPpnzDWFjSZanYllttVW7/46ayPZkcHBCSFBkBqIO9rWrflUvEmT2tF///hnPLwBJU3TJPzbBxpUEqCg==" }, + "@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -18423,6 +21577,12 @@ "undici-types": "~7.19.0" } }, + "@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, "@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -18437,6 +21597,21 @@ "@types/webidl-conversions": "*" } }, + "@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, "@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -18662,6 +21837,23 @@ "ansi-wrap": "0.1.0" } }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } + } + }, "ansi-gray": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", @@ -19022,6 +22214,15 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" }, + "async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "dev": true, + "requires": { + "tslib": "^2.4.0" + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -19070,6 +22271,107 @@ "proxy-from-env": "^2.1.0" } }, + "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": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "requires": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "dependencies": { + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + } + }, + "babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, "babel-walk": { "version": "3.0.0-canary-5", "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", @@ -19083,6 +22385,60 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "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.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "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.9.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.0.tgz", + "integrity": "sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q==", + "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.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", + "dev": true, + "requires": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + } + }, + "bare-url": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.2.tgz", + "integrity": "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==", + "dev": true, + "requires": { + "bare-path": "^3.0.0" + } + }, "base": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", @@ -19280,6 +22636,15 @@ "update-browserslist-db": "^1.2.3" } }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, "bson": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz", @@ -19504,6 +22869,12 @@ } } }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, "character-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", @@ -19615,6 +22986,12 @@ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==" }, + "cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -19711,11 +23088,23 @@ "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==" }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==" }, + "collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -19966,6 +23355,21 @@ } } }, + "create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + } + }, "cross-fetch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", @@ -20087,6 +23491,13 @@ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==" }, + "dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "requires": {} + }, "deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -20253,6 +23664,12 @@ "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, "display-notification": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/display-notification/-/display-notification-3.0.0.tgz", @@ -20400,6 +23817,12 @@ "preview-email": "^3.0.10" } }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -20460,7 +23883,7 @@ } }, "error-handler": { - "version": "file:../../../../@agn/error-handler", + "version": "file:../../../@agn/error-handler", "requires": { "debug": "^4.4.0", "key-file-storage": "^2.3.3", @@ -20548,6 +23971,15 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, + "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" + } + }, "exceljs": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", @@ -20619,6 +24051,12 @@ "util-extend": "^1.0.1" } }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -20665,6 +24103,19 @@ } } }, + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + } + }, "express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -20836,6 +24287,12 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "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-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -20871,6 +24328,15 @@ "strnum": "^2.2.0" } }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -21780,6 +25246,16 @@ } } }, + "import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -21952,6 +25428,12 @@ "number-is-nan": "^1.0.0" } }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -22223,6 +25705,622 @@ "resolved": "https://registry.npmjs.org/jdataview/-/jdataview-2.5.0.tgz", "integrity": "sha512-ZJop3D5nyDcWPBPv4NPnhCvx3HgQNsCXMfw8gpNKY16BobgxmVF+kJ08aHuqk6bJQVeL2mkf6nDCcZPMompalw==" }, + "jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + } + }, + "jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + } + }, + "jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "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, + "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", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + } + } + }, + "jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + } + }, + "jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "dependencies": { + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "requires": {} + }, + "jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true + }, + "jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "requires": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + } + }, + "jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + } + }, + "jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + } + }, + "jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + } + }, + "jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + } + }, "joi": { "version": "17.13.3", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", @@ -22475,6 +26573,12 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, "lazy-cache": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", @@ -22530,6 +26634,12 @@ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, "libbase64": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.2.1.tgz", @@ -22846,6 +26956,15 @@ "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-7.5.0.tgz", "integrity": "sha512-0booA+aVYyVFoR67JBHdfVk0U08HmrBH2FrtmBqBa+NldlqXv/G2Z9VQuQq6Wgp2jDWdybEWGfBkk1cq5264WA==" }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -23168,6 +27287,100 @@ "whatwg-url": "^11.0.0" } }, + "mongodb-memory-server": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-9.5.0.tgz", + "integrity": "sha512-In3zRT40cLlVtpy7FK6b96Lby6JBAdXj8Kf9YrH4p1Aa2X4ptojq7SmiRR3x47Lo0/UCXXIwhJpkdbYY8kRZAw==", + "dev": true, + "requires": { + "mongodb-memory-server-core": "9.5.0", + "tslib": "^2.6.3" + } + }, + "mongodb-memory-server-core": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-9.5.0.tgz", + "integrity": "sha512-Jb/V80JeYAKWaF4bPFme7SmTR6ew1PWgkpPUepLDfRraeN49i1cruxICeA4zz4T33W/o31N+zazP8wI8ebf7yw==", + "dev": true, + "requires": { + "async-mutex": "^0.4.1", + "camelcase": "^6.3.0", + "debug": "^4.3.7", + "find-cache-dir": "^3.3.2", + "follow-redirects": "^1.15.9", + "https-proxy-agent": "^7.0.5", + "mongodb": "^5.9.2", + "new-find-package-json": "^2.0.0", + "semver": "^7.6.3", + "tar-stream": "^3.1.7", + "tslib": "^2.6.3", + "yauzl": "^3.1.3" + }, + "dependencies": { + "agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true + }, + "bson": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", + "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==", + "dev": true + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + } + }, + "mongodb": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", + "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==", + "dev": true, + "requires": { + "@mongodb-js/saslprep": "^1.1.0", + "bson": "^5.5.0", + "mongodb-connection-string-url": "^2.6.0", + "socks": "^2.7.1" + } + }, + "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, + "requires": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "yauzl": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.0.tgz", + "integrity": "sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + } + } + } + }, "mongoose": { "version": "6.13.9", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.13.9.tgz", @@ -23342,6 +27555,12 @@ } } }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", @@ -23352,6 +27571,15 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "new-find-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz", + "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, "node-abi": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", @@ -23438,6 +27666,12 @@ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==" }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, "node-libxml": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/node-libxml/-/node-libxml-4.1.2.tgz", @@ -24648,6 +28882,12 @@ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==" }, + "pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true + }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -24744,6 +28984,25 @@ "tar-fs": "^2.1.0" } }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, "preview-email": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/preview-email/-/preview-email-3.1.3.tgz", @@ -24815,6 +29074,16 @@ "asap": "~2.0.3" } }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -25037,6 +29306,12 @@ } } }, + "pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true + }, "qs": { "version": "6.15.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", @@ -25128,6 +29403,12 @@ } } }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -25405,6 +29686,15 @@ "supports-preserve-symlinks-flag": "^1.0.0" } }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, "resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -25416,6 +29706,12 @@ "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==" }, + "resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true + }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -25735,11 +30031,23 @@ "resolved": "https://registry.npmjs.org/simplify-path/-/simplify-path-1.1.0.tgz", "integrity": "sha512-qvEyrV36pP6YjRoEAe7ymSqFwurrTQXltcmZaQXFVh8cTWUfHYGeobbMK8V7WHlT9+ysW3GNVkCd6TF8aM0ydw==" }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, "skmeans": { "version": "0.9.7", "resolved": "https://registry.npmjs.org/skmeans/-/skmeans-0.9.7.tgz", "integrity": "sha512-hNj1/oZ7ygsfmPZ7ZfN5MUBRoGg1gtpnImuJBgLO0ljQ67DtJuiQaiYdS4lUA6s0KCwnPhGivtC/WRwIZLkHyg==" }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "slice-source": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/slice-source/-/slice-source-0.4.1.tgz", @@ -25869,6 +30177,16 @@ "urix": "^0.1.0" } }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "source-map-url": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", @@ -25955,6 +30273,23 @@ "tweetnacl": "~0.14.0" } }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, "standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -26008,6 +30343,17 @@ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", "integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==" }, + "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", @@ -26021,6 +30367,33 @@ "resolved": "https://registry.npmjs.org/string-format-js/-/string-format-js-1.0.0.tgz", "integrity": "sha512-huaT7ujQTFhhMmFnJOjwrZQvUu3vgRKrrzcmOF8Ls6iKIg1EHvyQCaGDOrKV1sRnOku4CTI+iJbXBAmWkeVhTQ==" }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "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, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -26150,6 +30523,15 @@ "readable-stream": "^3.1.1" } }, + "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" + } + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -26161,6 +30543,15 @@ "minimatch": "^3.0.4" } }, + "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-encoding": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", @@ -26242,6 +30633,12 @@ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==" }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, "to-gfm-code-block": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/to-gfm-code-block/-/to-gfm-code-block-0.1.1.tgz", @@ -26674,6 +31071,25 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==" }, + "v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "dependencies": { + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + } + } + }, "valid-data-url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", @@ -26704,6 +31120,15 @@ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, "warning-symbol": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/warning-symbol/-/warning-symbol-0.1.0.tgz", diff --git a/Development/server/package.json b/Development/server/package.json index 4f54a3a..f69a62c 100644 --- a/Development/server/package.json +++ b/Development/server/package.json @@ -17,6 +17,8 @@ "test:dlq": "mocha --exit --require tests/setup.js 'tests/dlq/test_*.js'", "test:parsing": "mocha --exit --require tests/setup.js 'tests/parsing/test_*.js'", "test:integration": "mocha --exit --require tests/setup.js 'tests/integration/test_*.js'", + "test:integration:contract": "node tests/integration/check_controller_contract.js", + "test:integration:jest": "NODE_ENV=test jest --config jest.integration.config.js --forceExit", "test:utils": "mocha --exit --require tests/setup.js 'tests/utils/test_*.js'", "test:verbose": "mocha --recursive --exit --require tests/setup.js 'tests/**/test_*.js' --reporter spec", "test:bail": "mocha --recursive --exit --bail --require tests/setup.js 'tests/**/test_*.js'", @@ -64,7 +66,7 @@ "debug": "^4.1.1", "dotenv": "^16.4.5", "email-templates": "11.0.3", - "error-handler": "file:../../../../@agn/error-handler", + "error-handler": "file:../../../@agn/error-handler", "exceljs": "^4.2.1", "express": "^4.18.1", "express-async-errors": "^3.1.1", @@ -123,7 +125,9 @@ }, "devDependencies": { "chai": "^4.3.10", + "jest": "^29.7.0", "mocha": "^10.2.0", + "mongodb-memory-server": "^9.5.0", "nyc": "^15.1.0" } } diff --git a/Development/server/tests/integration/api_export.integration.test.js b/Development/server/tests/integration/api_export.integration.test.js new file mode 100644 index 0000000..071bbcb --- /dev/null +++ b/Development/server/tests/integration/api_export.integration.test.js @@ -0,0 +1,240 @@ +'use strict'; + +/** + * Integration tests – api_export controller (controllers/api_export.js) + * + * Note: generateExport (async file-writing) runs via setImmediate — we do not wait for + * it in these tests. triggerExport returns status='pending' immediately and that is what + * we assert against. downloadExport requires a ready file path and is excluded here. + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockClient, mockJob, mockReq, mockRes } = require('./mock_data'); + +let Job, ExportJob, Customer, Client; + +beforeAll(async () => { + await connectDB(); + ({ Job } = require('../../model')); + ExportJob = require('../../model/export_job'); + Customer = require('../../model/customer'); + Client = require('../../model/client'); +}); + +afterAll(async () => { + // Flush pending setImmediate callbacks (e.g. generateExport) before disconnecting + await new Promise(resolve => setImmediate(resolve)); + await disconnectDB(); +}); + +const apiExportCtl = require('../../controllers/api_export'); + +let _nextJobId = 800001; +function nextJobId() { return _nextJobId++; } + +describe('api_export controller', () => { + let applicator, client, jobRecord; + + beforeAll(async () => { + await clearCollection(Job); + await clearCollection(ExportJob); + + applicator = await Customer.create(mockApplicator()); + client = await Client.create(mockClient(applicator._id)); + + const jobId = nextJobId(); + jobRecord = await Job.create( + mockJob(applicator._id, { _id: jobId, client: client._id }) + ); + }); + + afterAll(async () => { + await clearCollection(Job); + await clearCollection(ExportJob); + await Customer.deleteMany({ _id: { $in: [applicator._id, client._id] } }); + }); + + const makeReq = (extra = {}) => + mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); + + // ------------------------------------------------------------------------- + describe('triggerExport', () => { + it('throws when jobId param is not a finite number', async () => { + const req = makeReq({ + params: { jobId: 'nan' }, + body: { format: 'csv' }, + }); + const res = mockRes(); + await expect(apiExportCtl.triggerExport(req, res)).rejects.toThrow(); + }); + + it('throws JOB_NOT_FOUND when job does not exist', async () => { + const req = makeReq({ + params: { jobId: '999999' }, + body: { format: 'csv' }, + }); + const res = mockRes(); + await expect(apiExportCtl.triggerExport(req, res)).rejects.toThrow(); + }); + + it('throws AppAuthError when uid does not own the job', async () => { + const other = await Customer.create(mockApplicator()); + const req = mockReq({ + uid: other._id, + puid: other._id, + params: { jobId: String(jobRecord._id) }, + body: { format: 'csv' }, + }); + const res = mockRes(); + await expect(apiExportCtl.triggerExport(req, res)).rejects.toThrow(); + await Customer.deleteMany({ _id: other._id }); + }); + + it('returns 400 when format is invalid', async () => { + const req = makeReq({ + params: { jobId: String(jobRecord._id) }, + body: { format: 'xlsx' }, + }); + const res = mockRes(); + + await apiExportCtl.triggerExport(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + }); + + it('creates a pending ExportJob and returns exportId + status=pending for csv', async () => { + await ExportJob.deleteMany({}); + + const req = makeReq({ + params: { jobId: String(jobRecord._id) }, + body: { format: 'csv' }, + }); + const res = mockRes(); + + await apiExportCtl.triggerExport(req, res); + + expect(res.statusCode).toBe(202); + expect(res._data.status).toBe('pending'); + expect(res._data.exportId).toBeDefined(); + expect(res._data.format).toBe('csv'); + + const saved = await ExportJob.findById(res._data.exportId); + expect(saved).not.toBeNull(); + expect(saved.jobId).toBe(jobRecord._id); + }); + + it('creates a pending ExportJob for geojson format', async () => { + await ExportJob.deleteMany({}); + + const req = makeReq({ + params: { jobId: String(jobRecord._id) }, + body: { format: 'geojson' }, + }); + const res = mockRes(); + + await apiExportCtl.triggerExport(req, res); + + expect(res.statusCode).toBe(202); + expect(res._data.format).toBe('geojson'); + }); + + it('deduplicates: returns existing pending export on second identical request', async () => { + await ExportJob.deleteMany({}); + + const reqBody = { format: 'csv', units: 'metric' }; + + const req1 = makeReq({ params: { jobId: String(jobRecord._id) }, body: reqBody }); + const res1 = mockRes(); + await apiExportCtl.triggerExport(req1, res1); + const firstExportId = String(res1._data.exportId); + + // Second identical request within dedup window + const req2 = makeReq({ params: { jobId: String(jobRecord._id) }, body: reqBody }); + const res2 = mockRes(); + await apiExportCtl.triggerExport(req2, res2); + + expect(res2._data.reused).toBe(true); + expect(String(res2._data.exportId)).toBe(firstExportId); + }); + + it('deduplicates: different format creates a new export', async () => { + await ExportJob.deleteMany({}); + + const req1 = makeReq({ + params: { jobId: String(jobRecord._id) }, + body: { format: 'csv' }, + }); + const res1 = mockRes(); + await apiExportCtl.triggerExport(req1, res1); + + const req2 = makeReq({ + params: { jobId: String(jobRecord._id) }, + body: { format: 'geojson' }, + }); + const res2 = mockRes(); + await apiExportCtl.triggerExport(req2, res2); + + expect(res2._data.reused).toBeUndefined(); + expect(String(res2._data.exportId)).not.toBe(String(res1._data.exportId)); + }); + }); + + // ------------------------------------------------------------------------- + describe('getExportStatus', () => { + it('throws when exportId is not a valid ObjectId', async () => { + const req = makeReq({ params: { exportId: 'not-an-id' } }); + const res = mockRes(); + await expect(apiExportCtl.getExportStatus(req, res)).rejects.toThrow(); + }); + + it('returns 404 when exportId does not exist', async () => { + const mongoose = require('mongoose'); + const req = makeReq({ + params: { exportId: new mongoose.Types.ObjectId().toHexString() }, + }); + const res = mockRes(); + await apiExportCtl.getExportStatus(req, res); + expect(res.statusCode).toBe(404); + }); + + it('returns the export status for a valid exportId owned by the user', async () => { + const req = makeReq({ + params: { jobId: String(jobRecord._id) }, + body: { format: 'csv' }, + }); + const createRes = mockRes(); + await apiExportCtl.triggerExport(req, createRes); + const exportId = String(createRes._data.exportId); + + const statusReq = makeReq({ params: { exportId } }); + const statusRes = mockRes(); + + await apiExportCtl.getExportStatus(statusReq, statusRes); + + expect(statusRes.json).toHaveBeenCalled(); + expect(statusRes._data.status).toBeDefined(); + expect(String(statusRes._data.exportId)).toBe(exportId); + }); + + it('returns 404 when export belongs to a different user', async () => { + const mongoose = require('mongoose'); + const { ObjectId } = require('mongodb'); + + // Create an ExportJob owned by someone else + const otherOwnerId = new mongoose.Types.ObjectId(); + const job = await ExportJob.create({ + owner: ObjectId(otherOwnerId.toHexString()), + jobId: jobRecord._id, + format: 'csv', + units: 'metric', + fm: false, + status: 'pending', + }); + + const req = makeReq({ params: { exportId: String(job._id) } }); + const res = mockRes(); + await apiExportCtl.getExportStatus(req, res); + expect(res.statusCode).toBe(404); + }); + }); +}); diff --git a/Development/server/tests/integration/api_key.integration.test.js b/Development/server/tests/integration/api_key.integration.test.js new file mode 100644 index 0000000..d7316c5 --- /dev/null +++ b/Development/server/tests/integration/api_key.integration.test.js @@ -0,0 +1,189 @@ +'use strict'; + +/** + * Integration tests – api_key controller (controllers/api_key.js) + * + * Note: bcrypt operations are slow (10 rounds) – jest timeout is raised to + * 30 seconds per test. + */ + +jest.setTimeout(30000); + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockReq, mockRes, newId } = require('./mock_data'); + +let ApiKey, Customer; + +beforeAll(async () => { + await connectDB(); + ApiKey = require('../../model/api_key'); + Customer = require('../../model/customer'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const apiKeyCtl = require('../../controllers/api_key'); + +describe('api_key controller – data methods', () => { + let applicator; + + beforeAll(async () => { + await clearCollection(ApiKey); + applicator = await Customer.create(mockApplicator()); + }); + + afterAll(async () => { + await clearCollection(ApiKey); + await Customer.deleteMany({ _id: applicator._id }); + }); + + const makeReq = (extra = {}) => + mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); + + // ------------------------------------------------------------------------- + describe('createKey', () => { + it('creates an API key and returns the raw key once', async () => { + const req = makeReq({ body: { label: 'Key 1' } }); + const res = mockRes(); + + await apiKeyCtl.createKey(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.key).toBeDefined(); + expect(typeof res._data.key).toBe('string'); + expect(res._data.key.length).toBeGreaterThan(10); + }); + + it('throws when owner already has 10 keys', async () => { + await clearCollection(ApiKey); + // Create 10 keys first + const docs = []; + for (let i = 0; i < 10; i++) { + docs.push({ + owner: applicator._id, + label: `Key ${i}`, + keyHash: `fakehash${i}`, + prefix: `pfx${i}`, + }); + } + await ApiKey.insertMany(docs); + + const req = makeReq({ body: { label: 'Extra Key' } }); + const res = mockRes(); + + await expect(apiKeyCtl.createKey(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('listKeys', () => { + beforeAll(async () => { + await clearCollection(ApiKey); + const req = makeReq({ body: { label: 'Listed Key' } }); + const res = mockRes(); + await apiKeyCtl.createKey(req, res); + }); + + it('returns list of API keys without hash field', async () => { + const req = makeReq(); + const res = mockRes(); + + await apiKeyCtl.listKeys(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + expect(res._data.length).toBeGreaterThan(0); + expect(res._data[0].hash).toBeUndefined(); + }); + }); + + // ------------------------------------------------------------------------- + describe('revokeKey', () => { + let keyId; + + beforeAll(async () => { + await clearCollection(ApiKey); + const req = makeReq({ body: { label: 'Key to Revoke' } }); + const res = mockRes(); + await apiKeyCtl.createKey(req, res); + const listReq = makeReq(); + const listRes = mockRes(); + await apiKeyCtl.listKeys(listReq, listRes); + keyId = String(listRes._data[0]._id); + }); + + it('revokes an active API key', async () => { + const req = makeReq({ params: { keyId }, ut: '0' }); + const res = mockRes(); + + await apiKeyCtl.revokeKey(req, res); + + expect(res.end).toHaveBeenCalled(); + const doc = await ApiKey.findById(keyId); + expect(doc.active).toBe(false); + }); + + it('returns 404 when key not found', async () => { + const req = makeReq({ params: { keyId: String(newId()) }, ut: '0' }); + const res = mockRes(); + await apiKeyCtl.revokeKey(req, res); + expect(res.statusCode).toBe(404); + }); + }); + + // ------------------------------------------------------------------------- + describe('regenerateKey', () => { + let keyId; + + beforeAll(async () => { + await clearCollection(ApiKey); + const req = makeReq({ body: { label: 'Key to Regen' } }); + const res = mockRes(); + await apiKeyCtl.createKey(req, res); + const listReq = makeReq(); + const listRes = mockRes(); + await apiKeyCtl.listKeys(listReq, listRes); + keyId = String(listRes._data[0]._id); + }); + + it('regenerates key and returns new raw key', async () => { + const req = makeReq({ params: { keyId } }); + const res = mockRes(); + + await apiKeyCtl.regenerateKey(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.key).toBeDefined(); + expect(typeof res._data.key).toBe('string'); + }); + }); + + // ------------------------------------------------------------------------- + describe('deleteKey', () => { + let keyId; + + beforeAll(async () => { + await clearCollection(ApiKey); + const req = makeReq({ body: { label: 'Key to Delete' } }); + const res = mockRes(); + await apiKeyCtl.createKey(req, res); + const listReq = makeReq(); + const listRes = mockRes(); + await apiKeyCtl.listKeys(listReq, listRes); + keyId = String(listRes._data[0]._id); + }); + + it('permanently deletes an API key', async () => { + const req = makeReq({ params: { keyId } }); + const res = mockRes(); + + await apiKeyCtl.deleteKey(req, res); + + expect(res.end).toHaveBeenCalled(); + const found = await ApiKey.findById(keyId); + expect(found).toBeNull(); + }); + }); +}); diff --git a/Development/server/tests/integration/api_pub.integration.test.js b/Development/server/tests/integration/api_pub.integration.test.js new file mode 100644 index 0000000..8f8f0c0 --- /dev/null +++ b/Development/server/tests/integration/api_pub.integration.test.js @@ -0,0 +1,198 @@ +'use strict'; + +/** + * Integration tests – api_pub controller (controllers/api_pub.js) + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockClient, mockJob, mockReq, mockRes } = require('./mock_data'); + +let Job, App, AppFile, Customer, Client; + +beforeAll(async () => { + await connectDB(); + ({ Job, App, AppFile } = require('../../model')); + Customer = require('../../model/customer'); + Client = require('../../model/client'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const apiPubCtl = require('../../controllers/api_pub'); + +/** Generate a unique integer job id that won't collide across tests */ +let _nextJobId = 900001; +function nextJobId() { return _nextJobId++; } + +describe('api_pub controller', () => { + let applicator, client, jobRecord; + + beforeAll(async () => { + await clearCollection(Job); + await clearCollection(App); + await clearCollection(AppFile); + + applicator = await Customer.create(mockApplicator()); + client = await Client.create(mockClient(applicator._id)); + + const jobId = nextJobId(); + jobRecord = await Job.create( + mockJob(applicator._id, { + _id: jobId, + client: client._id, + }) + ); + }); + + afterAll(async () => { + await clearCollection(Job); + await clearCollection(App); + await clearCollection(AppFile); + await Customer.deleteMany({ _id: { $in: [applicator._id, client._id] } }); + }); + + const makeReq = (extra = {}) => + mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); + + // ------------------------------------------------------------------------- + describe('getSessions', () => { + it('throws AppParamError when jobId is not a number', async () => { + const req = makeReq({ params: { jobId: 'not-a-number' } }); + const res = mockRes(); + await expect(apiPubCtl.getSessions(req, res)).rejects.toThrow(); + }); + + it('throws JOB_NOT_FOUND for a job that does not exist', async () => { + const req = makeReq({ params: { jobId: '999999' } }); + const res = mockRes(); + await expect(apiPubCtl.getSessions(req, res)).rejects.toThrow(); + }); + + it('throws AppAuthError when uid does not own the job', async () => { + const other = await Customer.create(mockApplicator()); + const req = mockReq({ + uid: other._id, + puid: other._id, + params: { jobId: String(jobRecord._id) }, + }); + const res = mockRes(); + await expect(apiPubCtl.getSessions(req, res)).rejects.toThrow(); + await Customer.deleteMany({ _id: other._id }); + }); + + it('returns empty data array when job has no apps', async () => { + const req = makeReq({ params: { jobId: String(jobRecord._id) } }); + const res = mockRes(); + + await apiPubCtl.getSessions(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.jobId).toBe(jobRecord._id); + expect(res._data.data).toEqual([]); + }); + + it('returns sessions when apps exist for the job', async () => { + const app = await App.create({ + _id: require('mongoose').Types.ObjectId(), + jobId: jobRecord._id, + fileName: 'testflight.agn', + fileSize: 0, + markedDelete: false, + }); + const req = makeReq({ params: { jobId: String(jobRecord._id) } }); + const res = mockRes(); + + await apiPubCtl.getSessions(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.data.length).toBe(1); + expect(String(res._data.data[0].sessionId)).toBe(String(app._id)); + + await App.deleteMany({ _id: app._id }); + }); + }); + + // ------------------------------------------------------------------------- + describe('getAreas', () => { + it('throws when jobId is not a number', async () => { + const req = makeReq({ params: { jobId: 'abc' } }); + const res = mockRes(); + await expect(apiPubCtl.getAreas(req, res)).rejects.toThrow(); + }); + + it('throws JOB_NOT_FOUND when job does not exist', async () => { + const req = makeReq({ params: { jobId: '999998' } }); + const res = mockRes(); + await expect(apiPubCtl.getAreas(req, res)).rejects.toThrow(); + }); + + it('returns a GeoJSON FeatureCollection with empty features when job has no areas', async () => { + const req = makeReq({ params: { jobId: String(jobRecord._id) } }); + const res = mockRes(); + + await apiPubCtl.getAreas(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.type).toBe('FeatureCollection'); + expect(Array.isArray(res._data.features)).toBe(true); + expect(res._data.features.length).toBe(0); + }); + + it('returns area features when sprayAreas are defined on the job', async () => { + const area = { + properties: { name: 'Block A', area: 2.5 }, + geometry: { + type: 'Polygon', + coordinates: [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]], + }, + }; + await Job.findByIdAndUpdate(jobRecord._id, { sprayAreas: [area] }); + + const req = makeReq({ params: { jobId: String(jobRecord._id) } }); + const res = mockRes(); + + await apiPubCtl.getAreas(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.features.length).toBe(1); + expect(res._data.features[0].properties.name).toBe('Block A'); + + // Cleanup + await Job.findByIdAndUpdate(jobRecord._id, { sprayAreas: [] }); + }); + }); + + // ------------------------------------------------------------------------- + describe('getSessionRecords', () => { + it('throws when jobId is not a number', async () => { + const mongoose = require('mongoose'); + const req = makeReq({ + params: { jobId: 'abc', fileId: new mongoose.Types.ObjectId().toHexString() }, + }); + const res = mockRes(); + await expect(apiPubCtl.getSessionRecords(req, res)).rejects.toThrow(); + }); + + it('throws when fileId is not a valid ObjectId', async () => { + const req = makeReq({ + params: { jobId: String(jobRecord._id), fileId: 'invalid' }, + }); + const res = mockRes(); + await expect(apiPubCtl.getSessionRecords(req, res)).rejects.toThrow(); + }); + + it('throws NOT_FOUND when AppFile does not exist', async () => { + const mongoose = require('mongoose'); + const req = makeReq({ + params: { + jobId: String(jobRecord._id), + fileId: new mongoose.Types.ObjectId().toHexString(), + }, + }); + const res = mockRes(); + await expect(apiPubCtl.getSessionRecords(req, res)).rejects.toThrow(); + }); + }); +}); diff --git a/Development/server/tests/integration/billing.integration.test.js b/Development/server/tests/integration/billing.integration.test.js new file mode 100644 index 0000000..6b5ed1b --- /dev/null +++ b/Development/server/tests/integration/billing.integration.test.js @@ -0,0 +1,104 @@ +'use strict'; + +/** + * Integration tests – billing controller (controllers/billing.js) + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockReq, mockRes } = require('./mock_data'); + +let Customer; + +beforeAll(async () => { + await connectDB(); + Customer = require('../../model/customer'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const billingCtl = require('../../controllers/billing'); + +describe('billing controller – getCustUsage_post', () => { + let applicator; + + beforeAll(async () => { + applicator = await Customer.create(mockApplicator()); + }); + + afterAll(async () => { + await Customer.deleteMany({ _id: applicator._id }); + }); + + const makeReq = (bodyOverrides = {}) => + mockReq({ + uid: applicator._id, + puid: applicator._id, + body: { + from: '2024-01-01', + to: '2024-12-31', + tz: 'America/Chicago', + ...bodyOverrides, + }, + }); + + it('throws AppParamError when body is null', async () => { + const req = mockReq({ body: null }); + const res = mockRes(); + await expect(billingCtl.getCustUsage_post(req, res)).rejects.toThrow(); + }); + + it('throws AppParamError when from date is missing', async () => { + const req = makeReq({ from: undefined }); + const res = mockRes(); + await expect(billingCtl.getCustUsage_post(req, res)).rejects.toThrow(); + }); + + it('throws AppParamError when to date is missing', async () => { + const req = makeReq({ to: undefined }); + const res = mockRes(); + await expect(billingCtl.getCustUsage_post(req, res)).rejects.toThrow(); + }); + + it('throws AppParamError when from is an invalid date string', async () => { + const req = makeReq({ from: 'not-a-date' }); + const res = mockRes(); + await expect(billingCtl.getCustUsage_post(req, res)).rejects.toThrow(); + }); + + it('returns an empty array when no customers have jobs in the date range', async () => { + const req = makeReq(); + const res = mockRes(); + + await billingCtl.getCustUsage_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + // No active customers have any jobs in the test DB for this date range + expect(res._data.length).toBe(0); + }); + + it('accepts ISO datetime strings for from/to', async () => { + const req = makeReq({ + from: '2024-01-01T00:00:00.000Z', + to: '2024-12-31T23:59:59.999Z', + }); + const res = mockRes(); + + await billingCtl.getCustUsage_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + + it('billable filter is accepted without error', async () => { + const req = makeReq({ billable: true }); + const res = mockRes(); + + await billingCtl.getCustUsage_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); +}); diff --git a/Development/server/tests/integration/check_controller_contract.js b/Development/server/tests/integration/check_controller_contract.js new file mode 100644 index 0000000..67c18c0 --- /dev/null +++ b/Development/server/tests/integration/check_controller_contract.js @@ -0,0 +1,274 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const ROOT_DIR = path.resolve(__dirname, '..', '..'); +const CONTROLLERS_DIR = path.join(ROOT_DIR, 'controllers'); +const TESTS_DIR = __dirname; + +const EXCLUDED_CONTROLLERS = { + dlq: 'No Jest integration suite yet.', + export: 'Route-wiring controller module; not covered by the current Jest integration pattern.', + geoutil: 'No Jest integration suite yet.', + health: 'No Jest integration suite yet.', + location: 'No Jest integration suite yet.', + subscription: 'No Jest integration suite yet.', + upload_job: 'Route-wiring controller module; not covered by the current Jest integration pattern.', +}; + +const EXCLUDED_METHODS = { + api_export: { + downloadExport: 'File download path is not covered by the current integration suite.', + }, + api_pub: { + getSessionRecords: 'No Jest integration case yet.', + getAreas: 'No Jest integration case yet.', + }, + billing: { + exportUsageDetail_post: 'No Jest integration case yet.', + getCustBillingStatus_get: 'No Jest integration case yet.', + uploadLogo_post: 'No Jest integration case yet.', + deleteLogo_delete: 'No Jest integration case yet.', + }, + client: { + updateClient_put: 'No Jest integration case yet.', + deleteClient: 'Skipped in Jest because the controller requires MongoDB transactions.', + searchWithSetting_post: 'No Jest integration case yet.', + }, + customer: { + createCustomer_post: 'No Jest integration case yet.', + getCustomer_get: 'No Jest integration case yet.', + updateCustomer_put: 'No Jest integration case yet.', + deleteCustomer: 'No Jest integration case yet.', + updateUser_put: 'No Jest integration case yet.', + deleteUser: 'No Jest integration case yet.', + getHierarchyById_get: 'No Jest integration case yet.', + }, + dealer: { + updateUser_put: 'No Jest integration case yet.', + deleteUser: 'No Jest integration case yet.', + }, + geoitem: { + search_post: 'No Jest integration case yet.', + getSharedItems_get: 'No Jest integration case yet.', + }, + invoice: { + createInvoice_post: 'No Jest integration case yet.', + getInvoiceById_get: 'No Jest integration case yet.', + updateInvoice_put: 'No Jest integration case yet.', + updateInvoiceById_put: 'No Jest integration case yet.', + deleteInvoice_delete: 'No Jest integration case yet.', + deleteInvoiceById: 'No Jest integration case yet.', + deleteInvoices: 'No Jest integration case yet.', + getInvoiceJobs_post: 'No Jest integration case yet.', + sendInvoiceEmail_post: 'No Jest integration case yet.', + getPreviewUrl_post: 'No Jest integration case yet.', + getPrintInformation_get: 'No Jest integration case yet.', + exportInvoices_get: 'No Jest integration case yet.', + payment_get: 'No Jest integration case yet.', + }, + invoice_settings: { + getInvoiceSettingDefault_get: 'No Jest integration case yet.', + copyInvoiceSetting_post: 'No Jest integration case yet.', + uploadLogo_post: 'No Jest integration case yet.', + deleteLogo_delete: 'No Jest integration case yet.', + }, + job: { + getData_post: 'No Jest integration case yet.', + getReportOps_get: 'No Jest integration case yet.', + preAppReport_post: 'No Jest integration case yet.', + getRptVars_post: 'No Jest integration case yet.', + setRptVars_post: 'No Jest integration case yet.', + saveReport_post: 'No Jest integration case yet.', + preLoadReport_post: 'No Jest integration case yet.', + getUploadedFiles_post: 'No Jest integration case yet.', + importStatus_post: 'No Jest integration case yet.', + importingStatus_post: 'No Jest integration case yet.', + deleteAppFile_post: 'No Jest integration case yet.', + getJobLogs_post: 'No Jest integration case yet.', + assign_post: 'No Jest integration case yet.', + assignments_post: 'No Jest integration case yet.', + countByClient_post: 'No Jest integration case yet.', + saveMapOps_post: 'No Jest integration case yet.', + appFiles_post: 'No Jest integration case yet.', + filesdata_post: 'No Jest integration case yet.', + getAppDataByJobId: 'No Jest integration case yet.', + fetchInvReadyJobs_post: 'No Jest integration case yet.', + searchJobs_post: 'No Jest integration case yet.', + }, + log_payment: { + createLogPayment_post: 'Skipped in Jest because the controller requires MongoDB transactions.', + createLogPayments_post: 'Skipped in Jest because the controller requires MongoDB transactions.', + }, + main: { + getSiteVer_post: 'No Jest integration case yet.', + doLongOp_post: 'No Jest integration case yet.', + getActivePromos_get: 'No Jest integration case yet.', + getSubscriptionPromos_get: 'No Jest integration case yet.', + setSubscriptionPromos_post: 'No Jest integration case yet.', + addSubscriptionPromo_post: 'No Jest integration case yet.', + deleteSubscriptionPromo_delete: 'No Jest integration case yet.', + updateSubscriptionPromo_put: 'No Jest integration case yet.', + getForeverCoupons_get: 'No Jest integration case yet.', + }, + obstacle: { + updateObstacle_put: 'No Jest integration case yet.', + deleteObstacle: 'No Jest integration case yet.', + }, + partner: { + getPartnerById_post: 'No Jest integration case yet.', + updatePartner_put: 'No Jest integration case yet.', + syncData_post: 'No Jest integration case yet.', + uploadJob_post: 'No Jest integration case yet.', + getSystemUsers_get: 'No Jest integration case yet.', + getCurrentSystemUser_get: 'No Jest integration case yet.', + getSystemUser_get: 'No Jest integration case yet.', + createSystemUser_post: 'No Jest integration case yet.', + updateSystemUser_put: 'No Jest integration case yet.', + updateSystemUser_post: 'No Jest integration case yet.', + deleteSystemUser: 'No Jest integration case yet.', + testPartnerAuth_post: 'No Jest integration case yet.', + getPartnerCustomers_get: 'No Jest integration case yet.', + getPartnerAircraft_get: 'No Jest integration case yet.', + getPartnerCustomerStats_get: 'No Jest integration case yet.', + getPartnerJobStats_get: 'No Jest integration case yet.', + syncPartnerCustomer_post: 'No Jest integration case yet.', + getPartnerServices_get: 'No Jest integration case yet.', + }, + pilot: { + updatePilot_put: 'No Jest integration case yet.', + deletePilot: 'Skipped in Jest because the controller requires MongoDB transactions.', + }, + product: { + search_post: 'No Jest integration assertion tied to a distinct behavior yet.', + }, + user: { + createUser_post: 'No Jest integration case yet.', + deleteUser: 'No Jest integration case yet.', + login_post: 'No Jest integration case yet.', + clearTempData_post: 'No Jest integration case yet.', + setUserLanguage_post: 'No Jest integration case yet.', + getUserDetail_post: 'No Jest integration case yet.', + mailPwdReset_post: 'No Jest integration case yet.', + validateResetPwdToken_post: 'No Jest integration case yet.', + resetPassword_post: 'No Jest integration case yet.', + ensureParentExists: 'Internal helper exported for reuse; not a request handler test target.', + clearTempData: 'Internal helper exported for reuse; not a request handler test target.', + getHostUrlFromReq: 'Internal helper exported for reuse; not a request handler test target.', + requestEmailVerification_post: 'No Jest integration case yet.', + verifyEmailCode_post: 'No Jest integration case yet.', + signup_post: 'No Jest integration case yet.', + }, + vehicle: { + updateVehicle_put: 'No Jest integration case yet.', + deleteVehicle: 'Skipped in Jest because the controller requires MongoDB transactions.', + unitIdExists_post: 'No Jest integration case yet.', + }, +}; + +function stripComments(input) { + return input + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/(^|\s)\/\/.*$/gm, '$1'); +} + +function parseObjectMembers(block) { + return stripComments(block) + .split(',') + .map(part => part.trim()) + .filter(Boolean) + .map(part => part.replace(/[}\s]+$/g, '')) + .map(part => part.split(':')[0].trim()) + .filter(Boolean) + .filter(part => /^[A-Za-z_$][\w$]*$/.test(part)); +} + +function parseExportedMethods(controllerSource) { + const directMatch = controllerSource.match(/module\.exports\s*=\s*\{([\s\S]*?)\}\s*;?\s*$/m); + if (directMatch) return parseObjectMembers(directMatch[1]); + + if (controllerSource.includes('module.exports = function')) { + const returnMatches = [...controllerSource.matchAll(/return\s*\{([\s\S]*?)\}\s*;?/g)]; + if (returnMatches.length > 0) { + return parseObjectMembers(returnMatches[returnMatches.length - 1][1]); + } + } + + return []; +} + +function getCoveredMethods(testSource, methods) { + const covered = new Set(); + for (const method of methods) { + const invocationRegex = new RegExp(`\\.${method}\\s*\\(`); + if (invocationRegex.test(testSource)) covered.add(method); + } + return covered; +} + +function getControllerFiles() { + return fs.readdirSync(CONTROLLERS_DIR) + .filter(fileName => fileName.endsWith('.js')) + .sort(); +} + +function main() { + const failures = []; + const warnings = []; + + for (const fileName of getControllerFiles()) { + const controllerName = path.basename(fileName, '.js'); + if (EXCLUDED_CONTROLLERS[controllerName]) { + warnings.push(`SKIP ${controllerName}: ${EXCLUDED_CONTROLLERS[controllerName]}`); + continue; + } + + const controllerPath = path.join(CONTROLLERS_DIR, fileName); + const testPath = path.join(TESTS_DIR, `${controllerName}.integration.test.js`); + + if (!fs.existsSync(testPath)) { + failures.push(`Missing integration suite for controller '${controllerName}': expected tests/integration/${controllerName}.integration.test.js`); + continue; + } + + const controllerSource = fs.readFileSync(controllerPath, 'utf8'); + const exportedMethods = parseExportedMethods(controllerSource); + if (exportedMethods.length === 0) { + failures.push(`Could not determine exported methods for controller '${controllerName}'.`); + continue; + } + + const testSource = fs.readFileSync(testPath, 'utf8'); + const coveredMethods = getCoveredMethods(testSource, exportedMethods); + const excludedMethods = EXCLUDED_METHODS[controllerName] || {}; + + for (const method of exportedMethods) { + if (coveredMethods.has(method)) continue; + if (excludedMethods[method]) { + warnings.push(`SKIP ${controllerName}.${method}: ${excludedMethods[method]}`); + continue; + } + + failures.push( + `Missing integration test coverage for ${controllerName}.${method}: add at least one test that invokes this exported method in tests/integration/${controllerName}.integration.test.js` + ); + } + } + + if (warnings.length > 0) { + console.log('Integration contract exclusions:'); + for (const warning of warnings) console.log(`- ${warning}`); + console.log(''); + } + + if (failures.length > 0) { + console.error('Integration controller contract check failed:'); + for (const failure of failures) console.error(`- ${failure}`); + process.exit(1); + } + + console.log('Integration controller contract check passed.'); +} + +main(); \ No newline at end of file diff --git a/Development/server/tests/integration/client.integration.test.js b/Development/server/tests/integration/client.integration.test.js new file mode 100644 index 0000000..9f01d53 --- /dev/null +++ b/Development/server/tests/integration/client.integration.test.js @@ -0,0 +1,113 @@ +'use strict'; + +/** + * Integration tests – client controller (controllers/client.js) + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockClient, mockReq, mockRes, newId } = require('./mock_data'); + +let Client, Customer; + +beforeAll(async () => { + await connectDB(); + Customer = require('../../model/customer'); + Client = require('../../model/client'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const clientCtl = require('../../controllers/client'); + +describe('client controller – data methods', () => { + let applicator; + + beforeAll(async () => { + await clearCollection(Client); + applicator = await Customer.create(mockApplicator()); + }); + + afterAll(async () => { + await clearCollection(Client); + await Customer.deleteMany({ _id: applicator._id }); + }); + + const makeReq = (extra = {}) => + mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); + + // ------------------------------------------------------------------------- + describe('create_post', () => { + it('creates a client and returns it', async () => { + const req = makeReq({ body: mockClient(applicator._id) }); + const res = mockRes(); + + await clientCtl.createClient_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data._id).toBeDefined(); + expect(res._data.name).toBe('Test Client'); + }); + + it('throws when body is missing', async () => { + const req = makeReq({ body: null }); + const res = mockRes(); + await expect(clientCtl.createClient_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('getById_get', () => { + let clientId; + + beforeAll(async () => { + const doc = await Client.create(mockClient(applicator._id)); + clientId = String(doc._id); + }); + + it('returns client by id', async () => { + const req = makeReq({ params: { id: clientId } }); + const res = mockRes(); + + await clientCtl.getClient_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(String(res._data._id)).toBe(clientId); + }); + }); + + // ------------------------------------------------------------------------- + describe('search_post', () => { + it('returns matching clients', async () => { + const req = makeReq({ body: { byPuid: applicator._id } }); + const res = mockRes(); + + await clientCtl.search_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('delete_delete', () => { + let clientId; + + beforeAll(async () => { + const doc = await Client.create(mockClient(applicator._id)); + clientId = String(doc._id); + }); + + it.skip('soft-deletes a client (skipped: requires MongoDB replica-set for transactions)', async () => { + const req = makeReq({ params: { id: clientId } }); + const res = mockRes(); + + await clientCtl.deleteClient(req, res); + + expect(res.json).toHaveBeenCalled(); + const found = await Client.findById(clientId); + expect(!found || found.markedDelete).toBeTruthy(); + }); + }); +}); diff --git a/Development/server/tests/integration/common.integration.test.js b/Development/server/tests/integration/common.integration.test.js new file mode 100644 index 0000000..c6b62a0 --- /dev/null +++ b/Development/server/tests/integration/common.integration.test.js @@ -0,0 +1,67 @@ +'use strict'; + +/** + * Integration tests – common controller (controllers/common.js) + * + * Covers: getCountries_get + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockReq, mockRes } = require('./mock_data'); + +let Country; + +beforeAll(async () => { + await connectDB(); + Country = require('../../model/country'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const commonCtl = require('../../controllers/common'); + +describe('common controller – data methods', () => { + beforeAll(async () => { + await clearCollection(Country); + await Country.create([ + { code: 'US', name: 'United States' }, + { code: 'CA', name: 'Canada' }, + ]); + }); + + afterAll(async () => { + await clearCollection(Country); + }); + + describe('getCountries_get', () => { + it('returns an array of countries with code and name', async () => { + const req = mockReq(); + const res = mockRes(); + + await commonCtl.getCountries_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + expect(res._data.length).toBeGreaterThanOrEqual(2); + const us = res._data.find(c => c.code === 'US'); + expect(us).toBeDefined(); + expect(us.name).toBe('United States'); + // _id should be excluded per the controller projection + expect(us._id).toBeUndefined(); + }); + + it('returns an empty array when no countries exist', async () => { + await clearCollection(Country); + + const req = mockReq(); + const res = mockRes(); + + await commonCtl.getCountries_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toEqual([]); + }); + }); +}); diff --git a/Development/server/tests/integration/costing_items.integration.test.js b/Development/server/tests/integration/costing_items.integration.test.js new file mode 100644 index 0000000..7100919 --- /dev/null +++ b/Development/server/tests/integration/costing_items.integration.test.js @@ -0,0 +1,143 @@ +'use strict'; + +/** + * Integration tests – costing_items controller (controllers/costing_items.js) + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockCostingItem, mockReq, mockRes, newId } = require('./mock_data'); + +let CostingItem, Customer; + +beforeAll(async () => { + await connectDB(); + CostingItem = require('../../model/costing_items'); + Customer = require('../../model/customer'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const costingCtl = require('../../controllers/costing_items'); + +describe('costing_items controller – data methods', () => { + let applicator; + + beforeAll(async () => { + await clearCollection(CostingItem); + applicator = await Customer.create(mockApplicator()); + }); + + afterAll(async () => { + await clearCollection(CostingItem); + await Customer.deleteMany({ _id: applicator._id }); + }); + + const makeReq = (extra = {}) => + mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); + + // ------------------------------------------------------------------------- + describe('create_post', () => { + it('creates a costing item and returns it', async () => { + const req = makeReq({ body: mockCostingItem(applicator._id, applicator._id) }); + const res = mockRes(); + + await costingCtl.createCostingItem_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data._id).toBeDefined(); + expect(res._data.name).toMatch(/CostItem-/); + }); + + it('throws when body is missing', async () => { + const req = makeReq({ body: null }); + const res = mockRes(); + await expect(costingCtl.createCostingItem_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('list_get', () => { + it('returns costing items for the applicator', async () => { + const req = makeReq({ query: {} }); + const res = mockRes(); + + await costingCtl.getCostingItems_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + expect(res._data.length).toBeGreaterThan(0); + }); + }); + + // ------------------------------------------------------------------------- + describe('getById_get', () => { + let itemId; + + beforeAll(async () => { + const doc = await CostingItem.create(mockCostingItem(applicator._id, applicator._id)); + itemId = String(doc._id); + }); + + it('returns costing item by id', async () => { + const req = makeReq({ params: { costingItemId: itemId } }); + const res = mockRes(); + + await costingCtl.getCostingItemDetail_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(String(res._data._id)).toBe(itemId); + }); + + it('throws when id is invalid', async () => { + const req = makeReq({ params: { costingItemId: 'bad-id' } }); + const res = mockRes(); + await expect(costingCtl.getCostingItemDetail_get(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('update_put', () => { + let itemId; + + beforeAll(async () => { + const doc = await CostingItem.create(mockCostingItem(applicator._id, applicator._id)); + itemId = String(doc._id); + }); + + it('updates a costing item', async () => { + const req = makeReq({ + params: { costingItemId: itemId }, + body: { name: 'Updated Cost Item', price: 20.0 }, + }); + const res = mockRes(); + + await costingCtl.updateCostingItem_put(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.name).toBe('Updated Cost Item'); + }); + }); + + // ------------------------------------------------------------------------- + describe('delete_delete', () => { + let itemId; + + beforeAll(async () => { + const doc = await CostingItem.create(mockCostingItem(applicator._id, applicator._id)); + itemId = String(doc._id); + }); + + it('deletes a costing item', async () => { + const req = makeReq({ params: { costingItemId: itemId } }); + const res = mockRes(); + + await costingCtl.deleteCostingItem(req, res); + + expect(res.json).toHaveBeenCalled(); + const found = await CostingItem.findById(itemId); + expect(found).toBeNull(); + }); + }); +}); diff --git a/Development/server/tests/integration/crop.integration.test.js b/Development/server/tests/integration/crop.integration.test.js new file mode 100644 index 0000000..c3b1734 --- /dev/null +++ b/Development/server/tests/integration/crop.integration.test.js @@ -0,0 +1,155 @@ +'use strict'; + +/** + * Integration tests – crop controller (controllers/crop.js) + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockCrop, mockReq, mockRes, newId } = require('./mock_data'); + +let Crop, Customer; + +beforeAll(async () => { + await connectDB(); + Crop = require('../../model/crop'); + Customer = require('../../model/customer'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const cropCtl = require('../../controllers/crop'); + +describe('crop controller – data methods', () => { + let applicator; + + beforeAll(async () => { + await clearCollection(Crop); + applicator = await Customer.create(mockApplicator()); + }); + + afterAll(async () => { + await clearCollection(Crop); + await Customer.deleteMany({ _id: applicator._id }); + }); + + const makeReq = (extra = {}) => + mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); + + // ------------------------------------------------------------------------- + describe('create_post', () => { + it('creates a crop and returns it', async () => { + const req = makeReq({ body: mockCrop(applicator._id) }); + const res = mockRes(); + + await cropCtl.createCrop_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data._id).toBeDefined(); + expect(res._data.name).toMatch(/Crop-/); + }); + + it('throws when body is missing', async () => { + const req = makeReq({ body: null }); + const res = mockRes(); + await expect(cropCtl.createCrop_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('getAll_get', () => { + it('returns crops for the applicator', async () => { + const req = makeReq({ query: {} }); + const res = mockRes(); + + await cropCtl.getCrops_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('getById_get', () => { + let cropId; + + beforeAll(async () => { + const doc = await Crop.create(mockCrop(applicator._id)); + cropId = String(doc._id); + }); + + it('returns a crop by id', async () => { + const req = makeReq({ params: { crop_id: cropId } }); + const res = mockRes(); + + await cropCtl.getCrop_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(String(res._data._id)).toBe(cropId); + }); + + it('throws when id is invalid', async () => { + const req = makeReq({ params: { crop_id: 'bad-id' } }); + const res = mockRes(); + await expect(cropCtl.getCrop_get(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('update_put', () => { + let cropId; + + beforeAll(async () => { + const doc = await Crop.create(mockCrop(applicator._id)); + cropId = String(doc._id); + }); + + it('updates a crop', async () => { + const req = makeReq({ + params: { crop_id: cropId }, + body: { name: 'Updated Crop' }, + }); + const res = mockRes(); + + await cropCtl.updateCrop_put(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.name).toBe('Updated Crop'); + }); + }); + + // ------------------------------------------------------------------------- + describe('search_post', () => { + it('returns matching crops', async () => { + const req = makeReq({ body: { byUserId: applicator._id } }); + const res = mockRes(); + + await cropCtl.search_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('delete_delete', () => { + let cropId; + + beforeAll(async () => { + const doc = await Crop.create(mockCrop(applicator._id)); + cropId = String(doc._id); + }); + + it('deletes a crop', async () => { + const req = makeReq({ params: { crop_id: cropId } }); + const res = mockRes(); + + await cropCtl.deleteCrop(req, res); + + expect(res.json).toHaveBeenCalled(); + const found = await Crop.findById(cropId); + expect(found).toBeNull(); + }); + }); +}); diff --git a/Development/server/tests/integration/customer.integration.test.js b/Development/server/tests/integration/customer.integration.test.js new file mode 100644 index 0000000..bcebe60 --- /dev/null +++ b/Development/server/tests/integration/customer.integration.test.js @@ -0,0 +1,75 @@ +'use strict'; + +/** + * Integration tests – customer controller (controllers/customer.js) + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockClient, mockReq, mockRes, newId } = require('./mock_data'); + +let Customer, Client; + +beforeAll(async () => { + await connectDB(); + Customer = require('../../model/customer'); + Client = require('../../model/client'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const customerCtl = require('../../controllers/customer'); + +describe('customer controller – data methods', () => { + let applicator, client; + + beforeAll(async () => { + await clearCollection(Client); + applicator = await Customer.create(mockApplicator()); + client = await Client.create(mockClient(applicator._id)); + }); + + afterAll(async () => { + await clearCollection(Client); + await Customer.deleteMany({ _id: applicator._id }); + }); + + const makeReq = (extra = {}) => + mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); + + // ------------------------------------------------------------------------- + describe('getCustomers_get', () => { + it('returns aggregated customer list for applicator', async () => { + const req = makeReq({ query: {} }); + const res = mockRes(); + + await customerCtl.getCustomers_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + + it('filters by name when provided', async () => { + const filters = JSON.stringify({ name: { value: 'Test Applicator', valueOperator: 'contains' } }); + const req = makeReq({ query: { filters } }); + const res = mockRes(); + + await customerCtl.getCustomers_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.length).toBeGreaterThan(0); + }); + + it('returns empty when name does not match', async () => { + const filters = JSON.stringify({ name: { value: 'NONEXISTENT_CLIENT_XYZ', valueOperator: 'contains' } }); + const req = makeReq({ query: { filters } }); + const res = mockRes(); + + await customerCtl.getCustomers_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.length).toBe(0); + }); + }); +}); diff --git a/Development/server/tests/integration/dealer.integration.test.js b/Development/server/tests/integration/dealer.integration.test.js new file mode 100644 index 0000000..adcfd1e --- /dev/null +++ b/Development/server/tests/integration/dealer.integration.test.js @@ -0,0 +1,202 @@ +'use strict'; + +/** + * Integration tests – dealer controller (controllers/dealer.js) + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockReq, mockRes } = require('./mock_data'); + +let Dealer; + +beforeAll(async () => { + await connectDB(); + Dealer = require('../../model/dealer'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const dealerCtl = require('../../controllers/dealer'); + +function mockDealer(overrides = {}) { + return { + companyName: `Dealer-${Date.now()}`, + country: 'US', + contactName: 'John Smith', + address: '789 Dealer Rd', + phone: '+15550003000', + email: `dealer_${Date.now()}@test.com`, + ...overrides, + }; +} + +describe('dealer controller – data methods', () => { + beforeAll(async () => { + await clearCollection(Dealer); + }); + + afterAll(async () => { + await clearCollection(Dealer); + }); + + // ------------------------------------------------------------------------- + describe('getDealers_get', () => { + it('returns an empty array when no dealers exist', async () => { + const req = mockReq(); + const res = mockRes(); + + await dealerCtl.getDealers_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + expect(res._data.length).toBe(0); + }); + + it('returns dealers sorted by country then companyName', async () => { + await Dealer.create([ + mockDealer({ companyName: 'Zulu Ag', country: 'CA' }), + mockDealer({ companyName: 'Alpha Ag', country: 'US' }), + mockDealer({ companyName: 'Beta Ag', country: 'CA' }), + ]); + + const req = mockReq(); + const res = mockRes(); + + await dealerCtl.getDealers_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.length).toBe(3); + // First two entries should be CA (sorted by country first) + expect(res._data[0].country).toBe('CA'); + expect(res._data[1].country).toBe('CA'); + // Within CA, sorted by companyName: Beta < Zulu + expect(res._data[0].companyName).toBe('Beta Ag'); + expect(res._data[1].companyName).toBe('Zulu Ag'); + }); + }); + + // ------------------------------------------------------------------------- + describe('createDealer_post', () => { + it('creates a dealer and returns the saved document', async () => { + const req = mockReq({ body: mockDealer() }); + const res = mockRes(); + + await dealerCtl.createDealer_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data._id).toBeDefined(); + expect(res._data.country).toBe('US'); + }); + + it('strips the _id from input so Mongo assigns its own', async () => { + const mongoose = require('mongoose'); + const injectedId = new mongoose.Types.ObjectId(); + const req = mockReq({ body: mockDealer({ _id: injectedId }) }); + const res = mockRes(); + + await dealerCtl.createDealer_post(req, res); + + expect(res.json).toHaveBeenCalled(); + // The returned _id must differ from the injected one because createDealer_post deletes it + expect(String(res._data._id)).not.toBe(String(injectedId)); + }); + + it('throws when body is null', async () => { + const req = mockReq({ body: null }); + const res = mockRes(); + await expect(dealerCtl.createDealer_post(req, res)).rejects.toThrow(); + }); + + it('throws when companyName is missing', async () => { + const req = mockReq({ body: { country: 'US' } }); + const res = mockRes(); + await expect(dealerCtl.createDealer_post(req, res)).rejects.toThrow(); + }); + + it('throws when country is missing', async () => { + const req = mockReq({ body: { companyName: 'Test Dealer' } }); + const res = mockRes(); + await expect(dealerCtl.createDealer_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('updateDealer_put', () => { + let dealerId; + + beforeAll(async () => { + const doc = await Dealer.create(mockDealer()); + dealerId = String(doc._id); + }); + + it('updates a dealer and returns the updated document', async () => { + const req = mockReq({ + params: { id: dealerId }, + body: { contactName: 'Jane Doe' }, + }); + const res = mockRes(); + + await dealerCtl.updateDealer_put(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.contactName).toBe('Jane Doe'); + expect(String(res._data._id)).toBe(dealerId); + }); + + it('throws when id param is missing', async () => { + const req = mockReq({ params: {}, body: { contactName: 'Jane Doe' } }); + const res = mockRes(); + await expect(dealerCtl.updateDealer_put(req, res)).rejects.toThrow(); + }); + + it('throws when id does not match any dealer', async () => { + const mongoose = require('mongoose'); + const req = mockReq({ + params: { id: new mongoose.Types.ObjectId().toHexString() }, + body: { contactName: 'Nobody' }, + }); + const res = mockRes(); + await expect(dealerCtl.updateDealer_put(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('deleteDealer_delete', () => { + let dealerId; + + beforeAll(async () => { + const doc = await Dealer.create(mockDealer()); + dealerId = String(doc._id); + }); + + it('deletes a dealer and returns { ok: true }', async () => { + const req = mockReq({ params: { id: dealerId } }); + const res = mockRes(); + + await dealerCtl.deleteDealer_delete(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toEqual({ ok: true }); + + const found = await Dealer.findById(dealerId); + expect(found).toBeNull(); + }); + + it('throws when id param is missing', async () => { + const req = mockReq({ params: {} }); + const res = mockRes(); + await expect(dealerCtl.deleteDealer_delete(req, res)).rejects.toThrow(); + }); + + it('throws when id does not match any dealer', async () => { + const mongoose = require('mongoose'); + const req = mockReq({ + params: { id: new mongoose.Types.ObjectId().toHexString() }, + }); + const res = mockRes(); + await expect(dealerCtl.deleteDealer_delete(req, res)).rejects.toThrow(); + }); + }); +}); diff --git a/Development/server/tests/integration/geoitem.integration.test.js b/Development/server/tests/integration/geoitem.integration.test.js new file mode 100644 index 0000000..2f608e8 --- /dev/null +++ b/Development/server/tests/integration/geoitem.integration.test.js @@ -0,0 +1,189 @@ +'use strict'; + +/** + * Integration tests – geoitem controller (controllers/geoitem.js) + */ + +const mongoose = require('mongoose'); +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockClient, mockReq, mockRes, newId } = require('./mock_data'); + +let Area, Customer, Client; + +beforeAll(async () => { + await connectDB(); + Area = require('../../model/area'); + Customer = require('../../model/customer'); + Client = require('../../model/client'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const geoitemCtl = require('../../controllers/geoitem'); + +/** Minimal valid GeoJSON Polygon coordinates (a small square) */ +function squareCoords() { + return [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]]; +} + +function mockArea(clientId, overrides = {}) { + return { + properties: { + name: `Area-${Date.now()}`, + type: 0, + color: '#FF0000', + area: 1.5, + }, + geometry: { + type: 'Polygon', + coordinates: squareCoords(), + }, + client: clientId, + ...overrides, + }; +} + +describe('geoitem controller – data methods', () => { + let applicator, client; + + beforeAll(async () => { + await clearCollection(Area); + applicator = await Customer.create(mockApplicator()); + client = await Client.create(mockClient(applicator._id)); + }); + + afterAll(async () => { + await clearCollection(Area); + await Customer.deleteMany({ _id: { $in: [applicator._id, client._id] } }); + }); + + const makeReq = (extra = {}) => + mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); + + // ------------------------------------------------------------------------- + describe('findAreas_post', () => { + it('throws when clientId is missing from body', async () => { + const req = makeReq({ body: {} }); + const res = mockRes(); + await expect(geoitemCtl.findAreas_post(req, res)).rejects.toThrow(); + }); + + it('throws when clientId is not a valid ObjectId', async () => { + const req = makeReq({ body: { clientId: 'not-an-id' } }); + const res = mockRes(); + await expect(geoitemCtl.findAreas_post(req, res)).rejects.toThrow(); + }); + + it('returns an empty array when no areas exist for the client', async () => { + const req = makeReq({ body: { clientId: client._id.toHexString() } }); + const res = mockRes(); + + await geoitemCtl.findAreas_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + expect(res._data.length).toBe(0); + }); + + it('returns areas belonging to the specified client', async () => { + await Area.create(mockArea(client._id)); + await Area.create(mockArea(client._id)); + + const req = makeReq({ body: { clientId: client._id.toHexString() } }); + const res = mockRes(); + + await geoitemCtl.findAreas_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.length).toBe(2); + // toGeoItems output shape: { _id, properties, geometry, client } + expect(res._data[0]).toHaveProperty('_id'); + expect(res._data[0]).toHaveProperty('geometry'); + }); + + it('does not return areas belonging to a different client', async () => { + const otherClient = await Client.create(mockClient(applicator._id)); + await Area.create(mockArea(otherClient._id)); + + const req = makeReq({ body: { clientId: client._id.toHexString() } }); + const res = mockRes(); + + await geoitemCtl.findAreas_post(req, res); + + expect(res.json).toHaveBeenCalled(); + // Should still only see 2 (created in previous test) + const clientIds = res._data.map(f => String(f.properties?.client ?? '')); + expect(clientIds.every(id => id !== String(otherClient._id) || id === '')).toBe(true); + + await Client.deleteMany({ _id: otherClient._id }); + }); + }); + + // ------------------------------------------------------------------------- + describe('update_post', () => { + it('returns immediately when all update arrays are empty', async () => { + const req = makeReq({ body: { c: [], u: [], r: [] } }); + const res = mockRes(); + + await geoitemCtl.update_post(req, res); + + expect(res.end).toHaveBeenCalled(); + }); + + it('returns immediately when body is null', async () => { + const req = makeReq({ body: null }); + const res = mockRes(); + + await geoitemCtl.update_post(req, res); + + expect(res.end).toHaveBeenCalled(); + }); + + it('inserts new areas via the c (create) array', async () => { + const area = mockArea(client._id); + const req = makeReq({ body: { c: [area] } }); + const res = mockRes(); + + await geoitemCtl.update_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toEqual({ ok: true }); + + const found = await Area.find({ client: client._id }); + expect(found.length).toBeGreaterThan(0); + }); + + it('removes areas via the r (remove) array', async () => { + const doc = await Area.create(mockArea(client._id)); + const countBefore = await Area.countDocuments({ client: client._id }); + + const req = makeReq({ body: { r: [String(doc._id)] } }); + const res = mockRes(); + + await geoitemCtl.update_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toEqual({ ok: true }); + + const countAfter = await Area.countDocuments({ client: client._id }); + expect(countAfter).toBe(countBefore - 1); + }); + }); + + // ------------------------------------------------------------------------- + describe('addToLibrary_post', () => { + it('throws when areas array is empty', async () => { + const req = makeReq({ body: { areas: [] } }); + const res = mockRes(); + await expect(geoitemCtl.addToLibrary_post(req, res)).rejects.toThrow(); + }); + + it('throws when body has no areas field', async () => { + const req = makeReq({ body: {} }); + const res = mockRes(); + await expect(geoitemCtl.addToLibrary_post(req, res)).rejects.toThrow(); + }); + }); +}); diff --git a/Development/server/tests/integration/invoice.integration.test.js b/Development/server/tests/integration/invoice.integration.test.js new file mode 100644 index 0000000..fb50a53 --- /dev/null +++ b/Development/server/tests/integration/invoice.integration.test.js @@ -0,0 +1,128 @@ +'use strict'; + +/** + * Integration tests – Invoice controller (controllers/invoice.js) + * + * We focus on the read path (getInvoices_get) which does not require the full + * complex invoice-creation pipeline (jobs, clients, Stripe, etc.). + * A seeded Invoice document is created directly via the model for query tests. + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockClient, mockReq, mockRes, newId } = require('./mock_data'); + +let Invoice, Customer, Client; + +beforeAll(async () => { + await connectDB(); + Invoice = require('../../model/invoice'); + Customer = require('../../model/customer'); + Client = require('../../model/client'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const invoiceCtl = require('../../controllers/invoice'); + +describe('Invoice controller – data methods', () => { + let applicator, client; + + function makeInvoiceDoc(puid, clientId) { + return { + code: `INV-${Date.now()}`, + companyName: 'Test Applicator Co.', + address: '123 Main St', + currency: 'USD', + dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), + openDate: new Date(), + status: 'open', + paymentTerm: 30, + byPuid: puid, + clients: [ + { + billTo: clientId, + code: `INV-${Date.now()}-1`, + split: '100', + subTotal: '100.00', + }, + ], + jobs: [], + }; + } + + beforeAll(async () => { + await clearCollection(Invoice); + applicator = await Customer.create(mockApplicator()); + client = await Client.create(mockClient(applicator._id)); + }); + + afterAll(async () => { + await clearCollection(Invoice); + await Client.deleteMany({ _id: client._id }); + await Customer.deleteMany({ _id: applicator._id }); + }); + + const makeReq = (extra = {}) => + mockReq({ + uid: applicator._id, + puid: applicator._id, + ut: '1', + userInfo: { + puid: applicator._id, + kind: '1', + premium: 0, + membership: null, + markedDelete: false, + }, + ...extra, + }); + + // ------------------------------------------------------------------------- + describe('getInvoices_get', () => { + beforeAll(async () => { + await Invoice.create(makeInvoiceDoc(applicator._id, client._id)); + }); + + it('returns invoices for the applicator', async () => { + const req = makeReq({ query: {} }); + const res = mockRes(); + + await invoiceCtl.getInvoices_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + expect(res._data.length).toBeGreaterThan(0); + }); + + it('returns empty array when no invoices match filter', async () => { + const filters = JSON.stringify({ code: { value: 'NONEXISTENT_CODE_12345', valueOperator: 'contains' } }); + const req = makeReq({ query: { filters } }); + const res = mockRes(); + + await invoiceCtl.getInvoices_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.length).toBe(0); + }); + + it('throws AppParamError when applicator puid is invalid', async () => { + const req = mockReq({ + uid: newId(), + puid: 'invalid-puid', + ut: '1', + userInfo: { + puid: 'invalid-puid', + kind: '1', + premium: 0, + membership: null, + markedDelete: false, + }, + }); + const res = mockRes(); + + await expect(invoiceCtl.getInvoices_get(req, res)).rejects.toThrow(); + }); + }); +}); diff --git a/Development/server/tests/integration/invoice_settings.integration.test.js b/Development/server/tests/integration/invoice_settings.integration.test.js new file mode 100644 index 0000000..97fd2d7 --- /dev/null +++ b/Development/server/tests/integration/invoice_settings.integration.test.js @@ -0,0 +1,143 @@ +'use strict'; + +/** + * Integration tests – invoice_settings controller (controllers/invoice_settings.js) + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockClient, mockInvoiceSetting, mockReq, mockRes, newId } = require('./mock_data'); + +let InvoiceSetting, Customer, Client; + +beforeAll(async () => { + await connectDB(); + InvoiceSetting = require('../../model/invoice_settings'); + Customer = require('../../model/customer'); + Client = require('../../model/client'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const invSettingsCtl = require('../../controllers/invoice_settings'); + +describe('invoice_settings controller – data methods', () => { + let applicator, client; + + beforeAll(async () => { + await clearCollection(InvoiceSetting); + applicator = await Customer.create(mockApplicator()); + client = await Client.create(mockClient(applicator._id)); + }); + + afterAll(async () => { + await clearCollection(InvoiceSetting); + await Client.deleteMany({ _id: client._id }); + await Customer.deleteMany({ _id: applicator._id }); + }); + + const makeReq = (extra = {}) => + mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); + + // ------------------------------------------------------------------------- + describe('createInvoiceSetting_post', () => { + it('creates an invoice setting and returns it', async () => { + const req = makeReq({ body: mockInvoiceSetting(applicator._id, client._id) }); + const res = mockRes(); + + await invSettingsCtl.createInvoiceSetting_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data._id).toBeDefined(); + expect(res._data.companyName).toBe('Test Applicator Co.'); + }); + }); + + // ------------------------------------------------------------------------- + describe('getInvoiceSettings_get', () => { + it('returns invoice settings for the applicator', async () => { + const req = makeReq({ query: {} }); + const res = mockRes(); + + await invSettingsCtl.getInvoiceSettings_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + expect(res._data.length).toBeGreaterThan(0); + }); + }); + + // ------------------------------------------------------------------------- + describe('getInvoiceSettingDetail_get', () => { + let settingId; + + beforeAll(async () => { + await clearCollection(InvoiceSetting); + const doc = await InvoiceSetting.create(mockInvoiceSetting(applicator._id, client._id)); + settingId = String(doc._id); + }); + + it('returns invoice setting by id', async () => { + const req = makeReq({ params: { invoiceSettingId: settingId } }); + const res = mockRes(); + + await invSettingsCtl.getInvoiceSettingDetail_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(String(res._data._id)).toBe(settingId); + }); + + it('throws when id is invalid', async () => { + const req = makeReq({ params: { invoiceSettingId: 'bad-id' } }); + const res = mockRes(); + await expect(invSettingsCtl.getInvoiceSettingDetail_get(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('updateInvoiceSetting_put', () => { + let settingId; + + beforeAll(async () => { + await clearCollection(InvoiceSetting); + const doc = await InvoiceSetting.create(mockInvoiceSetting(applicator._id, client._id)); + settingId = String(doc._id); + }); + + it('updates an invoice setting', async () => { + const req = makeReq({ + params: { invoiceSettingId: settingId }, + body: { companyName: 'Updated Co.', paymentTerm: 45 }, + }); + const res = mockRes(); + + await invSettingsCtl.updateInvoiceSetting_put(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.companyName).toBe('Updated Co.'); + }); + }); + + // ------------------------------------------------------------------------- + describe('deleteInvoiceSetting', () => { + let settingId; + + beforeAll(async () => { + await clearCollection(InvoiceSetting); + const doc = await InvoiceSetting.create(mockInvoiceSetting(applicator._id, client._id)); + settingId = String(doc._id); + }); + + it('deletes an invoice setting', async () => { + const req = makeReq({ params: { invoiceSettingId: settingId } }); + const res = mockRes(); + + await invSettingsCtl.deleteInvoiceSetting(req, res); + + expect(res.json).toHaveBeenCalled(); + const found = await InvoiceSetting.findById(settingId); + expect(found).toBeNull(); + }); + }); +}); diff --git a/Development/server/tests/integration/jest.setup.js b/Development/server/tests/integration/jest.setup.js new file mode 100644 index 0000000..6da01a3 --- /dev/null +++ b/Development/server/tests/integration/jest.setup.js @@ -0,0 +1,65 @@ +'use strict'; + +/** + * Jest integration test setup. + * Uses an in-memory MongoDB instance (mongodb-memory-server) so tests run + * without any external database credentials or infrastructure. + * + * TOKEN_SECRET may optionally be set in the environment; a safe default + * is used when it is absent (tests only, never production). + */ + +// Ensure NODE_ENV=test so application code skips production-only paths. +process.env.NODE_ENV = 'test'; +// Provide a default token secret for JWT signing in tests. +if (!process.env.TOKEN_SECRET) { + process.env.TOKEN_SECRET = 'test-secret-not-for-production'; +} + +const { MongoMemoryServer } = require('mongodb-memory-server'); +const mongoose = require('mongoose'); + +let mongod = null; + +/** + * Start the in-memory MongoDB server and connect mongoose. + * Call this inside beforeAll() in each test file. + */ +async function connectDB() { + if (mongoose.connection.readyState !== 0) return; // already connected + + mongod = await MongoMemoryServer.create(); + const uri = mongod.getUri(); + + mongoose.set('strictPopulate', false); + await mongoose.connect(uri, { + connectTimeoutMS: 15000, + socketTimeoutMS: 30000, + }); + + // Register all discriminators in the correct order (user → sub-types). + // Require-ordering matters – base model first. + require('../../model'); +} + +/** + * Disconnect mongoose and stop the in-memory server. + * Call this inside afterAll() in each test file. + */ +async function disconnectDB() { + await mongoose.disconnect(); + if (mongod) { + await mongod.stop(); + mongod = null; + } +} + +/** + * Remove all documents from a collection. + * Call this inside beforeEach() / afterEach() to keep tests isolated. + */ +async function clearCollection(model) { + await model.deleteMany({}); +} + +module.exports = { connectDB, disconnectDB, clearCollection }; diff --git a/Development/server/tests/integration/job.integration.test.js b/Development/server/tests/integration/job.integration.test.js new file mode 100644 index 0000000..5888629 --- /dev/null +++ b/Development/server/tests/integration/job.integration.test.js @@ -0,0 +1,213 @@ +'use strict'; + +/** + * Integration tests – job controller (controllers/job.js) + * + * The job controller is a factory: require('../../controllers/job')({}) + * Job._id is an auto-increment numeric field. + * deleteJob wraps transaction errors with try/catch so it works on + * standalone MongoDB (no replica-set needed). + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { + mockApplicator, + mockClient, + mockPilot, + mockVehicle, + mockJob, + mockReq, + mockRes, + newId, +} = require('./mock_data'); + +let Job, Customer, Client, Pilot, Vehicle; + +beforeAll(async () => { + await connectDB(); + Customer = require('../../model/customer'); + Client = require('../../model/client'); + Pilot = require('../../model/pilot'); + Vehicle = require('../../model/vehicle'); + Job = require('../../model/job'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +// Job controller is a factory +const jobCtl = require('../../controllers/job')({}); + +describe('job controller – data methods', () => { + let applicator, client, pilot, vehicle; + + beforeAll(async () => { + await clearCollection(Job); + applicator = await Customer.create(mockApplicator()); + client = await Client.create(mockClient(applicator._id)); + pilot = await Pilot.create(mockPilot(applicator._id)); + vehicle = await Vehicle.create(mockVehicle(applicator._id)); + }); + + afterAll(async () => { + await clearCollection(Job); + await Client.deleteMany({ _id: client._id }); + await Pilot.deleteMany({ _id: pilot._id }); + await Vehicle.deleteMany({ _id: vehicle._id }); + await Customer.deleteMany({ _id: applicator._id }); + }); + + const makeReq = (extra = {}) => + mockReq({ + uid: applicator._id, + puid: applicator._id, + ut: '1', + userInfo: { + puid: applicator._id, + kind: '1', + premium: 0, + membership: null, + markedDelete: false, + }, + ...extra, + }); + + // ------------------------------------------------------------------------- + describe('createJob_post', () => { + it('creates a job and returns it', async () => { + const body = mockJob(applicator._id); + body.client = client._id; + body.pilot = pilot._id; + body.vehicle = vehicle._id; + + const req = makeReq({ body }); + const res = mockRes(); + + await jobCtl.createJob_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data._id).toBeDefined(); + expect(typeof res._data._id).toBe('number'); + }); + + it('throws when puid is missing', async () => { + const req = makeReq({ puid: undefined, body: mockJob(applicator._id) }); + const res = mockRes(); + await expect(jobCtl.createJob_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('getJobs_get', () => { + it('returns jobs for the applicator', async () => { + const req = makeReq({ query: {} }); + const res = mockRes(); + + await jobCtl.getJobs_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + expect(res._data.length).toBeGreaterThan(0); + }); + + it('returns empty array when puid has no jobs', async () => { + const unknownId = newId(); + const req = mockReq({ + uid: unknownId, + puid: unknownId, + ut: '1', + userInfo: { puid: unknownId, kind: '1', premium: 0, membership: null, markedDelete: false }, + query: {}, + }); + const res = mockRes(); + + await jobCtl.getJobs_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.length).toBe(0); + }); + }); + + // ------------------------------------------------------------------------- + describe('getJob_get', () => { + let jobId; + + beforeAll(async () => { + const body = mockJob(applicator._id); + body.client = client._id; + body.pilot = pilot._id; + body.vehicle = vehicle._id; + const req = makeReq({ body }); + const res = mockRes(); + await jobCtl.createJob_post(req, res); + jobId = res._data._id; + }); + + it('returns a job by numeric id', async () => { + const req = makeReq({ params: { job_id: String(jobId) } }); + const res = mockRes(); + + await jobCtl.getJob_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data._id).toBe(jobId); + }); + }); + + // ------------------------------------------------------------------------- + describe('updateJob_put', () => { + let jobId; + + beforeAll(async () => { + const body = mockJob(applicator._id); + body.client = client._id; + body.pilot = pilot._id; + body.vehicle = vehicle._id; + const req = makeReq({ body }); + const res = mockRes(); + await jobCtl.createJob_post(req, res); + jobId = res._data._id; + }); + + it('updates a job field', async () => { + const req = makeReq({ + params: { job_id: String(jobId) }, + body: { job: { name: 'Updated Job Name' } }, + }); + const res = mockRes(); + + await jobCtl.updateJob_put(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.name).toBe('Updated Job Name'); + }); + }); + + // ------------------------------------------------------------------------- + describe('deleteJob', () => { + let jobId; + + beforeAll(async () => { + const body = mockJob(applicator._id); + body.client = client._id; + body.pilot = pilot._id; + body.vehicle = vehicle._id; + const req = makeReq({ body }); + const res = mockRes(); + await jobCtl.createJob_post(req, res); + jobId = res._data._id; + }); + + it('deletes a job (transaction error handled on standalone MongoDB)', async () => { + const req = makeReq({ params: { id: String(jobId) } }); + const res = mockRes(); + + // deleteJob wraps the transaction with try/catch – it either succeeds + // fully or returns a soft-success; either way res.json is called. + await jobCtl.deleteJob(req, res); + + expect(res.json).toHaveBeenCalled(); + }); + }); +}); diff --git a/Development/server/tests/integration/log_payment.integration.test.js b/Development/server/tests/integration/log_payment.integration.test.js new file mode 100644 index 0000000..9b3dd1b --- /dev/null +++ b/Development/server/tests/integration/log_payment.integration.test.js @@ -0,0 +1,108 @@ +'use strict'; + +/** + * Integration tests – log_payment controller (controllers/log_payment.js) + * + * createLogPayment_post and createLogPayments_post are skipped because they + * use MongoDB transactions which require a replica-set. Only the read path + * (getLogPayments_get) is tested here. + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockClient, mockReq, mockRes, newId } = require('./mock_data'); + +let LogPayment, Invoice, Customer, Client; + +beforeAll(async () => { + await connectDB(); + Customer = require('../../model/customer'); + Client = require('../../model/client'); + LogPayment = require('../../model/log_payment'); + ({ Invoice } = require('../../model')); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const logPaymentCtl = require('../../controllers/log_payment'); + +describe('log_payment controller – data methods', () => { + let applicator, client, invoice; + + function makeInvoiceDoc(puid, clientId) { + return { + code: `INV-TEST-${Date.now()}`, + currency: 'USD', + dueDate: new Date(), + openDate: new Date(), + byPuid: puid, + clients: [{ billTo: clientId, split: '100', subTotal: '0.00' }], + jobs: [{ totalAmount: '0.00' }], + }; + } + + function makeLogPaymentDoc(puid, clientId, invoiceId) { + return { + byPuid: puid, + invoice: invoiceId, + client: clientId, + amount: '100.00', + paymentDate: new Date(), + paymentMethod: 'cash', + }; + } + + beforeAll(async () => { + await clearCollection(LogPayment); + applicator = await Customer.create(mockApplicator()); + client = await Client.create(mockClient(applicator._id)); + invoice = await Invoice.create(makeInvoiceDoc(applicator._id, client._id)); + await LogPayment.create(makeLogPaymentDoc(applicator._id, client._id, invoice._id)); + }); + + afterAll(async () => { + await clearCollection(LogPayment); + await Invoice.deleteMany({ _id: invoice._id }); + await Client.deleteMany({ _id: client._id }); + await Customer.deleteMany({ _id: applicator._id }); + }); + + const makeReq = (extra = {}) => + mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); + + // ------------------------------------------------------------------------- + describe('getLogPayments_get', () => { + it('returns log payments for the applicator', async () => { + const req = makeReq({ query: { invoiceId: String(invoice._id) } }); + const res = mockRes(); + + await logPaymentCtl.getLogPayments_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + expect(res._data.length).toBeGreaterThan(0); + }); + + it('returns empty array when invoice has no log payments', async () => { + const emptyInvoice = await Invoice.create(makeInvoiceDoc(applicator._id, client._id)); + const req = makeReq({ query: { invoiceId: String(emptyInvoice._id) } }); + const res = mockRes(); + + await logPaymentCtl.getLogPayments_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.length).toBe(0); + await Invoice.deleteMany({ _id: emptyInvoice._id }); + }); + }); + + // ------------------------------------------------------------------------- + describe('createLogPayment_post (transaction – skipped)', () => { + it.skip('skipped: MongoDB transactions require a replica-set', () => {}); + }); + + describe('createLogPayments_post (transaction – skipped)', () => { + it.skip('skipped: MongoDB transactions require a replica-set', () => {}); + }); +}); diff --git a/Development/server/tests/integration/main.integration.test.js b/Development/server/tests/integration/main.integration.test.js new file mode 100644 index 0000000..1e4466c --- /dev/null +++ b/Development/server/tests/integration/main.integration.test.js @@ -0,0 +1,76 @@ +'use strict'; + +/** + * Integration tests – main controller (controllers/main.js) + * + * Covers: pingAPI_get, getAppConfig_get, setAppConfig_post + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockReq, mockRes, newId } = require('./mock_data'); + +let Settings; + +beforeAll(async () => { + await connectDB(); + Settings = require('../../model/setting'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const mainCtl = require('../../controllers/main'); + +describe('main controller – data methods', () => { + afterAll(async () => { + await clearCollection(Settings); + }); + + // ------------------------------------------------------------------------- + describe('pingAPI_get', () => { + it('responds with a pong / alive message', async () => { + const req = mockReq(); + const res = mockRes(); + + await mainCtl.pingAPI_get(req, res); + + // pingAPI_get uses res.send() or res.json() + const called = res.json.mock.calls.length > 0 || res.send.mock.calls.length > 0; + expect(called).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('getAppConfig_get', () => { + it('returns an app config object', async () => { + const req = mockReq({ query: {} }); + const res = mockRes(); + + await mainCtl.getAppConfig_get(req, res); + + const called = res.json.mock.calls.length > 0 || res.send.mock.calls.length > 0; + expect(called).toBe(true); + expect(typeof res._data).toBe('object'); + }); + }); + + // ------------------------------------------------------------------------- + describe('setAppConfig_post', () => { + it('saves an app config value and returns the saved document', async () => { + await clearCollection(Settings); + + const req = mockReq({ + uid: newId(), + ut: '1', + body: { key: 'testKey', value: 'testValue' }, + }); + const res = mockRes(); + + await mainCtl.setAppConfig_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toBeTruthy(); + }); + }); +}); diff --git a/Development/server/tests/integration/mock_data.js b/Development/server/tests/integration/mock_data.js new file mode 100644 index 0000000..9c0d362 --- /dev/null +++ b/Development/server/tests/integration/mock_data.js @@ -0,0 +1,225 @@ +'use strict'; + +/** + * Shared mock data factories for integration tests. + * All ids are fresh ObjectIds so tests are fully isolated. + */ + +const mongoose = require('mongoose'); + +const newId = () => new mongoose.Types.ObjectId(); + +// --------------------------------------------------------------------------- +// Applicator / Customer (kind = "1") +// --------------------------------------------------------------------------- +function mockApplicator(overrides = {}) { + return { + _id: newId(), + kind: '1', + username: `applicator_${Date.now()}@test.com`, + password: '$2a$10$hashedpassword', + name: 'Test Applicator Co.', + email: `applicator_${Date.now()}@test.com`, + phone: '+15550001000', + address: '123 Main St', + country: 'US', + active: true, + billable: true, + premium: 0, + selfSignup: false, + markedDelete: false, + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// Client (kind = "3") +// --------------------------------------------------------------------------- +function mockClient(parentId, overrides = {}) { + return { + _id: newId(), + kind: '3', + username: `client_${Date.now()}@test.com`, + name: 'Test Client', + email: `client_${Date.now()}@test.com`, + phone: '+15550002000', + address: '456 Oak Ave', + country: 'US', + active: true, + parent: parentId, + markedDelete: false, + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// Pilot (kind = "5") +// --------------------------------------------------------------------------- +function mockPilot(parentId, overrides = {}) { + return { + _id: newId(), + kind: '5', + username: `pilot_${Date.now()}@test.com`, + name: 'Test Pilot', + email: `pilot_${Date.now()}@test.com`, + active: true, + parent: parentId, + markedDelete: false, + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// Vehicle / Device (kind = "9") +// --------------------------------------------------------------------------- +function mockVehicle(parentId, overrides = {}) { + return { + _id: newId(), + kind: '9', + name: `Aircraft-${Date.now()}`, + model: 'AgNav TestBird', + color: 'yellow', + vehicleType: 0, + active: true, + tracking: false, + pkgActive: false, + parent: parentId, + markedDelete: false, + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// Product +// --------------------------------------------------------------------------- +function mockProduct(puid, overrides = {}) { + return { + name: `Product-${Date.now()}`, + type: 1, // ACTIVE + restricted: false, + epaReg: `EPA-${Date.now()}`, + desc: 'Integration test product', + rate: { value: 2.5, unit: 1 }, // GAL + byPuid: puid, + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// Crop +// --------------------------------------------------------------------------- +function mockCrop(puid, overrides = {}) { + return { + name: `Crop-${Date.now()}`, + color: '#00AA00', + desc: 'Integration test crop', + byPuid: puid, + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// Job +// --------------------------------------------------------------------------- +function mockJob(puid, overrides = {}) { + return { + name: `Job-${Date.now()}`, + orderNumber: `ORD-${Date.now()}`, + measureUnit: false, // false = acres + swathWidth: 60, + byPuid: puid, + status: 0, // PENDING + markedDelete: false, + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// Costing Item +// --------------------------------------------------------------------------- +function mockCostingItem(puid, userId, overrides = {}) { + return { + name: `CostItem-${Date.now()}`, + type: 0, + unit: 1, // GAL + price: 10.5, + byPuid: puid, + createdBy: userId, + updatedBy: userId, + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// Invoice Setting +// --------------------------------------------------------------------------- +function mockInvoiceSetting(puid, userId, overrides = {}) { + return { + byPuid: puid, + userId, + companyName: 'Test Applicator Co.', + address: '123 Main St, Testville, TX', + taxValue: 0, + discount: 0, + termOpts: [15, 30, 60], + paymentTerm: 30, + currency: 'USD', + note: 'Integration test invoice setting', + logo: '', + createdBy: userId, + updatedBy: userId, + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// Request / response mocks used for controller unit-style tests +// --------------------------------------------------------------------------- + +/** + * Build a minimal Express-like req object for an authenticated applicator. + */ +function mockReq({ uid, puid, ut = '1', body = {}, params = {}, query = {}, userInfo = null } = {}) { + const _uid = uid ? String(uid) : String(new mongoose.Types.ObjectId()); + const _puid = puid ? String(puid) : _uid; + return { + uid: _uid, + ut, + userInfo: userInfo || { puid: _puid, kind: ut, premium: 0, membership: null, markedDelete: false }, + body, + params, + query, + headers: {}, + file: undefined, + }; +} + +/** + * Build a minimal Express-like res object with jest spy functions. + */ +function mockRes() { + const res = { + statusCode: 200, + _data: undefined, + }; + res.json = jest.fn((data) => { res._data = data; return res; }); + res.send = jest.fn((data) => { res._data = data; return res; }); + res.status = jest.fn((code) => { res.statusCode = code; return res; }); + res.end = jest.fn(() => res); + return res; +} + +module.exports = { + newId, + mockApplicator, + mockClient, + mockPilot, + mockVehicle, + mockProduct, + mockCrop, + mockJob, + mockCostingItem, + mockInvoiceSetting, + mockReq, + mockRes, +}; diff --git a/Development/server/tests/integration/obstacle.integration.test.js b/Development/server/tests/integration/obstacle.integration.test.js new file mode 100644 index 0000000..21986b5 --- /dev/null +++ b/Development/server/tests/integration/obstacle.integration.test.js @@ -0,0 +1,187 @@ +'use strict'; + +/** + * Integration tests – obstacle controller (controllers/obstacle.js) + * + * Covers: createObstacle_post, updateObstacle_put, deleteObstacle, near_post + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockReq, mockRes, newId } = require('./mock_data'); + +let Obstacle; + +beforeAll(async () => { + await connectDB(); + Obstacle = require('../../model/obstacles'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const obstacleCtl = require('../../controllers/obstacle'); + +describe('obstacle controller – data methods', () => { + afterAll(async () => { + await clearCollection(Obstacle); + }); + + // ------------------------------------------------------------------------- + describe('createObstacle_post', () => { + it('creates an obstacle and returns it as a plain object', async () => { + const userId = newId(); + const req = mockReq({ + uid: userId, + body: { + item: { + name: 'Test Tower', + type: 'USER', + agl: 150, + amsl: 500, + coors: [-80.1234, 25.7617], + }, + pUser: String(userId), + }, + }); + const res = mockRes(); + + await obstacleCtl.createObstacle_post(req, res); + + expect(res.json).toHaveBeenCalled(); + const obstacle = res._data; + expect(obstacle._id).toBeDefined(); + expect(obstacle.name).toBe('Test Tower'); + expect(obstacle.agl).toBe(150); + expect(obstacle.amsl).toBe(500); + }); + + it('throws AppParamError when body is missing', async () => { + const req = mockReq({ body: null }); + const res = mockRes(); + await expect(obstacleCtl.createObstacle_post(req, res)).rejects.toThrow(); + }); + + it('throws AppParamError when coordinates are missing', async () => { + const req = mockReq({ + body: { + item: { name: 'Bad Obs', agl: 100, coors: [] }, + pUser: String(newId()), + }, + }); + const res = mockRes(); + await expect(obstacleCtl.createObstacle_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('updateObstacle_put', () => { + let obstacleId; + + beforeAll(async () => { + const obs = await Obstacle.create({ + properties: { name: 'Original', type: 'USER', agl: 100, amsl: 300 }, + geometry: { type: 'Point', coordinates: [-80.0, 25.0] }, + }); + obstacleId = String(obs._id); + }); + + it('updates name and agl and returns updated obstacle', async () => { + const req = mockReq({ + params: { ob_id: obstacleId }, + body: { name: 'Updated Tower', agl: 200, amsl: 400 }, + }); + const res = mockRes(); + + await obstacleCtl.updateObstacle_put(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toBeTruthy(); + expect(res._data.name).toBe('Updated Tower'); + expect(res._data.agl).toBe(200); + }); + }); + + // ------------------------------------------------------------------------- + describe('deleteObstacle', () => { + let obstacleId; + + beforeAll(async () => { + const obs = await Obstacle.create({ + properties: { name: 'To Delete', type: 'USER', agl: 50, amsl: 200 }, + geometry: { type: 'Point', coordinates: [-79.0, 26.0] }, + }); + obstacleId = String(obs._id); + }); + + it('deletes obstacle and returns success message', async () => { + const req = mockReq({ params: { ob_id: obstacleId } }); + const res = mockRes(); + + await obstacleCtl.deleteObstacle(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.message).toMatch(/deleted/i); + const found = await Obstacle.findById(obstacleId); + expect(found).toBeNull(); + }); + + it('returns success even when obstacle does not exist', async () => { + const req = mockReq({ params: { ob_id: String(newId()) } }); + const res = mockRes(); + + await obstacleCtl.deleteObstacle(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.message).toMatch(/deleted/i); + }); + }); + + // ------------------------------------------------------------------------- + describe('near_post', () => { + beforeAll(async () => { + await Obstacle.ensureIndexes(); + await Obstacle.create({ + properties: { name: 'Miami Tower', type: 'USER', agl: 200, amsl: 600 }, + geometry: { type: 'Point', coordinates: [-80.1918, 25.7617] }, + }); + }); + + it('returns obstacles near the given center within radius', async () => { + const req = mockReq({ + body: { center: { lng: -80.1918, lat: 25.7617 }, min: 0 }, + }); + const res = mockRes(); + + await obstacleCtl.near_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + expect(res._data.length).toBeGreaterThan(0); + }); + + it('returns empty array when no obstacles are in the vicinity', async () => { + const req = mockReq({ + body: { center: { lng: -140.0, lat: 0.0 }, min: 0 }, + }); + const res = mockRes(); + + await obstacleCtl.near_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.length).toBe(0); + }); + + it('filters by minimum AGL height', async () => { + const req = mockReq({ + body: { center: { lng: -80.1918, lat: 25.7617 }, min: 999 }, + }); + const res = mockRes(); + + await obstacleCtl.near_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.length).toBe(0); + }); + }); +}); diff --git a/Development/server/tests/integration/partner.integration.test.js b/Development/server/tests/integration/partner.integration.test.js new file mode 100644 index 0000000..38bbde4 --- /dev/null +++ b/Development/server/tests/integration/partner.integration.test.js @@ -0,0 +1,180 @@ +'use strict'; + +/** + * Integration tests – partner controller (controllers/partner.js) + * + * Covers: createPartner_post, getPartners_get, getPartners_post, + * getPartnerById_get, updatePartner_put, deletePartner + * + * Skipped: syncData_post, uploadJob_post, testPartnerAuth_post + * (require external partner service / RabbitMQ). + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockReq, mockRes, newId } = require('./mock_data'); + +let Partner, Customer; + +beforeAll(async () => { + await connectDB(); + Customer = require('../../model/customer'); + // Ensure partner discriminator is registered + const partnerModel = require('../../model/partner'); + Partner = partnerModel.Partner || partnerModel; +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const partnerCtl = require('../../controllers/partner'); + +describe('partner controller – data methods', () => { + let admin; + + function makePartnerBody() { + return { + name: `Partner-${Date.now()}`, + username: `partner_${Date.now()}@test.com`, + password: 'Test@123', + active: true, + kind: '20', + partnerCode: `TESTCODE-${Date.now()}`, + }; + } + + beforeAll(async () => { + await clearCollection(Partner); + admin = await Customer.create(mockApplicator()); + }); + + afterAll(async () => { + await clearCollection(Partner); + await Customer.deleteMany({ _id: admin._id }); + }); + + const makeReq = (extra = {}) => + mockReq({ uid: admin._id, puid: admin._id, ut: '1', ...extra }); + + // ------------------------------------------------------------------------- + describe('createPartner_post', () => { + it('creates a partner and returns it', async () => { + const req = makeReq({ body: makePartnerBody() }); + const res = mockRes(); + + await partnerCtl.createPartner_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data._id).toBeDefined(); + expect(res._data.kind).toBe('20'); + }); + + it('throws when body is missing', async () => { + const req = makeReq({ body: null }); + const res = mockRes(); + await expect(partnerCtl.createPartner_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('getPartners_get', () => { + it('returns array of partners', async () => { + const req = makeReq({ query: {} }); + const res = mockRes(); + + await partnerCtl.getPartners_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('getPartners_post', () => { + it('returns partners matching search criteria', async () => { + const req = makeReq({ body: { name: 'Partner' } }); + const res = mockRes(); + + await partnerCtl.getPartners_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('getPartnerById_get', () => { + let partnerId; + + beforeAll(async () => { + const req = makeReq({ body: makePartnerBody() }); + const res = mockRes(); + await partnerCtl.createPartner_post(req, res); + partnerId = String(res._data._id); + }); + + it('returns partner by id', async () => { + const req = makeReq({ params: { id: partnerId } }); + const res = mockRes(); + + await partnerCtl.getPartnerById_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(String(res._data._id)).toBe(partnerId); + }); + + it('throws when partner id is invalid', async () => { + const req = makeReq({ params: { id: 'invalid-id' } }); + const res = mockRes(); + await expect(partnerCtl.getPartnerById_get(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('updatePartner_put', () => { + let partnerId; + + beforeAll(async () => { + const req = makeReq({ body: makePartnerBody() }); + const res = mockRes(); + await partnerCtl.createPartner_post(req, res); + partnerId = String(res._data._id); + }); + + it('updates a partner field', async () => { + const req = makeReq({ + params: { id: partnerId }, + body: { name: 'Updated Partner Name', kind: '20' }, + }); + const res = mockRes(); + + await partnerCtl.updatePartner_put(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.name).toBe('Updated Partner Name'); + }); + }); + + // ------------------------------------------------------------------------- + describe('deletePartner', () => { + let partnerId; + + beforeAll(async () => { + const req = makeReq({ body: makePartnerBody() }); + const res = mockRes(); + await partnerCtl.createPartner_post(req, res); + partnerId = String(res._data._id); + }); + + it('soft-deletes a partner', async () => { + const req = makeReq({ params: { id: partnerId } }); + const res = mockRes(); + + await partnerCtl.deletePartner(req, res); + + expect(res.json).toHaveBeenCalled(); + const found = await Partner.findById(partnerId); + expect(!found || found.active === false).toBeTruthy(); + }); + }); +}); diff --git a/Development/server/tests/integration/pilot.integration.test.js b/Development/server/tests/integration/pilot.integration.test.js new file mode 100644 index 0000000..69b9a86 --- /dev/null +++ b/Development/server/tests/integration/pilot.integration.test.js @@ -0,0 +1,113 @@ +'use strict'; + +/** + * Integration tests – pilot controller (controllers/pilot.js) + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockPilot, mockReq, mockRes, newId } = require('./mock_data'); + +let Pilot, Customer; + +beforeAll(async () => { + await connectDB(); + Customer = require('../../model/customer'); + Pilot = require('../../model/pilot'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const pilotCtl = require('../../controllers/pilot'); + +describe('pilot controller – data methods', () => { + let applicator; + + beforeAll(async () => { + await clearCollection(Pilot); + applicator = await Customer.create(mockApplicator()); + }); + + afterAll(async () => { + await clearCollection(Pilot); + await Customer.deleteMany({ _id: applicator._id }); + }); + + const makeReq = (extra = {}) => + mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); + + // ------------------------------------------------------------------------- + describe('create_post', () => { + it('creates a pilot and returns it', async () => { + const req = makeReq({ body: mockPilot(applicator._id) }); + const res = mockRes(); + + await pilotCtl.createPilot_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data._id).toBeDefined(); + expect(res._data.name).toBe('Test Pilot'); + }); + + it('throws when body is missing', async () => { + const req = makeReq({ body: null }); + const res = mockRes(); + await expect(pilotCtl.createPilot_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('getById_get', () => { + let pilotId; + + beforeAll(async () => { + const doc = await Pilot.create(mockPilot(applicator._id)); + pilotId = String(doc._id); + }); + + it('returns pilot by id', async () => { + const req = makeReq({ params: { id: pilotId } }); + const res = mockRes(); + + await pilotCtl.getPilot_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(String(res._data._id)).toBe(pilotId); + }); + }); + + // ------------------------------------------------------------------------- + describe('search_post', () => { + it('returns matching pilots', async () => { + const req = makeReq({ body: { byUserId: applicator._id } }); + const res = mockRes(); + + await pilotCtl.search_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('delete_delete', () => { + let pilotId; + + beforeAll(async () => { + const doc = await Pilot.create(mockPilot(applicator._id)); + pilotId = String(doc._id); + }); + + it.skip('soft-deletes a pilot (skipped: requires MongoDB replica-set for transactions)', async () => { + const req = makeReq({ params: { id: pilotId } }); + const res = mockRes(); + + await pilotCtl.deletePilot(req, res); + + expect(res.json).toHaveBeenCalled(); + const found = await Pilot.findById(pilotId); + expect(!found || found.markedDelete).toBeTruthy(); + }); + }); +}); diff --git a/Development/server/tests/integration/product.integration.test.js b/Development/server/tests/integration/product.integration.test.js new file mode 100644 index 0000000..ee6ec95 --- /dev/null +++ b/Development/server/tests/integration/product.integration.test.js @@ -0,0 +1,156 @@ +'use strict'; + +/** + * Integration tests – product controller (controllers/product.js) + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockProduct, mockReq, mockRes, newId } = require('./mock_data'); + +let Product, Customer; + +beforeAll(async () => { + await connectDB(); + Product = require('../../model/product'); + Customer = require('../../model/customer'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const productCtl = require('../../controllers/product'); + +describe('product controller – data methods', () => { + let applicator; + + beforeAll(async () => { + await clearCollection(Product); + applicator = await Customer.create(mockApplicator()); + }); + + afterAll(async () => { + await clearCollection(Product); + await Customer.deleteMany({ _id: applicator._id }); + }); + + const makeReq = (extra = {}) => + mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); + + // ------------------------------------------------------------------------- + describe('create_post', () => { + it('creates a product and returns it', async () => { + const req = makeReq({ body: mockProduct(applicator._id) }); + const res = mockRes(); + + await productCtl.createProduct_post(req, res); + + expect(res.json).toHaveBeenCalled(); + const p = res._data; + expect(p._id).toBeDefined(); + expect(p.name).toMatch(/Product-/); + }); + + it('throws when body is missing', async () => { + const req = makeReq({ body: null }); + const res = mockRes(); + await expect(productCtl.createProduct_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('getAll_get', () => { + it('returns products for the applicator', async () => { + const req = makeReq({ query: {} }); + const res = mockRes(); + + await productCtl.getProducts_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('getById_get', () => { + let productId; + + beforeAll(async () => { + const doc = await Product.create(mockProduct(applicator._id)); + productId = String(doc._id); + }); + + it('returns a product by id', async () => { + const req = makeReq({ params: { product_id: productId } }); + const res = mockRes(); + + await productCtl.getProduct_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(String(res._data._id)).toBe(productId); + }); + + it('throws when id is invalid', async () => { + const req = makeReq({ params: { product_id: 'bad-id' } }); + const res = mockRes(); + await expect(productCtl.getProduct_get(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('update_put', () => { + let productId; + + beforeAll(async () => { + const doc = await Product.create(mockProduct(applicator._id)); + productId = String(doc._id); + }); + + it('updates a product', async () => { + const req = makeReq({ + params: { product_id: productId }, + body: { name: 'Updated Product' }, + }); + const res = mockRes(); + + await productCtl.updateProduct_put(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.name).toBe('Updated Product'); + }); + }); + + // ------------------------------------------------------------------------- + describe('search_post', () => { + it('returns matching products', async () => { + const req = makeReq({ body: { byUserId: applicator._id } }); + const res = mockRes(); + + await productCtl.search_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('delete_delete', () => { + let productId; + + beforeAll(async () => { + const doc = await Product.create(mockProduct(applicator._id)); + productId = String(doc._id); + }); + + it('deletes a product', async () => { + const req = makeReq({ params: { product_id: productId } }); + const res = mockRes(); + + await productCtl.deleteProduct(req, res); + + expect(res.json).toHaveBeenCalled(); + const found = await Product.findById(productId); + expect(found).toBeNull(); + }); + }); +}); diff --git a/Development/server/tests/integration/user.integration.test.js b/Development/server/tests/integration/user.integration.test.js new file mode 100644 index 0000000..cb1bf26 --- /dev/null +++ b/Development/server/tests/integration/user.integration.test.js @@ -0,0 +1,112 @@ +'use strict'; + +/** + * Integration tests – user controller (controllers/user.js) + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockReq, mockRes, newId } = require('./mock_data'); + +let User, Customer; + +beforeAll(async () => { + await connectDB(); + User = require('../../model/user'); + Customer = require('../../model/customer'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const userCtl = require('../../controllers/user'); + +describe('user controller – data methods', () => { + let applicator; + + beforeAll(async () => { + applicator = await Customer.create(mockApplicator()); + }); + + afterAll(async () => { + await Customer.deleteMany({ _id: applicator._id }); + }); + + const makeReq = (extra = {}) => + mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); + + // ------------------------------------------------------------------------- + describe('getUser_get', () => { + it('returns a user by id', async () => { + const req = makeReq({ params: { id: String(applicator._id) }, query: {} }); + const res = mockRes(); + + await userCtl.getUser_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(String(res._data._id)).toBe(String(applicator._id)); + }); + + it('returns null when user not found', async () => { + const req = makeReq({ params: { id: String(newId()) }, query: {} }); + const res = mockRes(); + + await userCtl.getUser_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toBeNull(); + }); + }); + + // ------------------------------------------------------------------------- + describe('updateUser_put', () => { + it('updates a user field', async () => { + const req = makeReq({ + params: { id: String(applicator._id) }, + body: { name: 'Updated Applicator Name', kind: '1' }, + }); + const res = mockRes(); + + await userCtl.updateUser_put(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.name).toBe('Updated Applicator Name'); + }); + }); + + // ------------------------------------------------------------------------- + describe('usernameExists_post', () => { + it('returns true when username exists', async () => { + const req = makeReq({ body: { username: applicator.username } }); + const res = mockRes(); + + await userCtl.isUserNameExists_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toBe(1); + }); + + it('returns false when username does not exist', async () => { + const req = makeReq({ body: { username: `nonexistent_${Date.now()}@test.com` } }); + const res = mockRes(); + + await userCtl.isUserNameExists_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toBe(0); + }); + }); + + // ------------------------------------------------------------------------- + describe('searchUsers_post', () => { + it('returns matching users', async () => { + const req = makeReq({ body: { byPuid: applicator._id } }); + const res = mockRes(); + + await userCtl.search_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + }); +}); diff --git a/Development/server/tests/integration/vehicle.integration.test.js b/Development/server/tests/integration/vehicle.integration.test.js new file mode 100644 index 0000000..6d2f296 --- /dev/null +++ b/Development/server/tests/integration/vehicle.integration.test.js @@ -0,0 +1,137 @@ +'use strict'; + +/** + * Integration tests – vehicle controller (controllers/vehicle.js) + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockVehicle, mockReq, mockRes, newId } = require('./mock_data'); + +let Vehicle, Customer; + +beforeAll(async () => { + await connectDB(); + Customer = require('../../model/customer'); + Vehicle = require('../../model/vehicle'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const vehicleCtl = require('../../controllers/vehicle'); + +describe('vehicle controller – data methods', () => { + let applicator; + + beforeAll(async () => { + await clearCollection(Vehicle); + applicator = await Customer.create(mockApplicator()); + }); + + afterAll(async () => { + await clearCollection(Vehicle); + await Customer.deleteMany({ _id: applicator._id }); + }); + + const makeReq = (extra = {}) => + mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); + + // ------------------------------------------------------------------------- + describe('create_post', () => { + it('creates a vehicle and returns it', async () => { + const req = makeReq({ body: mockVehicle(applicator._id) }); + const res = mockRes(); + + await vehicleCtl.createVehicle_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data._id).toBeDefined(); + expect(res._data.name).toMatch(/Aircraft-/); + }); + + it('throws when body is missing', async () => { + const req = makeReq({ body: null }); + const res = mockRes(); + await expect(vehicleCtl.createVehicle_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('getById_get', () => { + let vehicleId; + + beforeAll(async () => { + const doc = await Vehicle.create(mockVehicle(applicator._id)); + vehicleId = String(doc._id); + }); + + it('returns vehicle by id', async () => { + const req = makeReq({ params: { id: vehicleId } }); + const res = mockRes(); + + await vehicleCtl.getVehicle_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(String(res._data._id)).toBe(vehicleId); + }); + }); + + // ------------------------------------------------------------------------- + describe('search_post', () => { + it('returns matching vehicles', async () => { + const req = makeReq({ body: { byUserId: applicator._id } }); + const res = mockRes(); + + await vehicleCtl.search_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('bulkUpdate_post', () => { + let vehicleIds; + + beforeAll(async () => { + const docs = await Vehicle.create([ + mockVehicle(applicator._id), + mockVehicle(applicator._id), + ]); + vehicleIds = docs.map(d => String(d._id)); + }); + + it('bulk-updates multiple vehicles', async () => { + const req = makeReq({ + body: vehicleIds.map(id => ({ _id: id, active: false })), + }); + const res = mockRes(); + + await vehicleCtl.updateVehicles_post(req, res); + + expect(res.json).toHaveBeenCalled(); + }); + }); + + // ------------------------------------------------------------------------- + describe('delete_delete', () => { + let vehicleId; + + beforeAll(async () => { + const doc = await Vehicle.create(mockVehicle(applicator._id)); + vehicleId = String(doc._id); + }); + + it.skip('soft-deletes a vehicle (skipped: requires MongoDB replica-set for transactions)', async () => { + const req = makeReq({ params: { id: vehicleId } }); + const res = mockRes(); + + await vehicleCtl.deleteVehicle(req, res); + + expect(res.json).toHaveBeenCalled(); + const found = await Vehicle.findById(vehicleId); + expect(!found || found.markedDelete).toBeTruthy(); + }); + }); +});