feat: Add unit tests for backend validators and configure Jest.
This commit is contained in:
parent
ebcae0b3e7
commit
9bb941b45e
16 changed files with 2071 additions and 381 deletions
22
Backend/jest.config.js
Normal file
22
Backend/jest.config.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/** @type {import('jest').Config} */
|
||||
const config = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/tests'],
|
||||
testMatch: ['**/*.test.ts'],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/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;
|
||||
962
Backend/package-lock.json
generated
962
Backend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
171
Backend/pnpm-lock.yaml
generated
171
Backend/pnpm-lock.yaml
generated
|
|
@ -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:
|
||||
|
|
|
|||
14
Backend/tests/tsconfig.json
Normal file
14
Backend/tests/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "../tsconfig.test.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "..",
|
||||
"types": [
|
||||
"node",
|
||||
"jest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"../src/**/*",
|
||||
"./**/*"
|
||||
]
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
263
Backend/tests/unit/validators/ChaptersLesson.validator.test.ts
Normal file
263
Backend/tests/unit/validators/ChaptersLesson.validator.test.ts
Normal file
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
45
Backend/tests/unit/validators/Lessons.validator.test.ts
Normal file
45
Backend/tests/unit/validators/Lessons.validator.test.ts
Normal file
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
115
Backend/tests/unit/validators/announcements.validator.test.ts
Normal file
115
Backend/tests/unit/validators/announcements.validator.test.ts
Normal file
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
246
Backend/tests/unit/validators/auth.validator.test.ts
Normal file
246
Backend/tests/unit/validators/auth.validator.test.ts
Normal file
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
121
Backend/tests/unit/validators/categories.validator.test.ts
Normal file
121
Backend/tests/unit/validators/categories.validator.test.ts
Normal file
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
100
Backend/tests/unit/validators/user.validator.test.ts
Normal file
100
Backend/tests/unit/validators/user.validator.test.ts
Normal file
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
20
Backend/tsconfig.test.json
Normal file
20
Backend/tsconfig.test.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"types": [
|
||||
"node",
|
||||
"jest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"tests/**/*"
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue