From 189641e202ffd25ee5b46b0aa1bf2b756aab45a1 Mon Sep 17 00:00:00 2001 From: ohamamarachi474-del Date: Thu, 28 May 2026 21:58:40 +0100 Subject: [PATCH 1/3] feat: implement scheduled job processing for claim scoring and user reputation using BullMQ --- package-lock.json | 346 ++++++++++++++++-- package.json | 5 + src/app.module.ts | 19 + .../services/audit-trail.service.spec.ts | 2 +- src/claims/claim-resolution.service.spec.ts | 2 +- src/claims/claims.service.spec.ts | 19 + src/claims/claims.service.ts | 23 +- src/claims/entities/claim.entity.ts | 2 +- src/jobs/jobs.module.ts | 21 +- src/jobs/jobs.processor.ts | 28 ++ src/jobs/jobs.service.spec.ts | 150 ++++++++ src/jobs/jobs.service.ts | 39 +- 12 files changed, 608 insertions(+), 48 deletions(-) create mode 100644 src/jobs/jobs.processor.ts create mode 100644 src/jobs/jobs.service.spec.ts diff --git a/package-lock.json b/package-lock.json index 19f9494..389f24f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,11 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@bull-board/api": "^7.1.5", + "@bull-board/express": "^7.1.5", + "@bull-board/nestjs": "^7.1.5", "@libsql/client": "^0.17.0", + "@nestjs/bullmq": "^11.0.4", "@nestjs/common": "^11.1.12", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.1.12", @@ -24,6 +28,7 @@ "@prisma/adapter-libsql": "^7.3.0", "@prisma/client": "^7.3.0", "@types/pg": "^8.16.0", + "bullmq": "^5.77.6", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", "dotenv": "^17.2.3", @@ -256,6 +261,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -751,6 +757,55 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/@bull-board/api": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-7.1.5.tgz", + "integrity": "sha512-EW0sbTtGIysu9vipdVpPQeToPqOpPgVZTt+pn1Ut3gbSS/GLWbEgIfFtMmSQDUoSL9WH00RzjgUY5K+43nWh0A==", + "license": "MIT", + "peer": true, + "dependencies": { + "redis-info": "^3.1.0" + }, + "peerDependencies": { + "@bull-board/ui": "7.1.5" + } + }, + "node_modules/@bull-board/express": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-7.1.5.tgz", + "integrity": "sha512-kp4SzhVjZlykryiQwcOhJjDhiLbBnZoAMoSgEstzqQ0raLw+jERRC6ryJ0MIQO+SO+Jv9EjjxrXCR8O2YSP/eg==", + "license": "MIT", + "dependencies": { + "@bull-board/api": "7.1.5", + "@bull-board/ui": "7.1.5", + "ejs": "^5.0.2", + "express": "^5.2.1" + } + }, + "node_modules/@bull-board/nestjs": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-7.1.5.tgz", + "integrity": "sha512-1y+HkjnDaZoSCXJRsiYfBNBVx+PX3I8x3Uv+SSJuSpt2vHifMRwFbChO3XDxeWXetT1eR+yqPVq6ub5eJwNOYQ==", + "license": "MIT", + "peerDependencies": { + "@bull-board/api": "^7.1.5", + "@nestjs/bull-shared": "^10.0.0 || ^11.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.8.1" + } + }, + "node_modules/@bull-board/ui": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-7.1.5.tgz", + "integrity": "sha512-2IkatKwNRx/1M9/lAZIptcxS1FPNq6icpp2M46Upwd4olVxs/ujF9Kvs+Ff9ExtIO/OgYfwx7mG2IprGZ+nQCg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@bull-board/api": "7.1.5" + } + }, "node_modules/@chevrotain/cst-dts-gen": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", @@ -842,7 +897,8 @@ "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz", "integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==", "devOptional": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@electric-sql/pglite-socket": { "version": "0.0.20", @@ -1459,9 +1515,9 @@ } }, "node_modules/@ioredis/commands": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz", - "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", "license": "MIT" }, "node_modules/@isaacs/cliui": { @@ -2242,6 +2298,84 @@ "node": ">=16" } }, + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@napi-rs/nice": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", @@ -2571,6 +2705,35 @@ "integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==", "license": "MIT" }, + "node_modules/@nestjs/bull-shared": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-11.0.4.tgz", + "integrity": "sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "2.8.1" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/bullmq": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-11.0.4.tgz", + "integrity": "sha512-wBzK9raAVG0/6NTMdvLGM4/FQ1lsB35/pYS8L6a0SDgkTiLpd7mAjQ8R692oMx5s7IjvgntaZOuTUrKYLNfIkA==", + "license": "MIT", + "dependencies": { + "@nestjs/bull-shared": "^11.0.4", + "tslib": "2.8.1" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "bullmq": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "11.0.16", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.16.tgz", @@ -2622,6 +2785,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -2792,6 +2956,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.14.tgz", "integrity": "sha512-IN/tlqd7Nl9gl6f0jsWEuOrQDaCI9vHzxv0fisHysfBQzfQIkqlv5A7w4Qge02BUQyczXT9HHPgHtWHCxhjRng==", "license": "MIT", + "peer": true, "dependencies": { "file-type": "21.3.0", "iterare": "1.2.1", @@ -2851,6 +3016,7 @@ "integrity": "sha512-7OXPPMoDr6z+5NkoQKu4hOhfjz/YYqM3bNilPqv1WVFWrzSmuNXxvhbX69YMmNmRYascPXiwESqf5jJdjKXEww==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -2934,6 +3100,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.14.tgz", "integrity": "sha512-Fs+/j+mBSBSXErOQJ/YdUn/HqJGSJ4pGfiJyYOyz04l42uNVnqEakvu1kXLbxMabR6vd6/h9d6Bi4tso9p7o4Q==", "license": "MIT", + "peer": true, "dependencies": { "cors": "2.8.6", "express": "5.2.1", @@ -3633,6 +3800,7 @@ "integrity": "sha512-Q5FsI3Cw0fGMXhmsg7c08i4EmXCrcl+WnAxb6LYOLHw4JFFC3yzmx9LaXZ7QMbA+JZXbigU2TirI7RAfO0Qlnw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@xhmikosr/bin-wrapper": "^13.0.5", @@ -3718,6 +3886,7 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" @@ -4090,6 +4259,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -4251,6 +4421,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4420,6 +4591,7 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -5270,6 +5442,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "devOptional": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5365,6 +5538,7 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5974,6 +6148,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6057,6 +6232,32 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/bullmq": { + "version": "5.77.6", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.77.6.tgz", + "integrity": "sha512-WCpSoCD4vWyRD+btOsFrO7iBGInrTgG155gTZCV8qY0Yex2KtsbVtFERx6V1WZ2xWl/5ZxnLar8Z8ufnS4f5jg==", + "license": "MIT", + "peer": true, + "dependencies": { + "cron-parser": "4.9.0", + "ioredis": "5.10.1", + "msgpackr": "2.0.1", + "node-abort-controller": "3.1.1", + "semver": "7.8.0", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "redis": ">=5.0.0" + }, + "peerDependenciesMeta": { + "redis": { + "optional": true + } + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -6396,6 +6597,7 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -6462,13 +6664,15 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/class-validator": { "version": "0.14.3", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", "license": "MIT", + "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -6895,6 +7099,18 @@ "url": "https://ko-fi.com/intcreator" } }, + "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==", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cross-fetch": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", @@ -6943,8 +7159,7 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "devOptional": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", @@ -7288,6 +7503,18 @@ "fast-check": "^3.23.1" } }, + "node_modules/ejs": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.2.tgz", + "integrity": "sha512-IpbUaI/CAW86l3f+T8zN0iggSc0LmMZLcIW5eRVStLVNCoTXkE0YlncbbH50fp8Cl6zHIky0sW2uUbhBqGw0Jw==", + "license": "Apache-2.0", + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.12.18" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.302", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", @@ -7493,6 +7720,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7553,6 +7781,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -7922,6 +8151,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -9010,6 +9240,7 @@ "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -9263,12 +9494,13 @@ } }, "node_modules/ioredis": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.3.tgz", - "integrity": "sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==", + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", "license": "MIT", + "peer": true, "dependencies": { - "@ioredis/commands": "1.5.0", + "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", @@ -9658,6 +9890,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -11306,6 +11539,37 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/msgpackr": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.1.tgz", + "integrity": "sha512-9J+tqTEsbHqY8YohazYgty7LgerFIWxvMLpUjqETSmjHojtJm2WnX2kK/2a1fLI7CO7ERP1YSEUXMucz4j+yBA==", + "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==", + "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/multer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", @@ -11383,6 +11647,7 @@ "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", @@ -11471,7 +11736,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", - "dev": true, "license": "MIT" }, "node_modules/node-addon-api": { @@ -11560,6 +11824,21 @@ "node": ">= 10.12.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==", + "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-gyp/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -11926,6 +12205,7 @@ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", "license": "MIT", + "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -12071,6 +12351,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", @@ -12180,6 +12461,7 @@ "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", "license": "MIT", + "peer": true, "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", @@ -12211,6 +12493,7 @@ "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-10.5.0.tgz", "integrity": "sha512-hD91XjgaKkSsdn8P7LaebrNzhGTdB086W3pyPihX0EzGPjq5uBJBXo4N5guqNaK6mUjg9aubMF7wDViYek9dRA==", "license": "MIT", + "peer": true, "dependencies": { "get-caller-file": "^2.0.5", "pino": "^9.0.0", @@ -12505,6 +12788,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -12563,6 +12847,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "7.4.1", "@prisma/dev": "0.20.0", @@ -12929,6 +13214,15 @@ "node": ">=4" } }, + "node_modules/redis-info": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redis-info/-/redis-info-3.1.0.tgz", + "integrity": "sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.11" + } + }, "node_modules/redis-parser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", @@ -12945,7 +13239,8 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/regexp-to-ast": { "version": "0.5.0", @@ -13196,6 +13491,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -13257,8 +13553,7 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "devOptional": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/schema-utils": { "version": "3.3.0", @@ -13310,9 +13605,9 @@ } }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -13769,6 +14064,7 @@ "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", "hasInstallScript": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^7.0.0", @@ -14316,6 +14612,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -14699,6 +14996,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -14871,6 +15169,7 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", "license": "MIT", + "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^4.2.0", @@ -15088,6 +15387,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15869,7 +16169,6 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -15888,7 +16187,6 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -15902,7 +16200,6 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -15917,7 +16214,6 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -15927,8 +16223,7 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/webpack/node_modules/schema-utils": { "version": "4.3.3", @@ -15936,7 +16231,6 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -16089,6 +16383,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -16223,6 +16518,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 9eb3aab..6d7bc84 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,11 @@ "benchmark:compare": "ts-node scripts/compare-benchmarks.ts" }, "dependencies": { + "@bull-board/api": "^7.1.5", + "@bull-board/express": "^7.1.5", + "@bull-board/nestjs": "^7.1.5", "@libsql/client": "^0.17.0", + "@nestjs/bullmq": "^11.0.4", "@nestjs/common": "^11.1.12", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.1.12", @@ -42,6 +46,7 @@ "@prisma/adapter-libsql": "^7.3.0", "@prisma/client": "^7.3.0", "@types/pg": "^8.16.0", + "bullmq": "^5.77.6", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", "dotenv": "^17.2.3", diff --git a/src/app.module.ts b/src/app.module.ts index ce3374f..2d1b0f8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,5 +1,8 @@ import { Module, Logger } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; +import { BullModule } from '@nestjs/bullmq'; +import { BullBoardModule } from '@bull-board/nestjs'; +import { ExpressAdapter } from '@bull-board/express'; import { ThrottlerModule } from '@nestjs/throttler'; import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { AppController } from './app.controller'; @@ -245,6 +248,22 @@ async function createThrottlerStorage(configService: ConfigService): Promise ({ + connection: { + host: configService.get('REDIS_HOST', 'localhost'), + port: configService.get('REDIS_PORT', 6379), + password: configService.get('REDIS_PASSWORD'), + db: configService.get('REDIS_DB', 0), + }, + }), + }), + BullBoardModule.forRoot({ + route: '/admin/queues', + adapter: ExpressAdapter, + }), RedisModule, LoggerModule, AuthModule, diff --git a/src/audit/services/audit-trail.service.spec.ts b/src/audit/services/audit-trail.service.spec.ts index 5f785cb..04be722 100644 --- a/src/audit/services/audit-trail.service.spec.ts +++ b/src/audit/services/audit-trail.service.spec.ts @@ -8,7 +8,7 @@ import { AuditActionType, AuditEntityType } from '../entities/audit-log.entity'; describe('AuditTrailService - IP Security', () => { let service: AuditTrailService; - let repository: jest.Mocked>; + let repository: any; let mockRequest: any; beforeEach(async () => { diff --git a/src/claims/claim-resolution.service.spec.ts b/src/claims/claim-resolution.service.spec.ts index 09e0420..8dabb03 100644 --- a/src/claims/claim-resolution.service.spec.ts +++ b/src/claims/claim-resolution.service.spec.ts @@ -1,7 +1,7 @@ import { ClaimResolutionService } from "./claim-resolution.service"; describe('Confidence Scoring', () => { - const service = new ClaimResolutionService(null as any); + const service = new ClaimResolutionService(null as any, null as any); it('returns high confidence for strong consensus', () => { const score = service.computeConfidenceScore({ diff --git a/src/claims/claims.service.spec.ts b/src/claims/claims.service.spec.ts index df7f4ed..1d3c8c0 100644 --- a/src/claims/claims.service.spec.ts +++ b/src/claims/claims.service.spec.ts @@ -1,4 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { BadRequestException } from '@nestjs/common'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ClaimsService } from './claims.service'; @@ -194,6 +195,24 @@ describe('ClaimsService', () => { expect(redisService.del).toHaveBeenCalledWith('claims:latest'); }); + + it('should throw BadRequestException if claim content length exceeds 5000 characters', async () => { + const longContent = 'a'.repeat(5001); + const createClaimDto = ClaimFactory.createCreateClaimDto({ content: longContent }); + + await expect(service.createClaim(createClaimDto)).rejects.toThrow( + new BadRequestException('Claim content exceeds maximum length of 5000 characters') + ); + }); + + it('should throw BadRequestException if claim title length exceeds 200 characters', async () => { + const longTitle = 'a'.repeat(201); + const createClaimDto = ClaimFactory.createCreateClaimDto({ title: longTitle }); + + await expect(service.createClaim(createClaimDto)).rejects.toThrow( + new BadRequestException('Claim title exceeds maximum length of 200 characters') + ); + }); }); describe('findOne', () => { diff --git a/src/claims/claims.service.ts b/src/claims/claims.service.ts index bff5121..29fa8ef 100644 --- a/src/claims/claims.service.ts +++ b/src/claims/claims.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger, BadRequestException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Claim } from './entities/claim.entity'; @@ -89,11 +89,17 @@ export class ClaimsService { captureAfterState: true, }) async createClaim(createClaimDto: CreateClaimDto): Promise { + if (createClaimDto.title && createClaimDto.title.length > 200) { + throw new BadRequestException('Claim title exceeds maximum length of 200 characters'); + } + if (createClaimDto.content && createClaimDto.content.length > 5000) { + throw new BadRequestException('Claim content exceeds maximum length of 5000 characters'); + } const claim = this.claimRepo.create({ title: createClaimDto.title, content: createClaimDto.content, - source: createClaimDto.source, - metadata: createClaimDto.metadata, + source: createClaimDto.source ?? null, + metadata: createClaimDto.metadata ?? null, resolvedVerdict: null, // Will be computed later confidenceScore: null, // Will be computed later finalized: false, @@ -172,16 +178,5 @@ export class ClaimsService { return updatedClaim; } - async findOne(id: string): Promise { - const claim = await this.repo.findOne({ - where: { id }, - }); - - if (!claim) { - throw new NotFoundException(`Claim with id ${id} not found`); - } - - return claim; - } } diff --git a/src/claims/entities/claim.entity.ts b/src/claims/entities/claim.entity.ts index 0c4a36c..ce832de 100644 --- a/src/claims/entities/claim.entity.ts +++ b/src/claims/entities/claim.entity.ts @@ -12,7 +12,7 @@ export class Claim { @Column({ type: 'varchar', length: 200 }) title: string; - @Column({ type: 'text' }) + @Column({ type: 'varchar', length: 5000 }) content: string; @Column({ type: 'varchar', length: 500, nullable: true }) diff --git a/src/jobs/jobs.module.ts b/src/jobs/jobs.module.ts index f23ec4f..6cd8f97 100644 --- a/src/jobs/jobs.module.ts +++ b/src/jobs/jobs.module.ts @@ -7,10 +7,25 @@ import { Wallet } from '../entities/wallet.entity'; import { Claim } from '../claims/entities/claim.entity'; import { User } from '../entities/user.entity'; import { AggregationModule } from '../aggregation/aggregation.module'; +import { BullModule } from '@nestjs/bullmq'; +import { BullBoardModule } from '@bull-board/nestjs'; +import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; +import { JobsProcessor } from './jobs.processor'; @Module({ - imports: [RedisModule, TypeOrmModule.forFeature([Stake, Wallet, Claim, User]), AggregationModule], - providers: [JobsService], - exports: [JobsService], + imports: [ + RedisModule, + TypeOrmModule.forFeature([Stake, Wallet, Claim, User]), + AggregationModule, + BullModule.registerQueue({ + name: 'jobs-queue', + }), + BullBoardModule.forFeature({ + name: 'jobs-queue', + adapter: BullMQAdapter, + }), + ], + providers: [JobsService, JobsProcessor], + exports: [JobsService, BullModule], }) export class JobsModule {} diff --git a/src/jobs/jobs.processor.ts b/src/jobs/jobs.processor.ts new file mode 100644 index 0000000..646a96c --- /dev/null +++ b/src/jobs/jobs.processor.ts @@ -0,0 +1,28 @@ +import { Processor, WorkerHost } from '@nestjs/bullmq'; +import { Job } from 'bullmq'; +import { Injectable, Logger } from '@nestjs/common'; +import { JobsService } from './jobs.service'; + +@Processor('jobs-queue') +@Injectable() +export class JobsProcessor extends WorkerHost { + private readonly logger = new Logger(JobsProcessor.name); + + constructor(private readonly jobsService: JobsService) { + super(); + } + + async process(job: Job): Promise { + this.logger.log(`Processing job ${job.id} of name ${job.name}`); + switch (job.name) { + case 'compute-scores': + await this.jobsService.computeScores(); + return { success: true }; + case 'compute-reputation': + await this.jobsService.computeReputation(); + return { success: true }; + default: + throw new Error(`Unknown job name: ${job.name}`); + } + } +} diff --git a/src/jobs/jobs.service.spec.ts b/src/jobs/jobs.service.spec.ts new file mode 100644 index 0000000..015c56c --- /dev/null +++ b/src/jobs/jobs.service.spec.ts @@ -0,0 +1,150 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { JobsService } from './jobs.service'; +import { JobsProcessor } from './jobs.processor'; +import { Stake } from '../staking/entities/stake.entity'; +import { Wallet } from '../entities/wallet.entity'; +import { Claim } from '../claims/entities/claim.entity'; +import { User } from '../entities/user.entity'; +import { ClaimsCache } from '../cache/claims.cache'; +import { RedisService } from '../redis/redis.service'; +import { AggregationService } from '../aggregation/aggregation.service'; +import { getQueueToken } from '@nestjs/bullmq'; +import { Repository } from 'typeorm'; +import { Job } from 'bullmq'; + +describe('Jobs (BullMQ & Scheduling)', () => { + let service: JobsService; + let processor: JobsProcessor; + let queueMock: any; + let stakeRepo: Repository; + let walletRepo: Repository; + let claimRepo: Repository; + let userRepo: Repository; + + beforeEach(async () => { + queueMock = { + getRepeatableJobs: jest.fn().mockResolvedValue([ + { key: 'old-scores-key' }, + { key: 'old-reputation-key' }, + ]), + removeRepeatableByKey: jest.fn().mockResolvedValue(true), + add: jest.fn().mockResolvedValue({ id: 'new-job' }), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + JobsService, + JobsProcessor, + { + provide: getQueueToken('jobs-queue'), + useValue: queueMock, + }, + { + provide: getRepositoryToken(Stake), + useClass: Repository, + }, + { + provide: getRepositoryToken(Wallet), + useClass: Repository, + }, + { + provide: getRepositoryToken(Claim), + useClass: Repository, + }, + { + provide: getRepositoryToken(User), + useClass: Repository, + }, + { + provide: ClaimsCache, + useValue: { + invalidateClaim: jest.fn(), + }, + }, + { + provide: RedisService, + useValue: {}, + }, + { + provide: AggregationService, + useValue: { + aggregate: jest.fn().mockReturnValue({ + confidence: 60, + status: 'VERIFIED_TRUE', + }), + }, + }, + ], + }).compile(); + + service = module.get(JobsService); + processor = module.get(JobsProcessor); + stakeRepo = module.get>(getRepositoryToken(Stake)); + walletRepo = module.get>(getRepositoryToken(Wallet)); + claimRepo = module.get>(getRepositoryToken(Claim)); + userRepo = module.get>(getRepositoryToken(User)); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + expect(processor).toBeDefined(); + }); + + describe('onModuleInit', () => { + it('should clear old repeatable jobs and schedule new ones', async () => { + await service.onModuleInit(); + + expect(queueMock.getRepeatableJobs).toHaveBeenCalled(); + expect(queueMock.removeRepeatableByKey).toHaveBeenCalledTimes(2); + expect(queueMock.removeRepeatableByKey).toHaveBeenNthCalledWith(1, 'old-scores-key'); + expect(queueMock.removeRepeatableByKey).toHaveBeenNthCalledWith(2, 'old-reputation-key'); + + expect(queueMock.add).toHaveBeenCalledTimes(2); + expect(queueMock.add).toHaveBeenNthCalledWith(1, 'compute-scores', {}, expect.any(Object)); + expect(queueMock.add).toHaveBeenNthCalledWith(2, 'compute-reputation', {}, expect.any(Object)); + }); + }); + + describe('JobsProcessor', () => { + it('should invoke computeScores when processing compute-scores job', async () => { + const computeScoresSpy = jest.spyOn(service, 'computeScores').mockResolvedValue(undefined); + + const mockJob = { + id: '1', + name: 'compute-scores', + data: {}, + } as Job; + + const result = await processor.process(mockJob); + + expect(computeScoresSpy).toHaveBeenCalled(); + expect(result).toEqual({ success: true }); + }); + + it('should invoke computeReputation when processing compute-reputation job', async () => { + const computeReputationSpy = jest.spyOn(service, 'computeReputation').mockResolvedValue(undefined); + + const mockJob = { + id: '2', + name: 'compute-reputation', + data: {}, + } as Job; + + const result = await processor.process(mockJob); + + expect(computeReputationSpy).toHaveBeenCalled(); + expect(result).toEqual({ success: true }); + }); + + it('should throw error for unknown job name', async () => { + const mockJob = { + id: '3', + name: 'unknown-job', + data: {}, + } as Job; + + await expect(processor.process(mockJob)).rejects.toThrow('Unknown job name: unknown-job'); + }); + }); +}); diff --git a/src/jobs/jobs.service.ts b/src/jobs/jobs.service.ts index 5168202..ea96d48 100644 --- a/src/jobs/jobs.service.ts +++ b/src/jobs/jobs.service.ts @@ -8,6 +8,8 @@ import { Claim } from '../claims/entities/claim.entity'; import { User } from '../entities/user.entity'; import { AggregationService } from '../aggregation/aggregation.service'; import { ClaimsCache } from '../cache/claims.cache'; +import { InjectQueue } from '@nestjs/bullmq'; +import { Queue } from 'bullmq'; /** * JobsService @@ -29,18 +31,49 @@ export class JobsService implements OnModuleInit, OnModuleDestroy { @InjectRepository(User) private readonly userRepo: Repository, private readonly claimsCache: ClaimsCache, + @InjectQueue('jobs-queue') private readonly jobsQueue: Queue, private readonly aggregationService?: AggregationService, ) { } async onModuleInit() { - this.logger.log('JobsService initialized (bullmq to be integrated)'); + this.logger.log('JobsService initialized. Registering repeatable BullMQ jobs...'); + try { + const repeatableJobs = await this.jobsQueue.getRepeatableJobs(); + for (const rJob of repeatableJobs) { + await this.jobsQueue.removeRepeatableByKey(rJob.key); + } + + await this.jobsQueue.add( + 'compute-scores', + {}, + { + repeat: { + pattern: '0 * * * *', // hourly + }, + jobId: 'compute-scores-job', + }, + ); + await this.jobsQueue.add( + 'compute-reputation', + {}, + { + repeat: { + pattern: '0 0 * * *', // daily + }, + jobId: 'compute-reputation-job', + }, + ); + this.logger.log('Repeatable BullMQ jobs registered successfully'); + } catch (err) { + this.logger.error(`Failed to register repeatable BullMQ jobs: ${err.message}`); + } } async onModuleDestroy() { this.logger.log('JobsService shutdown'); } - private async computeScores() { + async computeScores() { this.logger.debug('computeScores: starting'); // Process claims in small batches @@ -105,7 +138,7 @@ export class JobsService implements OnModuleInit, OnModuleDestroy { this.logger.debug('computeScores: finished'); } - private async computeReputation() { + async computeReputation() { this.logger.debug('computeReputation: starting'); // Process users in batches From 378fa05936839bb0e7a6b77c799167dc44a8656b Mon Sep 17 00:00:00 2001 From: ohamamarachi474-del Date: Thu, 28 May 2026 21:59:58 +0100 Subject: [PATCH 2/3] test: add unit tests for AuditTrailService to verify IP spoofing protection logic --- src/audit/services/audit-trail.service.spec.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/audit/services/audit-trail.service.spec.ts b/src/audit/services/audit-trail.service.spec.ts index 04be722..e6137f5 100644 --- a/src/audit/services/audit-trail.service.spec.ts +++ b/src/audit/services/audit-trail.service.spec.ts @@ -153,6 +153,7 @@ describe('AuditTrailService - IP Security', () => { const serviceWithoutRequest = moduleWithoutRequest.get(AuditTrailService); + const innerRepo = moduleWithoutRequest.get(getRepositoryToken(AuditLog)) as any; const auditInput = { actionType: AuditActionType.CLAIM_CREATED, @@ -161,15 +162,15 @@ describe('AuditTrailService - IP Security', () => { description: 'Test without request', }; - repository.create.mockReturnValue({ + innerRepo.create.mockReturnValue({ ...auditInput, ipAddress: undefined, }); - repository.save.mockResolvedValue({ id: 'audit-4' }); + innerRepo.save.mockResolvedValue({ id: 'audit-4' }); await serviceWithoutRequest.log(auditInput); - expect(repository.create).toHaveBeenCalledWith( + expect(innerRepo.create).toHaveBeenCalledWith( expect.objectContaining({ ipAddress: undefined, }), From 1bf1a9c6b4d59c919a633e9f0300ad9666e87a97 Mon Sep 17 00:00:00 2001 From: Superray23 Date: Fri, 29 May 2026 08:40:36 +0100 Subject: [PATCH 3/3] feat: add BullMQ job scheduling and system health monitoring endpoint --- src/app.controller.spec.ts | 84 ++++++++++++++++++- src/app.controller.ts | 67 ++++++++++++++- src/jobs/jobs.module.ts | 2 + src/jobs/jobs.processor.ts | 3 + src/jobs/jobs.service.spec.ts | 44 +++++++++- src/jobs/jobs.service.ts | 19 +++++ .../sybil-resistance.service.spec.ts | 37 ++++++++ .../sybil-resistance.service.ts | 19 +++++ .../sybil-resistant-voting.service.spec.ts | 7 ++ 9 files changed, 279 insertions(+), 3 deletions(-) diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts index d22f389..a3f6e48 100644 --- a/src/app.controller.spec.ts +++ b/src/app.controller.spec.ts @@ -1,17 +1,55 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { RedisService } from './redis/redis.service'; +import { DataSource } from 'typeorm'; + +jest.mock('./prisma/prisma.service', () => { + return { + PrismaService: jest.fn().mockImplementation(() => ({ + $queryRaw: jest.fn().mockResolvedValue([1]), + })), + }; +}); + +import { PrismaService } from './prisma/prisma.service'; describe('AppController', () => { let appController: AppController; + let redisService: RedisService; + let prismaService: PrismaService; + let dataSource: DataSource; beforeEach(async () => { const app: TestingModule = await Test.createTestingModule({ controllers: [AppController], - providers: [AppService], + providers: [ + AppService, + { + provide: RedisService, + useValue: { + isHealthy: jest.fn().mockResolvedValue(true), + }, + }, + { + provide: PrismaService, + useValue: { + $queryRaw: jest.fn().mockResolvedValue([1]), + }, + }, + { + provide: DataSource, + useValue: { + query: jest.fn().mockResolvedValue([1]), + }, + }, + ], }).compile(); appController = app.get(AppController); + redisService = app.get(RedisService); + prismaService = app.get(PrismaService); + dataSource = app.get(DataSource); }); describe('root', () => { @@ -19,4 +57,48 @@ describe('AppController', () => { expect(appController.getHello()).toBe('Hello World!'); }); }); + + describe('health', () => { + it('should return healthy status when all services are healthy', async () => { + const health = await appController.getHealth(); + + expect(health.status).toBe('OK'); + expect(health.services.database).toBe('healthy'); + expect(health.services.prisma).toBe('healthy'); + expect(health.services.redis).toBe('healthy'); + }); + + it('should return error status when TypeORM database is unhealthy', async () => { + jest.spyOn(dataSource, 'query').mockRejectedValueOnce(new Error('Connection lost')); + + const health = await appController.getHealth(); + + expect(health.status).toBe('Error'); + expect(health.services.database).toContain('unhealthy'); + expect(health.services.prisma).toBe('healthy'); + expect(health.services.redis).toBe('healthy'); + }); + + it('should return error status when Prisma database is unhealthy', async () => { + jest.spyOn(prismaService, '$queryRaw').mockRejectedValueOnce(new Error('Prisma error')); + + const health = await appController.getHealth(); + + expect(health.status).toBe('Error'); + expect(health.services.database).toBe('healthy'); + expect(health.services.prisma).toContain('unhealthy'); + expect(health.services.redis).toBe('healthy'); + }); + + it('should return error status when Redis is unhealthy', async () => { + jest.spyOn(redisService, 'isHealthy').mockResolvedValueOnce(false); + + const health = await appController.getHealth(); + + expect(health.status).toBe('Error'); + expect(health.services.database).toBe('healthy'); + expect(health.services.prisma).toBe('healthy'); + expect(health.services.redis).toBe('unhealthy'); + }); + }); }); diff --git a/src/app.controller.ts b/src/app.controller.ts index af5204f..a953fb0 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,14 +1,79 @@ import { Controller, Get } from '@nestjs/common'; import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { AppService } from './app.service'; +import { RedisService } from './redis/redis.service'; +import { PrismaService } from './prisma/prisma.service'; +import { DataSource } from 'typeorm'; +import { Public } from './decorators/public.decorator'; @ApiTags('health') @Controller() export class AppController { - constructor(private readonly appService: AppService) {} + constructor( + private readonly appService: AppService, + private readonly redisService: RedisService, + private readonly prismaService: PrismaService, + private readonly dataSource: DataSource, + ) {} @Get() getHello(): string { return this.appService.getHello(); } + + @Public() + @Get('health') + @ApiOperation({ summary: 'Get application health status' }) + async getHealth() { + let dbStatus = 'healthy'; + let prismaStatus = 'healthy'; + let redisStatus = 'healthy'; + let isHealthy = true; + + // Check TypeORM DB + try { + const result = await this.dataSource.query('SELECT 1'); + if (!result || result.length === 0) { + dbStatus = 'unhealthy'; + isHealthy = false; + } + } catch (err) { + dbStatus = `unhealthy: ${err.message}`; + isHealthy = false; + } + + // Check Prisma DB + try { + const result = await this.prismaService.$queryRaw`SELECT 1`; + if (!result) { + prismaStatus = 'unhealthy'; + isHealthy = false; + } + } catch (err) { + prismaStatus = `unhealthy: ${err.message}`; + isHealthy = false; + } + + // Check Redis + try { + const redisHealthy = await this.redisService.isHealthy(); + if (!redisHealthy) { + redisStatus = 'unhealthy'; + isHealthy = false; + } + } catch (err) { + redisStatus = `unhealthy: ${err.message}`; + isHealthy = false; + } + + return { + status: isHealthy ? 'OK' : 'Error', + timestamp: new Date().toISOString(), + services: { + database: dbStatus, + prisma: prismaStatus, + redis: redisStatus, + }, + }; + } } diff --git a/src/jobs/jobs.module.ts b/src/jobs/jobs.module.ts index 6cd8f97..46bfc2c 100644 --- a/src/jobs/jobs.module.ts +++ b/src/jobs/jobs.module.ts @@ -11,12 +11,14 @@ import { BullModule } from '@nestjs/bullmq'; import { BullBoardModule } from '@bull-board/nestjs'; import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; import { JobsProcessor } from './jobs.processor'; +import { SybilResistanceModule } from '../sybil-resistance/sybil-resistance.module'; @Module({ imports: [ RedisModule, TypeOrmModule.forFeature([Stake, Wallet, Claim, User]), AggregationModule, + SybilResistanceModule, BullModule.registerQueue({ name: 'jobs-queue', }), diff --git a/src/jobs/jobs.processor.ts b/src/jobs/jobs.processor.ts index 646a96c..4893a09 100644 --- a/src/jobs/jobs.processor.ts +++ b/src/jobs/jobs.processor.ts @@ -21,6 +21,9 @@ export class JobsProcessor extends WorkerHost { case 'compute-reputation': await this.jobsService.computeReputation(); return { success: true }; + case 'cleanup-sybil-history': + const deletedCount = await this.jobsService.cleanupSybilHistory(); + return { success: true, deletedCount }; default: throw new Error(`Unknown job name: ${job.name}`); } diff --git a/src/jobs/jobs.service.spec.ts b/src/jobs/jobs.service.spec.ts index 015c56c..022131b 100644 --- a/src/jobs/jobs.service.spec.ts +++ b/src/jobs/jobs.service.spec.ts @@ -9,6 +9,14 @@ import { User } from '../entities/user.entity'; import { ClaimsCache } from '../cache/claims.cache'; import { RedisService } from '../redis/redis.service'; import { AggregationService } from '../aggregation/aggregation.service'; + +jest.mock('../prisma/prisma.service', () => { + return { + PrismaService: jest.fn().mockImplementation(() => ({})), + }; +}); + +import { SybilResistanceService } from '../sybil-resistance/sybil-resistance.service'; import { getQueueToken } from '@nestjs/bullmq'; import { Repository } from 'typeorm'; import { Job } from 'bullmq'; @@ -17,6 +25,7 @@ describe('Jobs (BullMQ & Scheduling)', () => { let service: JobsService; let processor: JobsProcessor; let queueMock: any; + let sybilResistanceServiceMock: any; let stakeRepo: Repository; let walletRepo: Repository; let claimRepo: Repository; @@ -32,6 +41,10 @@ describe('Jobs (BullMQ & Scheduling)', () => { add: jest.fn().mockResolvedValue({ id: 'new-job' }), }; + sybilResistanceServiceMock = { + cleanupScoreHistory: jest.fn().mockResolvedValue(42), + }; + const module: TestingModule = await Test.createTestingModule({ providers: [ JobsService, @@ -40,6 +53,10 @@ describe('Jobs (BullMQ & Scheduling)', () => { provide: getQueueToken('jobs-queue'), useValue: queueMock, }, + { + provide: SybilResistanceService, + useValue: sybilResistanceServiceMock, + }, { provide: getRepositoryToken(Stake), useClass: Repository, @@ -100,9 +117,19 @@ describe('Jobs (BullMQ & Scheduling)', () => { expect(queueMock.removeRepeatableByKey).toHaveBeenNthCalledWith(1, 'old-scores-key'); expect(queueMock.removeRepeatableByKey).toHaveBeenNthCalledWith(2, 'old-reputation-key'); - expect(queueMock.add).toHaveBeenCalledTimes(2); + expect(queueMock.add).toHaveBeenCalledTimes(3); expect(queueMock.add).toHaveBeenNthCalledWith(1, 'compute-scores', {}, expect.any(Object)); expect(queueMock.add).toHaveBeenNthCalledWith(2, 'compute-reputation', {}, expect.any(Object)); + expect(queueMock.add).toHaveBeenNthCalledWith(3, 'cleanup-sybil-history', {}, expect.any(Object)); + }); + }); + + describe('cleanupSybilHistory', () => { + it('should call sybilResistanceService cleanupScoreHistory and return deleted count', async () => { + const result = await service.cleanupSybilHistory(); + + expect(sybilResistanceServiceMock.cleanupScoreHistory).toHaveBeenCalled(); + expect(result).toBe(42); }); }); @@ -137,6 +164,21 @@ describe('Jobs (BullMQ & Scheduling)', () => { expect(result).toEqual({ success: true }); }); + it('should invoke cleanupSybilHistory when processing cleanup-sybil-history job', async () => { + const cleanupSybilHistorySpy = jest.spyOn(service, 'cleanupSybilHistory').mockResolvedValue(123); + + const mockJob = { + id: '4', + name: 'cleanup-sybil-history', + data: {}, + } as Job; + + const result = await processor.process(mockJob); + + expect(cleanupSybilHistorySpy).toHaveBeenCalled(); + expect(result).toEqual({ success: true, deletedCount: 123 }); + }); + it('should throw error for unknown job name', async () => { const mockJob = { id: '3', diff --git a/src/jobs/jobs.service.ts b/src/jobs/jobs.service.ts index ea96d48..913f771 100644 --- a/src/jobs/jobs.service.ts +++ b/src/jobs/jobs.service.ts @@ -10,6 +10,7 @@ import { AggregationService } from '../aggregation/aggregation.service'; import { ClaimsCache } from '../cache/claims.cache'; import { InjectQueue } from '@nestjs/bullmq'; import { Queue } from 'bullmq'; +import { SybilResistanceService } from '../sybil-resistance/sybil-resistance.service'; /** * JobsService @@ -32,6 +33,7 @@ export class JobsService implements OnModuleInit, OnModuleDestroy { private readonly userRepo: Repository, private readonly claimsCache: ClaimsCache, @InjectQueue('jobs-queue') private readonly jobsQueue: Queue, + private readonly sybilResistanceService: SybilResistanceService, private readonly aggregationService?: AggregationService, ) { } @@ -63,6 +65,16 @@ export class JobsService implements OnModuleInit, OnModuleDestroy { jobId: 'compute-reputation-job', }, ); + await this.jobsQueue.add( + 'cleanup-sybil-history', + {}, + { + repeat: { + pattern: '0 2 * * *', // daily at 2:00 AM + }, + jobId: 'cleanup-sybil-history-job', + }, + ); this.logger.log('Repeatable BullMQ jobs registered successfully'); } catch (err) { this.logger.error(`Failed to register repeatable BullMQ jobs: ${err.message}`); @@ -73,6 +85,13 @@ export class JobsService implements OnModuleInit, OnModuleDestroy { this.logger.log('JobsService shutdown'); } + async cleanupSybilHistory(): Promise { + this.logger.debug('cleanupSybilHistory: starting'); + const count = await this.sybilResistanceService.cleanupScoreHistory(); + this.logger.debug(`cleanupSybilHistory: deleted ${count} old records`); + return count; + } + async computeScores() { this.logger.debug('computeScores: starting'); diff --git a/src/sybil-resistance/sybil-resistance.service.spec.ts b/src/sybil-resistance/sybil-resistance.service.spec.ts index 8046607..90b1151 100644 --- a/src/sybil-resistance/sybil-resistance.service.spec.ts +++ b/src/sybil-resistance/sybil-resistance.service.spec.ts @@ -1,5 +1,24 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SybilResistanceService } from './sybil-resistance.service'; + +jest.mock('../prisma/prisma.service', () => { + return { + PrismaService: jest.fn().mockImplementation(() => ({ + user: { + findUnique: jest.fn(), + findMany: jest.fn(), + update: jest.fn(), + }, + sybilScore: { + create: jest.fn(), + findFirst: jest.fn(), + findMany: jest.fn(), + deleteMany: jest.fn(), + }, + })), + }; +}); + import { PrismaService } from '../prisma/prisma.service'; import { NotFoundException } from '@nestjs/common'; @@ -41,6 +60,7 @@ describe('SybilResistanceService', () => { create: jest.fn(), findFirst: jest.fn(), findMany: jest.fn(), + deleteMany: jest.fn(), }, }, }, @@ -518,4 +538,21 @@ describe('SybilResistanceService', () => { expect(score1).toBe(score2); }); }); + + describe('cleanupScoreHistory', () => { + it('should delete scores older than 1 year and return count', async () => { + jest.spyOn(prisma.sybilScore, 'deleteMany').mockResolvedValueOnce({ count: 99 }); + + const result = await service.cleanupScoreHistory(); + + expect(prisma.sybilScore.deleteMany).toHaveBeenCalledWith({ + where: { + createdAt: { + lt: expect.any(Date), + }, + }, + }); + expect(result).toBe(99); + }); + }); }); diff --git a/src/sybil-resistance/sybil-resistance.service.ts b/src/sybil-resistance/sybil-resistance.service.ts index 69ced8a..4dbe5a1 100644 --- a/src/sybil-resistance/sybil-resistance.service.ts +++ b/src/sybil-resistance/sybil-resistance.service.ts @@ -357,4 +357,23 @@ Final score: ${Number(composite.toFixed(4))} (weighted average)`, details: score.calculationDetails ? JSON.parse(score.calculationDetails) : null, }; } + + /** + * Delete SybilScore history records older than 1 year. + * Returns the count of deleted records. + */ + async cleanupScoreHistory(): Promise { + const oneYearAgo = new Date(); + oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1); + + const result = await this.prisma.sybilScore.deleteMany({ + where: { + createdAt: { + lt: oneYearAgo, + }, + }, + }); + + return result.count; + } } diff --git a/src/sybil-resistance/sybil-resistant-voting.service.spec.ts b/src/sybil-resistance/sybil-resistant-voting.service.spec.ts index eed2fef..4517697 100644 --- a/src/sybil-resistance/sybil-resistant-voting.service.spec.ts +++ b/src/sybil-resistance/sybil-resistant-voting.service.spec.ts @@ -1,5 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SybilResistantVotingService } from './sybil-resistant-voting.service'; + +jest.mock('../prisma/prisma.service', () => { + return { + PrismaService: jest.fn().mockImplementation(() => ({})), + }; +}); + import { SybilResistanceService } from './sybil-resistance.service'; describe('SybilResistantVotingService', () => {