diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cacd3fe..b9481e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,11 +34,37 @@ jobs: fail-fast: true matrix: node-version: [20, 22] + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: ajosave + POSTGRES_PASSWORD: ajosave + POSTGRES_DB: ajosave_test + ports: ["5432:5432"] + options: >- + --health-cmd pg_isready + --health-interval 5s + --health-timeout 5s + --health-retries 5 + redis: + image: redis:7-alpine + ports: ["6379:6379"] + options: >- + --health-cmd "redis-cli ping" + --health-interval 5s + --health-timeout 5s + --health-retries 5 + env: + DATABASE_URL: postgresql://ajosave:ajosave@localhost:5432/ajosave_test + REDIS_URL: redis://localhost:6379 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: "${{ matrix.node-version }}", cache: npm } - run: npm ci + - name: Run migrations before tests + run: npm run migrate - name: Run tests with coverage run: npm run test:coverage -- --ci - name: Upload coverage to Codecov diff --git a/jest.config.js b/jest.config.js index f7a5996..d6633c0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -24,6 +24,24 @@ const config = { statements: 70, }, }, + // Integration tests require the Node environment (no DOM needed for supertest) + testEnvironmentOptions: {}, + projects: [ + { + displayName: "unit", + testEnvironment: "jest-environment-jsdom", + testPathPattern: "src/(?!__tests__/integration)", + setupFilesAfterEnv: ["/jest.setup.ts"], + moduleNameMapper: { "^@/(.*)$": "/src/$1" }, + }, + { + displayName: "integration", + testEnvironment: "node", + testPathPattern: "src/__tests__/integration/.*\\.test\\.ts$", + setupFilesAfterEnv: ["/jest.setup.ts"], + moduleNameMapper: { "^@/(.*)$": "/src/$1" }, + }, + ], }; module.exports = createJestConfig(config); diff --git a/package-lock.json b/package-lock.json index 6db1415..8a4c16f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@stellar/freighter-api": "3.1.0", "@stellar/stellar-sdk": "^12.3.0", "@tanstack/react-query": "^5.40.0", + "@vercel/analytics": "^1.1.0", "axios": "^1.7.2", "clsx": "^2.1.1", "date-fns": "^3.6.0", @@ -39,15 +40,19 @@ "@types/pino": "^7.0.5", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@types/supertest": "^2.0.12", + "bullmq": "^1.81.0", "eslint": "^8.57.0", "eslint-config-next": "14.2.4", - "eslint-config-prettier": "^9.1.0", + "eslint-config-prettier": "^9.1.1", "fast-check": "4.6.0", "husky": "^9.0.11", + "ioredis": "^5.3.2", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "lint-staged": "^15.2.7", "prettier": "^3.3.2", + "supertest": "^6.3.3", "ts-node": "^10.9.2", "typescript": "^5.5.3" }, @@ -91,7 +96,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -739,6 +743,13 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@ioredis/commands": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz", + "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1330,6 +1341,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -1351,6 +1363,90 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -1524,6 +1620,19 @@ "node": ">= 10" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1577,7 +1686,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -1599,7 +1707,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz", "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" }, @@ -1636,7 +1743,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@types/shimmer": "^1.2.0", @@ -2178,7 +2284,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" } @@ -2207,6 +2312,16 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2224,7 +2339,6 @@ "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "playwright": "1.44.1" }, @@ -2295,7 +2409,6 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.12.1.tgz", "integrity": "sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==", "license": "MIT", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2" }, @@ -2399,7 +2512,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3203,7 +3315,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -3259,11 +3372,19 @@ "@types/node": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -3274,6 +3395,7 @@ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -3564,7 +3686,8 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/json5": { "version": "0.0.29", @@ -3573,6 +3696,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mysql": { "version": "2.15.26", "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", @@ -3587,7 +3717,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3636,7 +3765,6 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -3648,7 +3776,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -3666,6 +3793,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/superagent": { + "version": "8.1.10", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.10.tgz", + "integrity": "sha512-nbt4IWXABhW0jGmmpRzCFNlbmwCTzZ2gTUsNIr+X+ItdqPms+PAJZbWsNzpS2USqXjcoNLQcO6nXo60zcPQiIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.16.tgz", + "integrity": "sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/superagent": "*" + } + }, "node_modules/@types/tedious": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", @@ -4109,11 +4259,50 @@ "win32" ] }, + "node_modules/@vercel/analytics": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.6.1.tgz", + "integrity": "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==", + "license": "MPL-2.0", + "peerDependencies": { + "@remix-run/react": "^2", + "@sveltejs/kit": "^1 || ^2", + "next": ">= 13", + "react": "^18 || ^19 || ^19.0.0-rc", + "svelte": ">= 4", + "vue": "^3", + "vue-router": "^4" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -4123,25 +4312,29 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -4152,13 +4345,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4171,6 +4366,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "license": "MIT", + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -4180,6 +4376,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -4188,13 +4385,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4211,6 +4410,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -4224,6 +4424,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4236,6 +4437,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -4250,6 +4452,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -4259,13 +4462,15 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/abab": { "version": "2.0.6", @@ -4292,7 +4497,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4325,6 +4529,7 @@ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" }, @@ -4389,6 +4594,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -4406,6 +4612,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4421,7 +4628,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/ansi-escapes": { "version": "4.3.2", @@ -4683,6 +4891,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -5041,7 +5256,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -5096,6 +5310,83 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/bullmq": { + "version": "1.91.1", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-1.91.1.tgz", + "integrity": "sha512-u7dat9I8ZwouZ651AMZkBSvB6NVUPpnAjd4iokd9DM41whqIBnDjuL11h7+kEjcpiDKj6E+wxZiER00FqirZQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cron-parser": "^4.6.0", + "get-port": "6.1.2", + "glob": "^8.0.3", + "ioredis": "^5.2.2", + "lodash": "^4.17.21", + "msgpackr": "^1.6.2", + "semver": "^7.3.7", + "tslib": "^2.0.0", + "uuid": "^9.0.0" + } + }, + "node_modules/bullmq/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/bullmq/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/bullmq/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/bullmq/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -5262,6 +5553,7 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.0" } @@ -5529,6 +5821,16 @@ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "license": "MIT" }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5551,6 +5853,13 @@ "node": ">= 0.6" } }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -5580,6 +5889,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5830,6 +6152,16 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -5840,6 +6172,17 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -5850,6 +6193,17 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", @@ -5901,7 +6255,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/domexception": { "version": "4.0.0", @@ -5990,6 +6345,7 @@ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" @@ -6153,7 +6509,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -6264,7 +6621,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6446,7 +6802,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6945,7 +7300,8 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/fastq": { "version": "1.20.1", @@ -7097,6 +7453,22 @@ "node": ">= 6" } }, + "node_modules/formidable": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", + "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded-parse": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", @@ -7238,6 +7610,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-port": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-6.1.2.tgz", + "integrity": "sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -7336,7 +7721,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.1.0", @@ -7772,6 +8158,39 @@ "node": ">= 0.4" } }, + "node_modules/ioredis": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.0.tgz", + "integrity": "sha512-EZBErytyVovD8f6pDfG3Kb37N6Y3lmDA9NNj+4+IP13CzzHGeX+OyeRM2Um13khRzoBSzzL+5lVnCX8V2RLeMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.10.0", + "cluster-key-slot": "1.1.1", + "debug": "4.4.3", + "denque": "2.1.0", + "redis-errors": "1.2.0", + "redis-parser": "3.0.0", + "standard-as-callback": "2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/node_modules/cluster-key-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", + "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -9876,6 +10295,7 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.11.5" }, @@ -9899,6 +10319,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -10081,12 +10508,23 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -10158,6 +10596,16 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -10172,6 +10620,19 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -10269,6 +10730,39 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/msgpackr": { + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.12.tgz", + "integrity": "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -10314,7 +10808,8 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/next": { "version": "14.2.4", @@ -10322,7 +10817,6 @@ "integrity": "sha512-R8/V7vugY+822rsQGQCjoLhMuC9oFj9SOi4Cl4b2wjDrseD0LRZ10W7R6Czo4w9ZznVSshKjuIomsRjvm9EKJQ==", "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "14.2.4", "@swc/helpers": "0.5.5", @@ -10480,6 +10974,22 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -11105,13 +11615,15 @@ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/pg-connection-string": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/pg-int8": { "version": "1.0.1", @@ -11127,6 +11639,7 @@ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", "license": "MIT", + "peer": true, "peerDependencies": { "pg": ">=8.0" } @@ -11158,6 +11671,7 @@ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", "license": "MIT", + "peer": true, "dependencies": { "split2": "^4.1.0" } @@ -11371,7 +11885,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -11463,7 +11976,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -11519,6 +12031,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -11534,6 +12047,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -11667,6 +12181,22 @@ ], "license": "MIT" }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -11715,7 +12245,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11728,7 +12257,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -11742,7 +12270,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.72.1.tgz", "integrity": "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -11759,7 +12286,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-is-18": { "name": "react-is", @@ -11844,6 +12372,29 @@ "node": ">= 18.19.0" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dev": true, + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -11915,6 +12466,7 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12133,7 +12685,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -12287,6 +12838,7 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -12323,6 +12875,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -12334,7 +12887,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/secure-json-parse": { "version": "2.7.0", @@ -12699,6 +13253,13 @@ "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", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -13041,6 +13602,44 @@ } } }, + "node_modules/superagent": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", + "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/supertest": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", + "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", + "deprecated": "Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^8.1.2" + }, + "engines": { + "node": ">=6.4.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -13077,6 +13676,7 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "license": "MIT", + "peer": true, "engines": { "node": ">=6" }, @@ -13090,6 +13690,7 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.2.tgz", "integrity": "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -13108,6 +13709,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.5.0.tgz", "integrity": "sha512-UYhptBwhWvfIjKd/UuFo6D8uq9xpGLDK+z8EDsj/zWhrTaH34cKEbrkMKfV5YWqGBvAYA3tlzZbs2R+qYrbQJA==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -13141,6 +13743,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -13155,6 +13758,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -13169,13 +13773,15 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/terser/node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -13275,7 +13881,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13370,7 +13975,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -13576,7 +14180,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13783,6 +14386,7 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "license": "MIT", + "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -13806,6 +14410,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -13868,6 +14473,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -13881,6 +14487,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -13890,6 +14497,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } diff --git a/package.json b/package.json index 39e856b..238cc1a 100644 --- a/package.json +++ b/package.json @@ -75,15 +75,17 @@ "@types/pino": "^7.0.5", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@types/supertest": "^2.0.12", "eslint": "^8.57.0", "eslint-config-next": "14.2.4", - "eslint-config-prettier": "^9.1.0", + "eslint-config-prettier": "^9.1.1", "fast-check": "4.6.0", "husky": "^9.0.11", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "lint-staged": "^15.2.7", "prettier": "^3.3.2", + "supertest": "^6.3.3", "ts-node": "^10.9.2", "typescript": "^5.5.3", "bullmq": "^1.81.0", diff --git a/src/__tests__/integration/api-route-integration.test.ts b/src/__tests__/integration/api-route-integration.test.ts new file mode 100644 index 0000000..bfa52bf --- /dev/null +++ b/src/__tests__/integration/api-route-integration.test.ts @@ -0,0 +1,460 @@ +/** + * @jest-environment node + * + * API Route Integration Tests — Issue #289 + * + * Acceptance criteria: + * ✅ All CRUD routes tested (create, read, update, delete) + * ✅ Auth-protected routes tested with and without token + * ✅ Test DB seeded and cleaned between tests + * ✅ Tests run in CI + */ +import * as request from "supertest"; +import { getServerSession } from "next-auth"; +import { createTestServer } from "./supertest-app"; +import { + closeTestDatabase, + resetIntegrationDatabase, + seedCircle, + seedContribution, + seedMember, + seedUser, +} from "./test-db"; + +jest.mock("next-auth", () => ({ getServerSession: jest.fn() })); +jest.mock("@/lib/auth", () => ({ authOptions: {} })); +// Prevent real SMS / Redis calls in auth routes +jest.mock("@/lib/sms", () => ({ sendOtp: jest.fn().mockResolvedValue("123456") })); +jest.mock("@/lib/redis", () => ({ + getRedis: jest.fn().mockResolvedValue({ + get: jest.fn().mockResolvedValue(null), + set: jest.fn().mockResolvedValue("OK"), + del: jest.fn().mockResolvedValue(1), + ping: jest.fn().mockResolvedValue("PONG"), + incr: jest.fn().mockResolvedValue(1), + expire: jest.fn().mockResolvedValue(1), + ttl: jest.fn().mockResolvedValue(-1), + }), +})); +jest.mock("@/lib/lockout", () => ({ + getLockoutStatus: jest.fn().mockResolvedValue({ + isLocked: false, + attempts: 0, + remainingAttempts: 5, + }), +})); +jest.mock("@/server/middleware", () => { + const actual = jest.requireActual("@/server/middleware"); + return { + ...actual, + withRateLimit: (handler: unknown) => handler, + rateLimit: jest.fn().mockResolvedValue({ allowed: true, remaining: 9 }), + }; +}); + +const mockGetServerSession = getServerSession as jest.MockedFunction; +const app = createTestServer(); + +beforeEach(async () => { + await resetIntegrationDatabase(); + mockGetServerSession.mockReset(); +}); + +afterAll(async () => { + await closeTestDatabase(); +}); + +// --------------------------------------------------------------------------- +// Health +// --------------------------------------------------------------------------- +describe("GET /api/v1/health", () => { + it("returns status ok when db and redis are reachable", async () => { + const res = await request(app).get("/api/v1/health"); + // May be 200 (ok) or 503 (degraded) depending on env; just assert shape + expect([200, 503]).toContain(res.status); + expect(res.body).toHaveProperty("status"); + expect(res.body).toHaveProperty("db"); + expect(res.body).toHaveProperty("redis"); + expect(res.body).toHaveProperty("timestamp"); + }); +}); + +// --------------------------------------------------------------------------- +// Profile +// --------------------------------------------------------------------------- +describe("Profile routes", () => { + describe("GET /api/v1/profile", () => { + it("returns 401 without auth", async () => { + mockGetServerSession.mockResolvedValue(null); + const res = await request(app).get("/api/v1/profile"); + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ success: false, error: "Unauthorized" }); + }); + + it("returns profile data for authenticated user", async () => { + const userId = await seedUser({ phone: "+15551000001", displayName: "Alice" }); + mockGetServerSession.mockResolvedValue({ user: { id: userId } }); + + const res = await request(app).get("/api/v1/profile"); + expect(res.status).toBe(200); + expect(res.body).toMatchObject({ + success: true, + data: { + id: userId, + phone: "+15551000001", + displayName: "Alice", + reputationScore: 0, + contributionStats: { total: 0, confirmed: 0, missed: 0 }, + }, + }); + }); + + it("reflects contribution stats from seeded data", async () => { + const userId = await seedUser({ phone: "+15551000002" }); + const creatorId = await seedUser({ phone: "+15551000003" }); + const circleId = await seedCircle({ creatorId }); + const memberId = await seedMember(circleId, userId); + await seedContribution(memberId, { status: "confirmed" }); + await seedContribution(memberId, { status: "missed", cycle: 2 }); + + mockGetServerSession.mockResolvedValue({ user: { id: userId } }); + const res = await request(app).get("/api/v1/profile"); + expect(res.status).toBe(200); + expect(res.body.data.contributionStats).toMatchObject({ + total: 2, + confirmed: 1, + missed: 1, + }); + }); + }); + + describe("PATCH /api/v1/profile", () => { + it("returns 401 without auth", async () => { + mockGetServerSession.mockResolvedValue(null); + const res = await request(app) + .patch("/api/v1/profile") + .send({ displayName: "Bob" }) + .set("Content-Type", "application/json"); + expect(res.status).toBe(401); + }); + + it("updates display name", async () => { + const userId = await seedUser({ phone: "+15551000004", displayName: "Old Name" }); + mockGetServerSession.mockResolvedValue({ user: { id: userId } }); + + const patchRes = await request(app) + .patch("/api/v1/profile") + .send({ displayName: "New Name" }) + .set("Content-Type", "application/json"); + expect(patchRes.status).toBe(200); + expect(patchRes.body).toMatchObject({ success: true, data: { updated: true } }); + + const getRes = await request(app).get("/api/v1/profile"); + expect(getRes.body.data.displayName).toBe("New Name"); + }); + + it("returns 400 for invalid stellar public key", async () => { + const userId = await seedUser({ phone: "+15551000005" }); + mockGetServerSession.mockResolvedValue({ user: { id: userId } }); + + const res = await request(app) + .patch("/api/v1/profile") + .send({ stellarPublicKey: "not-a-valid-key" }) + .set("Content-Type", "application/json"); + expect(res.status).toBe(400); + expect(res.body.success).toBe(false); + }); + }); +}); + +// --------------------------------------------------------------------------- +// Circles CRUD +// --------------------------------------------------------------------------- +describe("Circles routes", () => { + describe("GET /api/v1/circles", () => { + it("returns paginated list of open circles (no auth required)", async () => { + const creatorId = await seedUser({ phone: "+15552000001" }); + await seedCircle({ creatorId, name: "Public Circle", status: "open" }); + + const res = await request(app).get("/api/v1/circles"); + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.data).toHaveProperty("circles"); + expect(Array.isArray(res.body.data.circles)).toBe(true); + }); + + it("returns 401 for filter=mine without auth", async () => { + mockGetServerSession.mockResolvedValue(null); + const res = await request(app).get("/api/v1/circles?filter=mine"); + expect(res.status).toBe(401); + }); + + it("returns only user's circles for filter=mine", async () => { + const userId = await seedUser({ phone: "+15552000002" }); + const otherId = await seedUser({ phone: "+15552000003" }); + await seedCircle({ creatorId: userId, name: "My Circle" }); + await seedCircle({ creatorId: otherId, name: "Other Circle" }); + + mockGetServerSession.mockResolvedValue({ user: { id: userId } }); + const res = await request(app).get("/api/v1/circles?filter=mine"); + expect(res.status).toBe(200); + expect(res.body.data.every((c: { creatorId: string }) => c.creatorId === userId)).toBe(true); + }); + }); + + describe("POST /api/v1/circles", () => { + it("returns 401 without auth", async () => { + mockGetServerSession.mockResolvedValue(null); + const res = await request(app) + .post("/api/v1/circles") + .send({ name: "Test", contributionUsdc: "10", maxMembers: 5, cycleFrequency: "weekly" }) + .set("Content-Type", "application/json"); + expect(res.status).toBe(401); + }); + + it("creates a circle and returns 201", async () => { + const userId = await seedUser({ phone: "+15552000004" }); + mockGetServerSession.mockResolvedValue({ user: { id: userId } }); + + const res = await request(app) + .post("/api/v1/circles") + .send({ + name: "New Circle", + contributionUsdc: "10", + contributionFiat: "5000", + contributionCurrency: "NGN", + maxMembers: 5, + cycleFrequency: "weekly", + payoutMethod: "randomized", + }) + .set("Content-Type", "application/json"); + + expect(res.status).toBe(201); + expect(res.body).toMatchObject({ success: true }); + expect(res.body.data).toHaveProperty("id"); + expect(res.body.data.name).toBe("New Circle"); + }); + + it("returns 400 for invalid payload", async () => { + const userId = await seedUser({ phone: "+15552000005" }); + mockGetServerSession.mockResolvedValue({ user: { id: userId } }); + + const res = await request(app) + .post("/api/v1/circles") + .send({ name: "" }) // missing required fields + .set("Content-Type", "application/json"); + expect(res.status).toBe(400); + expect(res.body.success).toBe(false); + }); + }); + + describe("GET /api/v1/circles/:id", () => { + it("returns circle data with members (no auth required)", async () => { + const creatorId = await seedUser({ phone: "+15552000006" }); + const circleId = await seedCircle({ creatorId }); + await seedMember(circleId, creatorId, { position: 1 }); + + const res = await request(app).get(`/api/v1/circles/${circleId}`); + expect(res.status).toBe(200); + expect(res.body).toMatchObject({ success: true }); + expect(res.body.data.circle.id).toBe(circleId); + expect(Array.isArray(res.body.data.members)).toBe(true); + }); + + it("returns 404 for non-existent circle", async () => { + const res = await request(app).get("/api/v1/circles/00000000-0000-0000-0000-000000000000"); + expect(res.status).toBe(404); + expect(res.body).toMatchObject({ success: false, error: "Circle not found" }); + }); + }); +}); + +// --------------------------------------------------------------------------- +// Circle membership — join / leave +// --------------------------------------------------------------------------- +describe("Circle membership", () => { + describe("POST /api/v1/circles/:id/join", () => { + it("returns 401 without auth", async () => { + mockGetServerSession.mockResolvedValue(null); + const creatorId = await seedUser({ phone: "+15553000001" }); + const circleId = await seedCircle({ creatorId, maxMembers: 5 }); + + const res = await request(app) + .post(`/api/v1/circles/${circleId}/join`) + .send({}) + .set("Content-Type", "application/json"); + expect(res.status).toBe(401); + }); + + it("allows an authenticated user to join an open circle", async () => { + const creatorId = await seedUser({ phone: "+15553000002" }); + const joinerId = await seedUser({ phone: "+15553000003" }); + const circleId = await seedCircle({ creatorId, maxMembers: 5 }); + + mockGetServerSession.mockResolvedValue({ user: { id: joinerId } }); + const res = await request(app) + .post(`/api/v1/circles/${circleId}/join`) + .send({}) + .set("Content-Type", "application/json"); + + expect(res.status).toBe(201); + expect(res.body).toMatchObject({ success: true }); + expect(res.body.data).toHaveProperty("id"); + }); + + it("returns 404 for non-existent circle", async () => { + const userId = await seedUser({ phone: "+15553000004" }); + mockGetServerSession.mockResolvedValue({ user: { id: userId } }); + + const res = await request(app) + .post("/api/v1/circles/00000000-0000-0000-0000-000000000000/join") + .send({}) + .set("Content-Type", "application/json"); + expect(res.status).toBe(404); + }); + }); + + describe("POST /api/v1/circles/:id/leave", () => { + it("returns 401 without auth", async () => { + mockGetServerSession.mockResolvedValue(null); + const creatorId = await seedUser({ phone: "+15553000005" }); + const circleId = await seedCircle({ creatorId }); + + const res = await request(app) + .post(`/api/v1/circles/${circleId}/leave`) + .send({}) + .set("Content-Type", "application/json"); + expect(res.status).toBe(401); + }); + + it("allows a member to leave a circle", async () => { + const creatorId = await seedUser({ phone: "+15553000006" }); + const memberId = await seedUser({ phone: "+15553000007" }); + const circleId = await seedCircle({ creatorId, maxMembers: 5 }); + await seedMember(circleId, memberId, { position: 2 }); + + mockGetServerSession.mockResolvedValue({ user: { id: memberId } }); + const res = await request(app) + .post(`/api/v1/circles/${circleId}/leave`) + .send({}) + .set("Content-Type", "application/json"); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + }); + }); +}); + +// --------------------------------------------------------------------------- +// Waitlist +// --------------------------------------------------------------------------- +describe("Waitlist routes", () => { + it("returns 401 for all waitlist actions without auth", async () => { + mockGetServerSession.mockResolvedValue(null); + const creatorId = await seedUser({ phone: "+15554000001" }); + const circleId = await seedCircle({ creatorId, maxMembers: 1 }); + + const [getRes, postRes, deleteRes] = await Promise.all([ + request(app).get(`/api/v1/circles/${circleId}/waitlist`), + request(app).post(`/api/v1/circles/${circleId}/waitlist`).send({}).set("Content-Type", "application/json"), + request(app).delete(`/api/v1/circles/${circleId}/waitlist`), + ]); + + expect(getRes.status).toBe(401); + expect(postRes.status).toBe(401); + expect(deleteRes.status).toBe(401); + }); + + it("full waitlist lifecycle: join → read → leave", async () => { + const creatorId = await seedUser({ phone: "+15554000002" }); + const memberId = await seedUser({ phone: "+15554000003" }); + const waitlistUserId = await seedUser({ phone: "+15554000004" }); + const circleId = await seedCircle({ creatorId, maxMembers: 1 }); + await seedMember(circleId, memberId, { position: 1 }); + + mockGetServerSession.mockResolvedValue({ user: { id: waitlistUserId } }); + + const joinRes = await request(app) + .post(`/api/v1/circles/${circleId}/waitlist`) + .send({}) + .set("Content-Type", "application/json"); + expect(joinRes.status).toBe(200); + expect(joinRes.body).toMatchObject({ success: true, data: { isOnWaitlist: true } }); + + const readRes = await request(app).get(`/api/v1/circles/${circleId}/waitlist`); + expect(readRes.status).toBe(200); + expect(readRes.body.data.isOnWaitlist).toBe(true); + + const leaveRes = await request(app).delete(`/api/v1/circles/${circleId}/waitlist`); + expect(leaveRes.status).toBe(200); + expect(leaveRes.body.data.isOnWaitlist).toBe(false); + + const afterLeave = await request(app).get(`/api/v1/circles/${circleId}/waitlist`); + expect(afterLeave.body.data.isOnWaitlist).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// Auth routes +// --------------------------------------------------------------------------- +describe("Auth routes", () => { + describe("POST /api/v1/auth/send-otp", () => { + it("returns 400 for missing phone", async () => { + const res = await request(app) + .post("/api/v1/auth/send-otp") + .send({}) + .set("Content-Type", "application/json"); + expect(res.status).toBe(400); + expect(res.body.success).toBe(false); + }); + + it("returns 400 for invalid phone format", async () => { + const res = await request(app) + .post("/api/v1/auth/send-otp") + .send({ phone: "not-a-phone" }) + .set("Content-Type", "application/json"); + expect(res.status).toBe(400); + }); + + it("returns 200 for valid phone", async () => { + const res = await request(app) + .post("/api/v1/auth/send-otp") + .send({ phone: "+15559000001" }) + .set("Content-Type", "application/json"); + expect(res.status).toBe(200); + expect(res.body).toMatchObject({ success: true }); + }); + }); + + describe("POST /api/v1/auth/verify-otp", () => { + it("returns 400 for missing fields", async () => { + const res = await request(app) + .post("/api/v1/auth/verify-otp") + .send({}) + .set("Content-Type", "application/json"); + expect(res.status).toBe(400); + }); + }); + + describe("POST /api/v1/auth/logout", () => { + it("returns 200 and clears session", async () => { + const res = await request(app) + .post("/api/v1/auth/logout") + .send({}) + .set("Content-Type", "application/json"); + // Logout may return 200 regardless of auth state + expect([200, 401]).toContain(res.status); + }); + }); +}); + +// --------------------------------------------------------------------------- +// Unknown routes +// --------------------------------------------------------------------------- +describe("Unknown routes", () => { + it("returns 404 for unregistered paths", async () => { + const res = await request(app).get("/api/v1/does-not-exist"); + expect(res.status).toBe(404); + expect(res.body).toMatchObject({ success: false, error: "Route not found" }); + }); +}); diff --git a/src/__tests__/integration/supertest-app.ts b/src/__tests__/integration/supertest-app.ts new file mode 100644 index 0000000..2e965c6 --- /dev/null +++ b/src/__tests__/integration/supertest-app.ts @@ -0,0 +1,170 @@ +import * as http from "http"; +import { NextRequest } from "next/server"; +import { GET as getProfile, PATCH as patchProfile } from "../../app/api/v1/profile/route"; +import { + GET as getWaitlist, + POST as postWaitlist, + DELETE as deleteWaitlist, +} from "../../app/api/v1/circles/[id]/waitlist/route"; +import { GET as getCircles, POST as postCircles } from "../../app/api/v1/circles/route"; +import { GET as getCircle } from "../../app/api/v1/circles/[id]/route"; +import { POST as joinCircle } from "../../app/api/v1/circles/[id]/join/route"; +import { POST as leaveCircle } from "../../app/api/v1/circles/[id]/leave/route"; +import { GET as getHealth } from "../../app/api/v1/health/route"; +import { POST as sendOtp } from "../../app/api/v1/auth/send-otp/route"; +import { POST as verifyOtp } from "../../app/api/v1/auth/verify-otp/route"; +import { POST as logout } from "../../app/api/v1/auth/logout/route"; + +function normalizeHeaders(rawHeaders: http.IncomingHttpHeaders): Headers { + const headers = new Headers(); + for (const [name, value] of Object.entries(rawHeaders)) { + if (!name || value == null) continue; + if (Array.isArray(value)) { + headers.set(name, value.join(",")); + } else if (typeof value === "string") { + headers.set(name, value); + } + } + return headers; +} + +type ParsedRoute = + | { route: "health" } + | { route: "profile" } + | { route: "circles" } + | { route: "circle"; circleId: string } + | { route: "circleJoin"; circleId: string } + | { route: "circleLeave"; circleId: string } + | { route: "waitlist"; circleId: string } + | { route: "authSendOtp" } + | { route: "authVerifyOtp" } + | { route: "authLogout" }; + +function parseRoute(pathname: string): ParsedRoute | null { + if (pathname === "/api/v1/health") return { route: "health" }; + if (pathname === "/api/v1/profile") return { route: "profile" }; + if (pathname === "/api/v1/circles") return { route: "circles" }; + if (pathname === "/api/v1/auth/send-otp") return { route: "authSendOtp" }; + if (pathname === "/api/v1/auth/verify-otp") return { route: "authVerifyOtp" }; + if (pathname === "/api/v1/auth/logout") return { route: "authLogout" }; + + const circleMatch = pathname.match(/^\/api\/v1\/circles\/([^/]+)\/?$/); + if (circleMatch) return { route: "circle", circleId: circleMatch[1] }; + + const joinMatch = pathname.match(/^\/api\/v1\/circles\/([^/]+)\/join\/?$/); + if (joinMatch) return { route: "circleJoin", circleId: joinMatch[1] }; + + const leaveMatch = pathname.match(/^\/api\/v1\/circles\/([^/]+)\/leave\/?$/); + if (leaveMatch) return { route: "circleLeave", circleId: leaveMatch[1] }; + + const waitlistMatch = pathname.match(/^\/api\/v1\/circles\/([^/]+)\/waitlist\/?$/); + if (waitlistMatch) return { route: "waitlist", circleId: waitlistMatch[1] }; + + return null; +} + +async function readBody(req: http.IncomingMessage): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + req.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))); + req.on("end", () => resolve(Buffer.concat(chunks))); + req.on("error", reject); + }); +} + +export function createTestServer() { + return http.createServer(async (req, res) => { + try { + const url = new URL(req.url ?? "", "http://localhost"); + const method = req.method?.toUpperCase() ?? "GET"; + const headers = normalizeHeaders(req.headers); + + let bodyInit: BodyInit | undefined; + if (!["GET", "HEAD"].includes(method)) { + const rawBody = await readBody(req); + bodyInit = rawBody.length > 0 ? rawBody : undefined; + } + + const nextRequest = new NextRequest(url.href, { method, headers, body: bodyInit }); + + const route = parseRoute(url.pathname); + if (!route) { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ success: false, error: "Route not found" })); + return; + } + + let nextResponse: Response | undefined; + + switch (route.route) { + case "health": + if (method === "GET") nextResponse = await getHealth(); + break; + + case "profile": + if (method === "GET") nextResponse = await getProfile(); + else if (method === "PATCH") nextResponse = await patchProfile(nextRequest); + break; + + case "circles": + if (method === "GET") nextResponse = await getCircles(nextRequest); + else if (method === "POST") nextResponse = await postCircles(nextRequest); + break; + + case "circle": { + const ctx = { params: { id: route.circleId } }; + if (method === "GET") nextResponse = await getCircle(nextRequest, ctx); + break; + } + + case "circleJoin": { + const ctx = { params: { id: route.circleId } }; + if (method === "POST") nextResponse = await joinCircle(nextRequest, ctx); + break; + } + + case "circleLeave": { + const ctx = { params: { id: route.circleId } }; + if (method === "POST") nextResponse = await leaveCircle(nextRequest, ctx); + break; + } + + case "waitlist": { + const ctx = { params: { id: route.circleId } }; + if (method === "GET") nextResponse = await getWaitlist(nextRequest, ctx); + else if (method === "POST") nextResponse = await postWaitlist(nextRequest, ctx); + else if (method === "DELETE") nextResponse = await deleteWaitlist(nextRequest, ctx); + break; + } + + case "authSendOtp": + if (method === "POST") nextResponse = await sendOtp(nextRequest); + break; + + case "authVerifyOtp": + if (method === "POST") nextResponse = await verifyOtp(nextRequest); + break; + + case "authLogout": + if (method === "POST") nextResponse = await logout(nextRequest); + break; + } + + if (!nextResponse) { + res.writeHead(405, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ success: false, error: "Method not allowed" })); + return; + } + + nextResponse.headers.forEach((value, name) => { + res.setHeader(name, value); + }); + res.statusCode = nextResponse.status; + const payload = await nextResponse.text(); + res.end(payload); + } catch (error) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ success: false, error: String(error) })); + } + }); +} diff --git a/src/__tests__/integration/test-db.ts b/src/__tests__/integration/test-db.ts new file mode 100644 index 0000000..9981331 --- /dev/null +++ b/src/__tests__/integration/test-db.ts @@ -0,0 +1,120 @@ +import { randomUUID } from "crypto"; +import { query, closePool } from "../../lib/db"; + +export async function resetIntegrationDatabase() { + await query( + `TRUNCATE circle_waitlist, contributions, members, circles, users RESTART IDENTITY CASCADE` + ); +} + +export async function seedUser(overrides?: { + id?: string; + phone?: string; + displayName?: string; + email?: string | null; + stellarPublicKey?: string | null; + reputationScore?: number; +}) { + const id = overrides?.id ?? randomUUID(); + await query( + `INSERT INTO users (id, phone, display_name, email, stellar_public_key, reputation_score, created_at) + VALUES ($1, $2, $3, $4, $5, $6, NOW())`, + [ + id, + overrides?.phone ?? "+15551234567", + overrides?.displayName ?? "Integration User", + overrides?.email ?? null, + overrides?.stellarPublicKey ?? null, + overrides?.reputationScore ?? 0, + ] + ); + return id; +} + +export async function seedCircle(options: { + creatorId: string; + id?: string; + name?: string; + maxMembers?: number; + status?: string; + contributionUsdc?: string; + contributionFiat?: string; + contributionCurrency?: string; + circleType?: string; + payoutMethod?: string; +}) { + const id = options.id ?? randomUUID(); + await query( + `INSERT INTO circles + (id, name, creator_id, contribution_usdc, contribution_fiat, contribution_currency, + max_members, cycle_frequency, payout_method, contract_id, grace_period_hours, status, + current_cycle, created_at, updated_at) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,NOW(),NOW())`, + [ + id, + options.name ?? "Integration Circle", + options.creatorId, + options.contributionUsdc ?? "1.0000000", + options.contributionFiat ?? "100.00", + options.contributionCurrency ?? "NGN", + options.maxMembers ?? 5, + "weekly", + options.payoutMethod ?? "randomized", + null, + 24, + options.status ?? "open", + 0, + ] + ); + return id; +} + +export async function seedMember( + circleId: string, + userId: string, + overrides?: { + id?: string; + position?: number; + status?: string; + } +) { + const id = overrides?.id ?? randomUUID(); + await query( + `INSERT INTO members + (id, circle_id, user_id, position, status, has_received_payout, joined_at) + VALUES ($1, $2, $3, $4, $5, false, NOW())`, + [id, circleId, userId, overrides?.position ?? 1, overrides?.status ?? "active"] + ); + return id; +} + +export async function seedContribution( + memberId: string, + overrides?: { + id?: string; + cycle?: number; + status?: string; + amountUsdc?: string; + paystackReference?: string; + } +) { + const id = overrides?.id ?? randomUUID(); + await query( + `INSERT INTO contributions + (id, member_id, cycle, status, amount_usdc, paystack_reference, created_at) + VALUES ($1, $2, $3, $4, $5, $6, NOW())`, + [ + id, + memberId, + overrides?.cycle ?? 1, + overrides?.status ?? "confirmed", + overrides?.amountUsdc ?? "1.0000000", + overrides?.paystackReference ?? `ref_${randomUUID()}`, + ] + ); + return id; +} + +export async function closeTestDatabase() { + await closePool(); +}