diff --git a/Backend/jest.config.js b/Backend/jest.config.js new file mode 100644 index 00000000..89ba304b --- /dev/null +++ b/Backend/jest.config.js @@ -0,0 +1,22 @@ +/** @type {import('jest').Config} */ +const config = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/tests'], + testMatch: ['**/*.test.ts'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1' + }, + clearMocks: true, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + '!src/generated/**', + '!src/server.ts' + ], + transform: { + '^.+\\.tsx?$': ['ts-jest', { tsconfig: './tsconfig.test.json' }] + } +}; + +module.exports = config; diff --git a/Backend/package-lock.json b/Backend/package-lock.json index 87a6afa3..7891b301 100644 --- a/Backend/package-lock.json +++ b/Backend/package-lock.json @@ -8,10 +8,10 @@ "name": "e-learning-backend", "version": "1.0.0", "dependencies": { + "@node-rs/bcrypt": "^1.10.7", "@pdf-lib/fontkit": "^1.1.1", "@prisma/client": "^5.22.0", "@tsoa/runtime": "^6.4.0", - "bcrypt": "^5.1.1", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.2", @@ -33,6 +33,7 @@ "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", + "@types/jest": "^30.0.0", "@types/jsonwebtoken": "^9.0.7", "@types/multer": "^1.4.12", "@types/node": "^22.10.5", @@ -1364,6 +1365,37 @@ "kuler": "^2.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -2350,6 +2382,16 @@ } } }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/environment": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", @@ -2411,6 +2453,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/globals": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", @@ -2427,6 +2479,30 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/reporters": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", @@ -2626,24 +2702,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "license": "BSD-3-Clause", + "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", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "license": "MIT", + "optional": true, "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" } }, "node_modules/@noble/hashes": { @@ -2659,6 +2727,259 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@node-rs/bcrypt": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt/-/bcrypt-1.10.7.tgz", + "integrity": "sha512-1wk0gHsUQC/ap0j6SJa2K34qNhomxXRcEe3T8cI5s+g6fgHBgLTN7U9LzWTG/HE6G4+2tWWLeCabk1wiYGEQSA==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@node-rs/bcrypt-android-arm-eabi": "1.10.7", + "@node-rs/bcrypt-android-arm64": "1.10.7", + "@node-rs/bcrypt-darwin-arm64": "1.10.7", + "@node-rs/bcrypt-darwin-x64": "1.10.7", + "@node-rs/bcrypt-freebsd-x64": "1.10.7", + "@node-rs/bcrypt-linux-arm-gnueabihf": "1.10.7", + "@node-rs/bcrypt-linux-arm64-gnu": "1.10.7", + "@node-rs/bcrypt-linux-arm64-musl": "1.10.7", + "@node-rs/bcrypt-linux-x64-gnu": "1.10.7", + "@node-rs/bcrypt-linux-x64-musl": "1.10.7", + "@node-rs/bcrypt-wasm32-wasi": "1.10.7", + "@node-rs/bcrypt-win32-arm64-msvc": "1.10.7", + "@node-rs/bcrypt-win32-ia32-msvc": "1.10.7", + "@node-rs/bcrypt-win32-x64-msvc": "1.10.7" + } + }, + "node_modules/@node-rs/bcrypt-android-arm-eabi": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm-eabi/-/bcrypt-android-arm-eabi-1.10.7.tgz", + "integrity": "sha512-8dO6/PcbeMZXS3VXGEtct9pDYdShp2WBOWlDvSbcRwVqyB580aCBh0BEFmKYtXLzLvUK8Wf+CG3U6sCdILW1lA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-android-arm64": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm64/-/bcrypt-android-arm64-1.10.7.tgz", + "integrity": "sha512-UASFBS/CucEMHiCtL/2YYsAY01ZqVR1N7vSb94EOvG5iwW7BQO06kXXCTgj+Xbek9azxixrCUmo3WJnkJZ0hTQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-darwin-arm64": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-arm64/-/bcrypt-darwin-arm64-1.10.7.tgz", + "integrity": "sha512-DgzFdAt455KTuiJ/zYIyJcKFobjNDR/hnf9OS7pK5NRS13Nq4gLcSIIyzsgHwZHxsJWbLpHmFc1H23Y7IQoQBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-darwin-x64": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-x64/-/bcrypt-darwin-x64-1.10.7.tgz", + "integrity": "sha512-SPWVfQ6sxSokoUWAKWD0EJauvPHqOGQTd7CxmYatcsUgJ/bruvEHxZ4bIwX1iDceC3FkOtmeHO0cPwR480n/xA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-freebsd-x64": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-freebsd-x64/-/bcrypt-freebsd-x64-1.10.7.tgz", + "integrity": "sha512-gpa+Ixs6GwEx6U6ehBpsQetzUpuAGuAFbOiuLB2oo4N58yU4AZz1VIcWyWAHrSWRs92O0SHtmo2YPrMrwfBbSw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-linux-arm-gnueabihf": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm-gnueabihf/-/bcrypt-linux-arm-gnueabihf-1.10.7.tgz", + "integrity": "sha512-kYgJnTnpxrzl9sxYqzflobvMp90qoAlaX1oDL7nhNTj8OYJVDIk0jQgblj0bIkjmoPbBed53OJY/iu4uTS+wig==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-linux-arm64-gnu": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-gnu/-/bcrypt-linux-arm64-gnu-1.10.7.tgz", + "integrity": "sha512-7cEkK2RA+gBCj2tCVEI1rDSJV40oLbSq7bQ+PNMHNI6jCoXGmj9Uzo7mg7ZRbNZ7piIyNH5zlJqutjo8hh/tmA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-linux-arm64-musl": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-musl/-/bcrypt-linux-arm64-musl-1.10.7.tgz", + "integrity": "sha512-X7DRVjshhwxUqzdUKDlF55cwzh+wqWJ2E/tILvZPboO3xaNO07Um568Vf+8cmKcz+tiZCGP7CBmKbBqjvKN/Pw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-linux-x64-gnu": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-gnu/-/bcrypt-linux-x64-gnu-1.10.7.tgz", + "integrity": "sha512-LXRZsvG65NggPD12hn6YxVgH0W3VR5fsE/o1/o2D5X0nxKcNQGeLWnRzs5cP8KpoFOuk1ilctXQJn8/wq+Gn/Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-linux-x64-musl": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-musl/-/bcrypt-linux-x64-musl-1.10.7.tgz", + "integrity": "sha512-tCjHmct79OfcO3g5q21ME7CNzLzpw1MAsUXCLHLGWH+V6pp/xTvMbIcLwzkDj6TI3mxK6kehTn40SEjBkZ3Rog==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-wasm32-wasi": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-wasm32-wasi/-/bcrypt-wasm32-wasi-1.10.7.tgz", + "integrity": "sha512-4qXSihIKeVXYglfXZEq/QPtYtBUvR8d3S85k15Lilv3z5B6NSGQ9mYiNleZ7QHVLN2gEc5gmi7jM353DMH9GkA==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@node-rs/bcrypt-win32-arm64-msvc": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-arm64-msvc/-/bcrypt-win32-arm64-msvc-1.10.7.tgz", + "integrity": "sha512-FdfUQrqmDfvC5jFhntMBkk8EI+fCJTx/I1v7Rj+Ezlr9rez1j1FmuUnywbBj2Cg15/0BDhwYdbyZ5GCMFli2aQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-win32-ia32-msvc": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-ia32-msvc/-/bcrypt-win32-ia32-msvc-1.10.7.tgz", + "integrity": "sha512-lZLf4Cx+bShIhU071p5BZft4OvP4PGhyp542EEsb3zk34U5GLsGIyCjOafcF/2DGewZL6u8/aqoxbSuROkgFXg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-win32-x64-msvc": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-x64-msvc/-/bcrypt-win32-x64-msvc-1.10.7.tgz", + "integrity": "sha512-hdw7tGmN1DxVAMTzICLdaHpXjy+4rxaxnBMgI8seG1JL5e3VcRGsd1/1vVDogVp2cbsmgq+6d6yAY+D9lW/DCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@paralleldrive/cuid2": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", @@ -3599,6 +3920,16 @@ "yarn": ">=1.9.4" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", @@ -3789,6 +4120,230 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@types/jest/node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4186,12 +4741,6 @@ "license": "(Unlicense OR Apache-2.0)", "optional": true }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC" - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -4228,18 +4777,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4317,26 +4854,6 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, - "node_modules/aproba": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", - "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "license": "ISC" - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -4527,20 +5044,6 @@ "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/bcrypt": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", - "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.11", - "node-addon-api": "^5.0.0" - }, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -4888,15 +5391,6 @@ "node": ">= 6" } }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -5013,15 +5507,6 @@ "node": ">=12.20" } }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "bin": { - "color-support": "bin.js" - } - }, "node_modules/color/node_modules/color-convert": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", @@ -5070,6 +5555,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, "license": "MIT" }, "node_modules/concat-stream": { @@ -5117,12 +5603,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC" - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -5232,6 +5712,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5313,12 +5794,6 @@ "node": ">=0.4.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT" - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -5338,15 +5813,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "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==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -6203,40 +6669,11 @@ "node": ">=14.14" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -6263,27 +6700,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -6386,6 +6802,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -6419,6 +6836,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -6429,6 +6847,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -6538,12 +6957,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC" - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -6592,19 +7005,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -6696,6 +7096,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -7910,30 +8311,6 @@ "yallist": "^3.0.2" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -8124,37 +8501,6 @@ "node": ">=8" } }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -8214,32 +8560,6 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, - "node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", - "license": "MIT" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8339,21 +8659,6 @@ "node": ">=4" } }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "license": "ISC", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -8377,19 +8682,6 @@ "node": ">=8" } }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8427,6 +8719,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -8584,6 +8877,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9096,22 +9390,6 @@ "node": ">=10" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -9239,12 +9517,6 @@ "node": ">= 0.8.0" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -9365,6 +9637,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, "license": "ISC" }, "node_modules/simple-update-notifier": { @@ -9737,41 +10010,6 @@ "express": ">=4.0.0 || >=5.0.0-beta" } }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -9913,12 +10151,6 @@ "nodetouch": "bin/nodetouch.js" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -10045,7 +10277,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, + "devOptional": true, "license": "0BSD" }, "node_modules/tsoa": { @@ -10301,22 +10533,6 @@ "@zxing/text-encoding": "0.9.0" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -10353,15 +10569,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, "node_modules/winston": { "version": "3.19.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", @@ -10453,6 +10660,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/Backend/package.json b/Backend/package.json index 392d6b82..8f71c83c 100644 --- a/Backend/package.json +++ b/Backend/package.json @@ -44,6 +44,7 @@ "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", + "@types/jest": "^30.0.0", "@types/jsonwebtoken": "^9.0.7", "@types/multer": "^1.4.12", "@types/node": "^22.10.5", diff --git a/Backend/pnpm-lock.yaml b/Backend/pnpm-lock.yaml index f7f8d01b..a30e07ad 100644 --- a/Backend/pnpm-lock.yaml +++ b/Backend/pnpm-lock.yaml @@ -78,6 +78,9 @@ importers: '@types/express': specifier: ^5.0.0 version: 5.0.6 + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 '@types/jsonwebtoken': specifier: ^9.0.7 version: 9.0.10 @@ -119,7 +122,7 @@ importers: version: 7.2.2 ts-jest: specifier: ^29.2.5 - version: 29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.5))(typescript@5.9.3) + version: 29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.5))(typescript@5.9.3) tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 @@ -615,6 +618,10 @@ packages: node-notifier: optional: true + '@jest/diff-sequences@30.0.1': + resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/environment@29.7.0': resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -623,6 +630,10 @@ packages: resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/expect-utils@30.2.0': + resolution: {integrity: sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/expect@29.7.0': resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -631,10 +642,18 @@ packages: resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/get-type@30.1.0': + resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/globals@29.7.0': resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/pattern@30.0.1': + resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/reporters@29.7.0': resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -648,6 +667,10 @@ packages: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/schemas@30.0.5': + resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/source-map@29.6.3': resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -668,6 +691,10 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/types@30.2.0': + resolution: {integrity: sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -862,6 +889,9 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@sinclair/typebox@0.34.48': + resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} + '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} @@ -1115,6 +1145,9 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@types/jest@30.0.0': + resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1433,6 +1466,10 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} @@ -1729,6 +1766,10 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + expect@30.2.0: + resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + express-rate-limit@7.5.1: resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} engines: {node: '>= 16'} @@ -2118,6 +2159,10 @@ packages: resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-diff@30.2.0: + resolution: {integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-docblock@29.7.0: resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2146,14 +2191,26 @@ packages: resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-matcher-utils@30.2.0: + resolution: {integrity: sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-message-util@29.7.0: resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-message-util@30.2.0: + resolution: {integrity: sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-mock@29.7.0: resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-mock@30.2.0: + resolution: {integrity: sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-pnp-resolver@1.2.3: resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} engines: {node: '>=6'} @@ -2167,6 +2224,10 @@ packages: resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-regex-util@30.0.1: + resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-resolve-dependencies@29.7.0: resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2191,6 +2252,10 @@ packages: resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-util@30.2.0: + resolution: {integrity: sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-validate@29.7.0: resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2584,6 +2649,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-format@30.2.0: + resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + prisma@5.22.0: resolution: {integrity: sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==} engines: {node: '>=16.13'} @@ -4027,6 +4096,8 @@ snapshots: - supports-color - ts-node + '@jest/diff-sequences@30.0.1': {} + '@jest/environment@29.7.0': dependencies: '@jest/fake-timers': 29.7.0 @@ -4038,6 +4109,10 @@ snapshots: dependencies: jest-get-type: 29.6.3 + '@jest/expect-utils@30.2.0': + dependencies: + '@jest/get-type': 30.1.0 + '@jest/expect@29.7.0': dependencies: expect: 29.7.0 @@ -4054,6 +4129,8 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 + '@jest/get-type@30.1.0': {} + '@jest/globals@29.7.0': dependencies: '@jest/environment': 29.7.0 @@ -4063,6 +4140,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/pattern@30.0.1': + dependencies: + '@types/node': 22.19.5 + jest-regex-util: 30.0.1 + '@jest/reporters@29.7.0': dependencies: '@bcoe/v8-coverage': 0.2.3 @@ -4096,6 +4178,10 @@ snapshots: dependencies: '@sinclair/typebox': 0.27.8 + '@jest/schemas@30.0.5': + dependencies: + '@sinclair/typebox': 0.34.48 + '@jest/source-map@29.6.3': dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -4145,6 +4231,16 @@ snapshots: '@types/yargs': 17.0.35 chalk: 4.1.2 + '@jest/types@30.2.0': + dependencies: + '@jest/pattern': 30.0.1 + '@jest/schemas': 30.0.5 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.19.5 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4316,6 +4412,8 @@ snapshots: '@sinclair/typebox@0.27.8': {} + '@sinclair/typebox@0.34.48': {} + '@sinonjs/commons@3.0.1': dependencies: type-detect: 4.0.8 @@ -4721,6 +4819,11 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 + '@types/jest@30.0.0': + dependencies: + expect: 30.2.0 + pretty-format: 30.2.0 + '@types/json-schema@7.0.15': {} '@types/jsonwebtoken@9.0.10': @@ -5116,6 +5219,8 @@ snapshots: ci-info@3.9.0: {} + ci-info@4.4.0: {} + cjs-module-lexer@1.4.3: {} cliui@8.0.1: @@ -5398,6 +5503,15 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 + expect@30.2.0: + dependencies: + '@jest/expect-utils': 30.2.0 + '@jest/get-type': 30.1.0 + jest-matcher-utils: 30.2.0 + jest-message-util: 30.2.0 + jest-mock: 30.2.0 + jest-util: 30.2.0 + express-rate-limit@7.5.1(express@4.22.1): dependencies: express: 4.22.1 @@ -5872,6 +5986,13 @@ snapshots: jest-get-type: 29.6.3 pretty-format: 29.7.0 + jest-diff@30.2.0: + dependencies: + '@jest/diff-sequences': 30.0.1 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.2.0 + jest-docblock@29.7.0: dependencies: detect-newline: 3.1.0 @@ -5923,6 +6044,13 @@ snapshots: jest-get-type: 29.6.3 pretty-format: 29.7.0 + jest-matcher-utils@30.2.0: + dependencies: + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + jest-diff: 30.2.0 + pretty-format: 30.2.0 + jest-message-util@29.7.0: dependencies: '@babel/code-frame': 7.27.1 @@ -5935,18 +6063,38 @@ snapshots: slash: 3.0.0 stack-utils: 2.0.6 + jest-message-util@30.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 30.2.0 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 30.2.0 + slash: 3.0.0 + stack-utils: 2.0.6 + jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 '@types/node': 22.19.5 jest-util: 29.7.0 + jest-mock@30.2.0: + dependencies: + '@jest/types': 30.2.0 + '@types/node': 22.19.5 + jest-util: 30.2.0 + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): optionalDependencies: jest-resolve: 29.7.0 jest-regex-util@29.6.3: {} + jest-regex-util@30.0.1: {} + jest-resolve-dependencies@29.7.0: dependencies: jest-regex-util: 29.6.3 @@ -6053,6 +6201,15 @@ snapshots: graceful-fs: 4.2.11 picomatch: 2.3.1 + jest-util@30.2.0: + dependencies: + '@jest/types': 30.2.0 + '@types/node': 22.19.5 + chalk: 4.1.2 + ci-info: 4.4.0 + graceful-fs: 4.2.11 + picomatch: 4.0.3 + jest-validate@29.7.0: dependencies: '@jest/types': 29.6.3 @@ -6444,6 +6601,12 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + pretty-format@30.2.0: + dependencies: + '@jest/schemas': 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + prisma@5.22.0: dependencies: '@prisma/engines': 5.22.0 @@ -6797,7 +6960,7 @@ snapshots: ts-deepmerge@7.0.3: {} - ts-jest@29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.5))(typescript@5.9.3): + ts-jest@29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.5))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -6813,9 +6976,9 @@ snapshots: optionalDependencies: '@babel/core': 7.28.5 '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 + '@jest/types': 30.2.0 babel-jest: 29.7.0(@babel/core@7.28.5) - jest-util: 29.7.0 + jest-util: 30.2.0 tsconfig-paths@4.2.0: dependencies: diff --git a/Backend/tests/tsconfig.json b/Backend/tests/tsconfig.json new file mode 100644 index 00000000..330cd8fd --- /dev/null +++ b/Backend/tests/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.test.json", + "compilerOptions": { + "rootDir": "..", + "types": [ + "node", + "jest" + ] + }, + "include": [ + "../src/**/*", + "./**/*" + ] +} \ No newline at end of file diff --git a/Backend/tests/unit/validators/AdminCourseApproval.validator.test.ts b/Backend/tests/unit/validators/AdminCourseApproval.validator.test.ts new file mode 100644 index 00000000..9068ad48 --- /dev/null +++ b/Backend/tests/unit/validators/AdminCourseApproval.validator.test.ts @@ -0,0 +1,67 @@ +import { + ApproveCourseValidator, + RejectCourseValidator, +} from '@/validators/AdminCourseApproval.validator'; + +describe('ApproveCourseValidator', () => { + it('should pass with no body (comment optional)', () => { + const { error } = ApproveCourseValidator.validate({}); + expect(error).toBeUndefined(); + }); + + it('should pass with optional comment', () => { + const { error } = ApproveCourseValidator.validate({ + comment: 'Looks great!', + }); + expect(error).toBeUndefined(); + }); + + it('should fail when comment exceeds 1000 characters', () => { + const { error } = ApproveCourseValidator.validate({ + comment: 'a'.repeat(1001), + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/must not exceed 1000/i); + }); + + it('should pass with comment exactly 1000 characters', () => { + const { error } = ApproveCourseValidator.validate({ + comment: 'a'.repeat(1000), + }); + expect(error).toBeUndefined(); + }); +}); + +describe('RejectCourseValidator', () => { + it('should pass with valid rejection comment', () => { + const { error } = RejectCourseValidator.validate({ + comment: 'The content is incomplete and needs more details.', + }); + expect(error).toBeUndefined(); + }); + + it('should fail without comment', () => { + const { error } = RejectCourseValidator.validate({}); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Comment is required when rejecting/i); + }); + + it('should fail when comment is too short (< 10 chars)', () => { + const { error } = RejectCourseValidator.validate({ comment: 'Too short' }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/at least 10 characters/i); + }); + + it('should pass with exactly 10 characters', () => { + const { error } = RejectCourseValidator.validate({ comment: '1234567890' }); + expect(error).toBeUndefined(); + }); + + it('should fail when comment exceeds 1000 characters', () => { + const { error } = RejectCourseValidator.validate({ + comment: 'a'.repeat(1001), + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/must not exceed 1000/i); + }); +}); diff --git a/Backend/tests/unit/validators/ChaptersLesson.validator.test.ts b/Backend/tests/unit/validators/ChaptersLesson.validator.test.ts new file mode 100644 index 00000000..7e80bc68 --- /dev/null +++ b/Backend/tests/unit/validators/ChaptersLesson.validator.test.ts @@ -0,0 +1,263 @@ +import { + CreateChapterValidator, + UpdateChapterValidator, + ReorderChapterValidator, + CreateLessonValidator, + UpdateLessonValidator, + ReorderLessonsValidator, + AddQuestionValidator, + UpdateQuizValidator, +} from '@/validators/ChaptersLesson.validator'; + + +// ============================================================ +// Chapter Validators +// ============================================================ + +describe('CreateChapterValidator', () => { + it('should pass with valid data', () => { + const { error } = CreateChapterValidator.validate({ + title: { th: 'บทที่ 1', en: 'Chapter 1' }, + }); + expect(error).toBeUndefined(); + }); + + it('should pass with optional fields', () => { + const { error } = CreateChapterValidator.validate({ + title: { th: 'บทที่ 1', en: 'Chapter 1' }, + description: { th: 'คำอธิบาย', en: 'Description' }, + sort_order: 0, + }); + expect(error).toBeUndefined(); + }); + + it('should fail if title is missing', () => { + const { error } = CreateChapterValidator.validate({}); + expect(error).toBeDefined(); + }); + + it('should fail if title.th is missing', () => { + const { error } = CreateChapterValidator.validate({ + title: { en: 'Chapter 1' }, + }); + expect(error).toBeDefined(); + }); + + it('should fail if title.en is missing', () => { + const { error } = CreateChapterValidator.validate({ + title: { th: 'บทที่ 1' }, + }); + expect(error).toBeDefined(); + }); +}); + +describe('UpdateChapterValidator', () => { + it('should pass with empty object (all fields optional)', () => { + const { error } = UpdateChapterValidator.validate({}); + expect(error).toBeUndefined(); + }); + + it('should pass with partial fields', () => { + const { error } = UpdateChapterValidator.validate({ + is_published: true, + }); + expect(error).toBeUndefined(); + }); +}); + +describe('ReorderChapterValidator', () => { + it('should pass with valid sort_order', () => { + const { error } = ReorderChapterValidator.validate({ sort_order: 0 }); + expect(error).toBeUndefined(); + }); + + it('should fail without sort_order', () => { + const { error } = ReorderChapterValidator.validate({}); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Sort order is required/i); + }); + + it('should fail with negative sort_order', () => { + const { error } = ReorderChapterValidator.validate({ sort_order: -1 }); + expect(error).toBeDefined(); + }); +}); + +// ============================================================ +// Lesson Validators +// ============================================================ + +describe('CreateLessonValidator', () => { + it('should pass with VIDEO type', () => { + const { error } = CreateLessonValidator.validate({ + title: { th: 'บทเรียน 1', en: 'Lesson 1' }, + type: 'VIDEO', + }); + expect(error).toBeUndefined(); + }); + + it('should pass with QUIZ type', () => { + const { error } = CreateLessonValidator.validate({ + title: { th: 'แบบทดสอบ', en: 'Quiz' }, + type: 'QUIZ', + }); + expect(error).toBeUndefined(); + }); + + it('should fail with invalid type', () => { + const { error } = CreateLessonValidator.validate({ + title: { th: 'บทเรียน', en: 'Lesson' }, + type: 'DOCUMENT', + }); + expect(error).toBeDefined(); + }); + + it('should fail without title', () => { + const { error } = CreateLessonValidator.validate({ type: 'VIDEO' }); + expect(error).toBeDefined(); + }); + + it('should fail without type', () => { + const { error } = CreateLessonValidator.validate({ + title: { th: 'บทเรียน', en: 'Lesson' }, + }); + expect(error).toBeDefined(); + }); +}); + +describe('UpdateLessonValidator — prerequisite_lesson_ids', () => { + it('should pass with valid array of ids', () => { + const { error } = UpdateLessonValidator.validate({ + prerequisite_lesson_ids: [1, 2, 3], + }); + expect(error).toBeUndefined(); + }); + + it('should pass with null (clear prerequisites)', () => { + const { error } = UpdateLessonValidator.validate({ + prerequisite_lesson_ids: null, + }); + expect(error).toBeUndefined(); + }); + + it('should pass with empty array (clear prerequisites)', () => { + const { error } = UpdateLessonValidator.validate({ + prerequisite_lesson_ids: [], + }); + expect(error).toBeUndefined(); + }); + + it('should pass without the field (no change)', () => { + const { error } = UpdateLessonValidator.validate({}); + expect(error).toBeUndefined(); + }); + + it('should fail with non-integer ids', () => { + const { error } = UpdateLessonValidator.validate({ + prerequisite_lesson_ids: [1.5], + }); + expect(error).toBeDefined(); + }); + + it('should fail with negative ids', () => { + const { error } = UpdateLessonValidator.validate({ + prerequisite_lesson_ids: [-1], + }); + expect(error).toBeDefined(); + }); + + it('should fail with string ids', () => { + const { error } = UpdateLessonValidator.validate({ + prerequisite_lesson_ids: ['abc'], + }); + expect(error).toBeDefined(); + }); +}); + +describe('ReorderLessonsValidator', () => { + it('should pass with valid data', () => { + const { error } = ReorderLessonsValidator.validate({ + lesson_id: 1, + sort_order: 0, + }); + expect(error).toBeUndefined(); + }); + + it('should fail without lesson_id', () => { + const { error } = ReorderLessonsValidator.validate({ sort_order: 0 }); + expect(error).toBeDefined(); + }); + + it('should fail without sort_order', () => { + const { error } = ReorderLessonsValidator.validate({ lesson_id: 1 }); + expect(error).toBeDefined(); + }); +}); + +// ============================================================ +// Quiz Validators +// ============================================================ + +describe('AddQuestionValidator', () => { + it('should pass with MULTIPLE_CHOICE type + choices', () => { + const { error } = AddQuestionValidator.validate({ + question: { th: 'ข้อที่ 1 คืออะไร?', en: 'What is question 1?' }, + question_type: 'MULTIPLE_CHOICE', + choices: [ + { text: { th: 'ก', en: 'A' }, is_correct: true }, + { text: { th: 'ข', en: 'B' }, is_correct: false }, + ], + }); + expect(error).toBeUndefined(); + }); + + it('should pass with TRUE_FALSE type without choices', () => { + const { error } = AddQuestionValidator.validate({ + question: { th: 'ถูกหรือผิด?', en: 'True or False?' }, + question_type: 'TRUE_FALSE', + }); + expect(error).toBeUndefined(); + }); + + it('should fail with invalid question_type', () => { + const { error } = AddQuestionValidator.validate({ + question: { th: 'คำถาม', en: 'Question' }, + question_type: 'ESSAY', + }); + expect(error).toBeDefined(); + }); + + it('should fail without question', () => { + const { error } = AddQuestionValidator.validate({ + question_type: 'TRUE_FALSE', + }); + expect(error).toBeDefined(); + }); +}); + +describe('UpdateQuizValidator', () => { + it('should pass with empty object (all optional)', () => { + const { error } = UpdateQuizValidator.validate({}); + expect(error).toBeUndefined(); + }); + + it('should pass with valid passing_score', () => { + const { error } = UpdateQuizValidator.validate({ passing_score: 70 }); + expect(error).toBeUndefined(); + }); + + it('should fail with passing_score > 100', () => { + const { error } = UpdateQuizValidator.validate({ passing_score: 101 }); + expect(error).toBeDefined(); + }); + + it('should fail with passing_score < 0', () => { + const { error } = UpdateQuizValidator.validate({ passing_score: -1 }); + expect(error).toBeDefined(); + }); + + it('should pass with time_limit 0 (no limit)', () => { + const { error } = UpdateQuizValidator.validate({ time_limit: 0 }); + expect(error).toBeUndefined(); + }); +}); diff --git a/Backend/tests/unit/validators/CoursesInstructor.validator.test.ts b/Backend/tests/unit/validators/CoursesInstructor.validator.test.ts new file mode 100644 index 00000000..95c13c07 --- /dev/null +++ b/Backend/tests/unit/validators/CoursesInstructor.validator.test.ts @@ -0,0 +1,150 @@ +import { + CreateCourseValidator, + UpdateCourseValidator, + CloneCourseValidator, + addInstructorCourseValidator, +} from '@/validators/CoursesInstructor.validator'; + +// ============================================================ +// addInstructorCourseValidator +// ============================================================ + +describe('addInstructorCourseValidator', () => { + it('should pass with valid user_id and course_id', () => { + const { error } = addInstructorCourseValidator.validate({ + user_id: 1, + course_id: 2, + }); + expect(error).toBeUndefined(); + }); + + it('should fail without user_id', () => { + const { error } = addInstructorCourseValidator.validate({ course_id: 2 }); + expect(error).toBeDefined(); + }); + + it('should fail without course_id', () => { + const { error } = addInstructorCourseValidator.validate({ user_id: 1 }); + expect(error).toBeDefined(); + }); +}); + +// ============================================================ +// CreateCourseValidator +// ============================================================ + +describe('CreateCourseValidator', () => { + const validPayload = { + category_id: 1, + title: { th: 'คอร์สทดสอบ', en: 'Test Course' }, + slug: 'test-course', + description: { th: 'คำอธิบาย', en: 'Description' }, + price: 500, + is_free: false, + have_certificate: true, + }; + + it('should pass with all required fields', () => { + const { error } = CreateCourseValidator.validate(validPayload); + expect(error).toBeUndefined(); + }); + + it('should fail if title.th is missing', () => { + const { error } = CreateCourseValidator.validate({ + ...validPayload, + title: { en: 'Test Course' }, + }); + expect(error).toBeDefined(); + }); + + it('should fail if slug is missing', () => { + const { error } = CreateCourseValidator.validate({ + ...validPayload, + slug: undefined, + }); + expect(error).toBeDefined(); + }); + + it('should fail if price is missing', () => { + const { error } = CreateCourseValidator.validate({ + ...validPayload, + price: undefined, + }); + expect(error).toBeDefined(); + }); + + it('should fail if is_free is missing', () => { + const { error } = CreateCourseValidator.validate({ + ...validPayload, + is_free: undefined, + }); + expect(error).toBeDefined(); + }); + + it('should allow price = 0 (free course)', () => { + const { error } = CreateCourseValidator.validate({ + ...validPayload, + price: 0, + is_free: true, + }); + expect(error).toBeUndefined(); + }); +}); + +// ============================================================ +// UpdateCourseValidator +// ============================================================ + +describe('UpdateCourseValidator', () => { + it('should pass with empty object (all optional)', () => { + const { error } = UpdateCourseValidator.validate({}); + expect(error).toBeUndefined(); + }); + + it('should pass with partial update', () => { + const { error } = UpdateCourseValidator.validate({ price: 999 }); + expect(error).toBeUndefined(); + }); + + it('should pass with title partial update (th only)', () => { + const { error } = UpdateCourseValidator.validate({ + title: { th: 'ชื่อใหม่' }, + }); + expect(error).toBeUndefined(); + }); +}); + +// ============================================================ +// CloneCourseValidator +// ============================================================ + +describe('CloneCourseValidator', () => { + it('should pass with valid title', () => { + const { error } = CloneCourseValidator.validate({ + title: { th: 'คอร์ส Copy', en: 'Course Copy' }, + }); + expect(error).toBeUndefined(); + }); + + it('should fail without title.th', () => { + const { error } = CloneCourseValidator.validate({ + title: { en: 'Course Copy' }, + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Thai title is required/i); + }); + + it('should fail without title.en', () => { + const { error } = CloneCourseValidator.validate({ + title: { th: 'คอร์ส Copy' }, + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/English title is required/i); + }); + + it('should fail without title entirely', () => { + const { error } = CloneCourseValidator.validate({}); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Title is required/i); + }); +}); diff --git a/Backend/tests/unit/validators/CoursesStudent.validator.test.ts b/Backend/tests/unit/validators/CoursesStudent.validator.test.ts new file mode 100644 index 00000000..ae9569c4 --- /dev/null +++ b/Backend/tests/unit/validators/CoursesStudent.validator.test.ts @@ -0,0 +1,96 @@ +import { + SaveVideoProgressValidator, + SubmitQuizValidator, +} from '@/validators/CoursesStudent.validator'; + +describe('SaveVideoProgressValidator', () => { + it('should pass with required field only', () => { + const { error } = SaveVideoProgressValidator.validate({ + video_progress_seconds: 60, + }); + expect(error).toBeUndefined(); + }); + + it('should pass with all fields', () => { + const { error } = SaveVideoProgressValidator.validate({ + video_progress_seconds: 120, + video_duration_seconds: 600, + }); + expect(error).toBeUndefined(); + }); + + it('should pass with progress = 0 (start of video)', () => { + const { error } = SaveVideoProgressValidator.validate({ + video_progress_seconds: 0, + }); + expect(error).toBeUndefined(); + }); + + it('should fail without video_progress_seconds', () => { + const { error } = SaveVideoProgressValidator.validate({}); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Video progress seconds is required/i); + }); + + it('should fail with negative progress', () => { + const { error } = SaveVideoProgressValidator.validate({ + video_progress_seconds: -1, + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/at least 0/i); + }); + + it('should fail with negative video duration', () => { + const { error } = SaveVideoProgressValidator.validate({ + video_progress_seconds: 10, + video_duration_seconds: -1, + }); + expect(error).toBeDefined(); + }); +}); + +describe('SubmitQuizValidator', () => { + const validAnswer = { question_id: 1, choice_id: 2 }; + + it('should pass with valid answers', () => { + const { error } = SubmitQuizValidator.validate({ + answers: [validAnswer, { question_id: 2, choice_id: 5 }], + }); + expect(error).toBeUndefined(); + }); + + it('should fail without answers', () => { + const { error } = SubmitQuizValidator.validate({}); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Answers are required/i); + }); + + it('should fail with empty answers array', () => { + const { error } = SubmitQuizValidator.validate({ answers: [] }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/At least one answer/i); + }); + + it('should fail if question_id is missing in an answer', () => { + const { error } = SubmitQuizValidator.validate({ + answers: [{ choice_id: 2 }], + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Question ID is required/i); + }); + + it('should fail if choice_id is missing in an answer', () => { + const { error } = SubmitQuizValidator.validate({ + answers: [{ question_id: 1 }], + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Choice ID is required/i); + }); + + it('should fail if question_id is not a positive integer', () => { + const { error } = SubmitQuizValidator.validate({ + answers: [{ question_id: -1, choice_id: 1 }], + }); + expect(error).toBeDefined(); + }); +}); diff --git a/Backend/tests/unit/validators/Lessons.validator.test.ts b/Backend/tests/unit/validators/Lessons.validator.test.ts new file mode 100644 index 00000000..81ef5471 --- /dev/null +++ b/Backend/tests/unit/validators/Lessons.validator.test.ts @@ -0,0 +1,45 @@ +import { SetYouTubeVideoValidator } from '@/validators/Lessons.validator'; + +describe('SetYouTubeVideoValidator', () => { + it('should pass with valid youtube_video_id and video_title', () => { + const { error } = SetYouTubeVideoValidator.validate({ + youtube_video_id: 'dQw4w9WgXcQ', + video_title: 'Introduction to TypeScript', + }); + expect(error).toBeUndefined(); + }); + + it('should fail without youtube_video_id', () => { + const { error } = SetYouTubeVideoValidator.validate({ + video_title: 'Intro to TS', + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/YouTube video ID is required/i); + }); + + it('should fail with empty youtube_video_id string', () => { + const { error } = SetYouTubeVideoValidator.validate({ + youtube_video_id: '', + video_title: 'Intro to TS', + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/cannot be empty/i); + }); + + it('should fail without video_title', () => { + const { error } = SetYouTubeVideoValidator.validate({ + youtube_video_id: 'dQw4w9WgXcQ', + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Video title is required/i); + }); + + it('should fail with empty video_title string', () => { + const { error } = SetYouTubeVideoValidator.validate({ + youtube_video_id: 'dQw4w9WgXcQ', + video_title: '', + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/cannot be empty/i); + }); +}); diff --git a/Backend/tests/unit/validators/announcements.validator.test.ts b/Backend/tests/unit/validators/announcements.validator.test.ts new file mode 100644 index 00000000..9e186ca2 --- /dev/null +++ b/Backend/tests/unit/validators/announcements.validator.test.ts @@ -0,0 +1,115 @@ +import { + CreateAnnouncementValidator, + UpdateAnnouncementValidator, +} from '@/validators/announcements.validator'; + +describe('CreateAnnouncementValidator', () => { + const validPayload = { + title: { th: 'ประกาศใหม่', en: 'New Announcement' }, + content: { th: 'เนื้อหา', en: 'Content' }, + status: 'DRAFT', + is_pinned: false, + }; + + it('should pass with all required fields', () => { + const { error } = CreateAnnouncementValidator.validate(validPayload); + expect(error).toBeUndefined(); + }); + + it('should pass with optional published_at as ISO date', () => { + const { error } = CreateAnnouncementValidator.validate({ + ...validPayload, + published_at: '2026-03-01T00:00:00.000Z', + }); + expect(error).toBeUndefined(); + }); + + it('should fail with invalid published_at format', () => { + const { error } = CreateAnnouncementValidator.validate({ + ...validPayload, + published_at: '01-03-2026', + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/ISO date/i); + }); + + it('should fail with invalid status', () => { + const { error } = CreateAnnouncementValidator.validate({ + ...validPayload, + status: 'HIDDEN', + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/DRAFT, PUBLISHED, ARCHIVED/i); + }); + + it('should pass with PUBLISHED status', () => { + const { error } = CreateAnnouncementValidator.validate({ + ...validPayload, + status: 'PUBLISHED', + }); + expect(error).toBeUndefined(); + }); + + it('should pass with ARCHIVED status', () => { + const { error } = CreateAnnouncementValidator.validate({ + ...validPayload, + status: 'ARCHIVED', + }); + expect(error).toBeUndefined(); + }); + + it('should fail without title', () => { + const { error } = CreateAnnouncementValidator.validate({ + ...validPayload, + title: undefined, + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Title is required/i); + }); + + it('should fail without content', () => { + const { error } = CreateAnnouncementValidator.validate({ + ...validPayload, + content: undefined, + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Content is required/i); + }); + + it('should fail without is_pinned', () => { + const { error } = CreateAnnouncementValidator.validate({ + ...validPayload, + is_pinned: undefined, + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/is_pinned is required/i); + }); +}); + +describe('UpdateAnnouncementValidator', () => { + it('should pass with empty object (all optional)', () => { + const { error } = UpdateAnnouncementValidator.validate({}); + expect(error).toBeUndefined(); + }); + + it('should pass with partial update', () => { + const { error } = UpdateAnnouncementValidator.validate({ + status: 'PUBLISHED', + is_pinned: true, + }); + expect(error).toBeUndefined(); + }); + + it('should fail with invalid status', () => { + const { error } = UpdateAnnouncementValidator.validate({ status: 'DELETED' }); + expect(error).toBeDefined(); + }); + + it('should fail with invalid published_at format', () => { + const { error } = UpdateAnnouncementValidator.validate({ + published_at: 'not-a-date', + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/ISO date/i); + }); +}); diff --git a/Backend/tests/unit/validators/auth.validator.test.ts b/Backend/tests/unit/validators/auth.validator.test.ts new file mode 100644 index 00000000..2ef38652 --- /dev/null +++ b/Backend/tests/unit/validators/auth.validator.test.ts @@ -0,0 +1,246 @@ +import { + loginSchema, + registerSchema, + refreshTokenSchema, + resetPasswordSchema, + changePasswordSchema, + resetRequestSchema, +} from '@/validators/auth.validator'; + +// ============================================================ +// loginSchema +// ============================================================ + +describe('loginSchema', () => { + it('should pass with valid email and password', () => { + const { error } = loginSchema.validate({ + email: 'user@example.com', + password: 'password123', + }); + expect(error).toBeUndefined(); + }); + + it('should fail with invalid email format', () => { + const { error } = loginSchema.validate({ + email: 'not-an-email', + password: 'password123', + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/valid email/i); + }); + + it('should fail without email', () => { + const { error } = loginSchema.validate({ password: 'password123' }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Email is required/i); + }); + + it('should fail without password', () => { + const { error } = loginSchema.validate({ email: 'user@example.com' }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Password is required/i); + }); + + it('should fail with password shorter than 6 characters', () => { + const { error } = loginSchema.validate({ + email: 'user@example.com', + password: '12345', + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/at least 6 characters/i); + }); +}); + +// ============================================================ +// registerSchema +// ============================================================ + +describe('registerSchema', () => { + const validPayload = { + username: 'john_doe', + email: 'john@example.com', + password: 'securepass', + first_name: 'John', + last_name: 'Doe', + phone: '0812345678', + }; + + it('should pass with all required fields', () => { + const { error } = registerSchema.validate(validPayload); + expect(error).toBeUndefined(); + }); + + it('should pass with optional prefix', () => { + const { error } = registerSchema.validate({ + ...validPayload, + prefix: { th: 'นาย', en: 'Mr.' }, + }); + expect(error).toBeUndefined(); + }); + + it('should fail if username has invalid characters', () => { + const { error } = registerSchema.validate({ + ...validPayload, + username: 'john doe!', + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/letters, numbers, and underscores/i); + }); + + it('should fail if username is too short (< 3 chars)', () => { + const { error } = registerSchema.validate({ + ...validPayload, + username: 'ab', + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/at least 3 characters/i); + }); + + it('should fail if username is too long (> 50 chars)', () => { + const { error } = registerSchema.validate({ + ...validPayload, + username: 'a'.repeat(51), + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/not exceed 50 characters/i); + }); + + it('should fail with invalid email', () => { + const { error } = registerSchema.validate({ + ...validPayload, + email: 'bad-email', + }); + expect(error).toBeDefined(); + }); + + it('should fail if phone is too short (< 10 chars)', () => { + const { error } = registerSchema.validate({ + ...validPayload, + phone: '081234', + }); + expect(error).toBeDefined(); + }); + + it('should fail without first_name', () => { + const { error } = registerSchema.validate({ + ...validPayload, + first_name: undefined, + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/First name is required/i); + }); + + it('should fail without last_name', () => { + const { error } = registerSchema.validate({ + ...validPayload, + last_name: undefined, + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Last name is required/i); + }); +}); + +// ============================================================ +// refreshTokenSchema +// ============================================================ + +describe('refreshTokenSchema', () => { + it('should pass with a valid refreshToken', () => { + const { error } = refreshTokenSchema.validate({ + refreshToken: 'some-refresh-token-string', + }); + expect(error).toBeUndefined(); + }); + + it('should fail without refreshToken', () => { + const { error } = refreshTokenSchema.validate({}); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Refresh token is required/i); + }); +}); + +// ============================================================ +// resetPasswordSchema +// ============================================================ + +describe('resetPasswordSchema', () => { + it('should pass with valid token and password', () => { + const { error } = resetPasswordSchema.validate({ + token: 'reset-token-abc', + password: 'newpassword', + }); + expect(error).toBeUndefined(); + }); + + it('should fail without token', () => { + const { error } = resetPasswordSchema.validate({ password: 'newpassword' }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Reset token is required/i); + }); + + it('should fail with password too short', () => { + const { error } = resetPasswordSchema.validate({ + token: 'abc', + password: '12345', + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/at least 6 characters/i); + }); + + it('should fail with password too long (> 100 chars)', () => { + const { error } = resetPasswordSchema.validate({ + token: 'abc', + password: 'a'.repeat(101), + }); + expect(error).toBeDefined(); + }); +}); + +// ============================================================ +// changePasswordSchema (auth) +// ============================================================ + +describe('changePasswordSchema (auth.validator)', () => { + it('should pass with valid old and new passwords', () => { + const { error } = changePasswordSchema.validate({ + oldPassword: 'oldpass123', + newPassword: 'newpass456', + }); + expect(error).toBeUndefined(); + }); + + it('should fail without oldPassword', () => { + const { error } = changePasswordSchema.validate({ newPassword: 'newpass456' }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Password is required/i); + }); + + it('should fail without newPassword', () => { + const { error } = changePasswordSchema.validate({ oldPassword: 'oldpass123' }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Password is required/i); + }); +}); + +// ============================================================ +// resetRequestSchema +// ============================================================ + +describe('resetRequestSchema', () => { + it('should pass with valid email', () => { + const { error } = resetRequestSchema.validate({ email: 'user@example.com' }); + expect(error).toBeUndefined(); + }); + + it('should fail without email', () => { + const { error } = resetRequestSchema.validate({}); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Email is required/i); + }); + + it('should fail with invalid email', () => { + const { error } = resetRequestSchema.validate({ email: 'not-email' }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/valid email/i); + }); +}); diff --git a/Backend/tests/unit/validators/categories.validator.test.ts b/Backend/tests/unit/validators/categories.validator.test.ts new file mode 100644 index 00000000..64249c2a --- /dev/null +++ b/Backend/tests/unit/validators/categories.validator.test.ts @@ -0,0 +1,121 @@ +import { + CreateCategoryValidator, + UpdateCategoryValidator, +} from '@/validators/categories.validator'; + +describe('CreateCategoryValidator', () => { + const validPayload = { + name: { th: 'การพัฒนาเว็บ', en: 'Web Development' }, + slug: 'web-development', + description: { th: 'หมวดหมู่การพัฒนาเว็บ', en: 'Web development category' }, + }; + + it('should pass with all required fields', () => { + const { error } = CreateCategoryValidator.validate(validPayload); + expect(error).toBeUndefined(); + }); + + it('should pass with optional created_by', () => { + const { error } = CreateCategoryValidator.validate({ + ...validPayload, + created_by: 1, + }); + expect(error).toBeUndefined(); + }); + + it('should fail without name', () => { + const { error } = CreateCategoryValidator.validate({ + ...validPayload, + name: undefined, + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Name is required/i); + }); + + it('should fail without name.th', () => { + const { error } = CreateCategoryValidator.validate({ + ...validPayload, + name: { en: 'Web Development' }, + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Thai name is required/i); + }); + + it('should fail without slug', () => { + const { error } = CreateCategoryValidator.validate({ + ...validPayload, + slug: undefined, + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Slug is required/i); + }); + + it('should fail with invalid slug format (uppercase)', () => { + const { error } = CreateCategoryValidator.validate({ + ...validPayload, + slug: 'Web-Development', + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/lowercase with hyphens/i); + }); + + it('should fail with invalid slug format (spaces)', () => { + const { error } = CreateCategoryValidator.validate({ + ...validPayload, + slug: 'web development', + }); + expect(error).toBeDefined(); + }); + + it('should pass with valid slug formats', () => { + const slugs = ['web', 'web-dev', 'web-development-101']; + for (const slug of slugs) { + const { error } = CreateCategoryValidator.validate({ + ...validPayload, + slug, + }); + expect(error).toBeUndefined(); + } + }); + + it('should fail without description', () => { + const { error } = CreateCategoryValidator.validate({ + ...validPayload, + description: undefined, + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Description is required/i); + }); +}); + +describe('UpdateCategoryValidator', () => { + it('should pass with only required id', () => { + const { error } = UpdateCategoryValidator.validate({ id: 1 }); + expect(error).toBeUndefined(); + }); + + it('should pass with all optional fields', () => { + const { error } = UpdateCategoryValidator.validate({ + id: 1, + name: { th: 'ใหม่', en: 'New' }, + slug: 'new-category', + description: { th: 'คำอธิบาย', en: 'Description' }, + }); + expect(error).toBeUndefined(); + }); + + it('should fail without id', () => { + const { error } = UpdateCategoryValidator.validate({ name: { th: 'ใหม่', en: 'New' } }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Category ID is required/i); + }); + + it('should fail with invalid slug format', () => { + const { error } = UpdateCategoryValidator.validate({ + id: 1, + slug: 'Invalid Slug!', + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/lowercase with hyphens/i); + }); +}); diff --git a/Backend/tests/unit/validators/user.validator.test.ts b/Backend/tests/unit/validators/user.validator.test.ts new file mode 100644 index 00000000..349ceaa4 --- /dev/null +++ b/Backend/tests/unit/validators/user.validator.test.ts @@ -0,0 +1,100 @@ +import { + profileUpdateSchema, + changePasswordSchema, +} from '@/validators/user.validator'; + +describe('profileUpdateSchema', () => { + it('should pass with empty object (all optional)', () => { + const { error } = profileUpdateSchema.validate({}); + expect(error).toBeUndefined(); + }); + + it('should pass with all fields', () => { + const { error } = profileUpdateSchema.validate({ + prefix: { th: 'นาย', en: 'Mr.' }, + first_name: 'John', + last_name: 'Doe', + phone: '0812345678', + avatar_url: 'https://example.com/avatar.jpg', + birth_date: new Date('1990-01-01'), + }); + expect(error).toBeUndefined(); + }); + + it('should pass with partial update (first_name only)', () => { + const { error } = profileUpdateSchema.validate({ first_name: 'Jane' }); + expect(error).toBeUndefined(); + }); + + it('should fail if first_name is empty string', () => { + const { error } = profileUpdateSchema.validate({ first_name: '' }); + expect(error).toBeDefined(); + }); + + it('should fail if first_name exceeds 100 characters', () => { + const { error } = profileUpdateSchema.validate({ first_name: 'a'.repeat(101) }); + expect(error).toBeDefined(); + }); + + it('should fail if phone is too short (< 10 chars)', () => { + const { error } = profileUpdateSchema.validate({ phone: '081234' }); + expect(error).toBeDefined(); + }); + + it('should fail if phone exceeds 15 characters', () => { + const { error } = profileUpdateSchema.validate({ phone: '1'.repeat(16) }); + expect(error).toBeDefined(); + }); + + it('should pass with valid birth_date', () => { + const { error } = profileUpdateSchema.validate({ birth_date: new Date('2000-06-15') }); + expect(error).toBeUndefined(); + }); +}); + +describe('changePasswordSchema (user.validator)', () => { + it('should pass with valid old and new passwords', () => { + const { error } = changePasswordSchema.validate({ + oldPassword: 'oldpass123', + newPassword: 'newpass456', + }); + expect(error).toBeUndefined(); + }); + + it('should fail without oldPassword', () => { + const { error } = changePasswordSchema.validate({ newPassword: 'newpass456' }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Old password is required/i); + }); + + it('should fail without newPassword', () => { + const { error } = changePasswordSchema.validate({ oldPassword: 'oldpass123' }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/New password is required/i); + }); + + it('should fail if oldPassword is shorter than 6 chars', () => { + const { error } = changePasswordSchema.validate({ + oldPassword: '12345', + newPassword: 'newpass456', + }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/at least 6 characters/i); + }); + + it('should fail if newPassword is shorter than 6 chars', () => { + const { error } = changePasswordSchema.validate({ + oldPassword: 'oldpass123', + newPassword: '123', + }); + expect(error).toBeDefined(); + }); + + it('should fail if oldPassword exceeds 100 characters', () => { + const { error } = changePasswordSchema.validate({ + oldPassword: 'a'.repeat(101), + newPassword: 'newpass456', + }); + expect(error).toBeDefined(); + }); +}); diff --git a/Backend/tests/unit/validators/usermanagement.validator.test.ts b/Backend/tests/unit/validators/usermanagement.validator.test.ts new file mode 100644 index 00000000..6d57de81 --- /dev/null +++ b/Backend/tests/unit/validators/usermanagement.validator.test.ts @@ -0,0 +1,59 @@ +import { + getUserByIdValidator, + updateUserRoleValidator, +} from '@/validators/usermanagement.validator'; + +describe('getUserByIdValidator', () => { + it('should pass with valid id', () => { + const { error } = getUserByIdValidator.validate({ id: 1 }); + expect(error).toBeUndefined(); + }); + + it('should fail without id', () => { + const { error } = getUserByIdValidator.validate({}); + expect(error).toBeDefined(); + }); + + it('should fail with non-numeric id', () => { + const { error } = getUserByIdValidator.validate({ id: 'abc' }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/ID must be a number/i); + }); + + it('should pass with id = 0', () => { + // Joi number() allows 0 by default unless positive() is specified + const { error } = getUserByIdValidator.validate({ id: 0 }); + expect(error).toBeUndefined(); + }); +}); + +describe('updateUserRoleValidator', () => { + it('should pass with valid id and role_id', () => { + const { error } = updateUserRoleValidator.validate({ id: 1, role_id: 2 }); + expect(error).toBeUndefined(); + }); + + it('should fail without id', () => { + const { error } = updateUserRoleValidator.validate({ role_id: 2 }); + expect(error).toBeDefined(); + }); + + it('should fail without role_id', () => { + const { error } = updateUserRoleValidator.validate({ id: 1 }); + expect(error).toBeDefined(); + // Joi uses field name in message when custom messages don't match + expect(error?.details[0].message).toContain('role_id'); + }); + + it('should fail with non-numeric role_id', () => { + const { error } = updateUserRoleValidator.validate({ id: 1, role_id: 'admin' }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/Role ID must be a number/i); + }); + + it('should fail with non-numeric id', () => { + const { error } = updateUserRoleValidator.validate({ id: 'abc', role_id: 1 }); + expect(error).toBeDefined(); + expect(error?.details[0].message).toMatch(/ID must be a number/i); + }); +}); diff --git a/Backend/tsconfig.test.json b/Backend/tsconfig.test.json new file mode 100644 index 00000000..5eb9ef95 --- /dev/null +++ b/Backend/tsconfig.test.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "baseUrl": ".", + "paths": { + "@/*": [ + "src/*" + ] + }, + "types": [ + "node", + "jest" + ] + }, + "include": [ + "src/**/*", + "tests/**/*" + ] +} \ No newline at end of file