From 3fe9fc7142619a1472baf1b127e78293ad379b06 Mon Sep 17 00:00:00 2001 From: qpismont Date: Tue, 12 Aug 2025 18:29:05 +0000 Subject: [PATCH] =?UTF-8?q?Ajout=20de=20la=20gestion=20des=20erreurs=20HTT?= =?UTF-8?q?P,=20impl=C3=A9mentation=20de=20la=20validation=20des=20comptes?= =?UTF-8?q?,=20et=20mise=20=C3=A0=20jour=20des=20d=C3=A9pendances.=20Cr?= =?UTF-8?q?=C3=A9ation=20de=20tests=20pour=20les=20entit=C3=A9s=20et=20ser?= =?UTF-8?q?vices=20de=20compte,=20ainsi=20que=20l'ajout=20d'un=20syst?= =?UTF-8?q?=C3=A8me=20de=20limitation=20de=20taux.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 1052 ++++++++++++++++- package.json | 9 +- src/app.ts | 9 + .../account/controller/AccountController.ts | 71 +- src/domain/account/entity/AccountEntity.ts | 38 +- src/domain/account/errors/AccountErrors.ts | 44 + .../repository/AccountRepositoryInterface.ts | 2 +- .../account/repository/AccoutRepository.ts | 87 +- src/domain/account/service/AccountService.ts | 17 +- .../service/AccountServiceInterface.ts | 2 +- .../account/validation/AccountValidation.ts | 25 + src/errors.ts | 17 + src/middleware/rateLimiter.ts | 57 + tests/AccountEntity.test.ts | 56 + tests/AccountRepository.test.ts | 116 ++ tests/AccountService.test.ts | 95 ++ tests/AccountValidation.test.ts | 132 +++ vite.config.ts | 9 + 18 files changed, 1774 insertions(+), 64 deletions(-) create mode 100644 src/domain/account/errors/AccountErrors.ts create mode 100644 src/domain/account/validation/AccountValidation.ts create mode 100644 src/errors.ts create mode 100644 src/middleware/rateLimiter.ts create mode 100644 tests/AccountEntity.test.ts create mode 100644 tests/AccountRepository.test.ts create mode 100644 tests/AccountService.test.ts create mode 100644 tests/AccountValidation.test.ts create mode 100644 vite.config.ts diff --git a/package-lock.json b/package-lock.json index 5304736..ba61634 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,19 +10,23 @@ "license": "MIT", "dependencies": { "@hono/node-server": "^1.18.1", + "@hono/zod-validator": "^0.7.2", + "bcrypt": "^6.0.0", "hono": "^4.9.0", "pg": "^8.16.3", "zod": "^4.0.17" }, "devDependencies": { "@prettier/plugin-oxc": "^0.0.4", + "@types/bcrypt": "^6.0.0", "@types/node": "^24.2.1", "@types/pg": "^8.15.5", "oxlint": "^1.11.1", "prettier": "3.6.2", "rolldown": "^1.0.0-beta.32", "tsx": "^4.20.3", - "typescript": "^5.9.2" + "typescript": "^5.9.2", + "vitest": "^3.2.4" } }, "node_modules/@emnapi/core": { @@ -513,6 +517,23 @@ "hono": "^4" } }, + "node_modules/@hono/zod-validator": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@hono/zod-validator/-/zod-validator-0.7.2.tgz", + "integrity": "sha512-ub5eL/NeZ4eLZawu78JpW/J+dugDAYhwqUIdp9KYScI6PZECij4Hx4UsrthlEUutqDDhPwRI0MscUfNkvn/mqQ==", + "license": "MIT", + "peerDependencies": { + "hono": ">=3.9.0", + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, "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", @@ -1214,6 +1235,286 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@tybys/wasm-util": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", @@ -1225,6 +1526,40 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.2.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", @@ -1247,6 +1582,121 @@ "pg-types": "^2.2.0" } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/ansis": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.1.0.tgz", @@ -1257,6 +1707,102 @@ "node": ">=14" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", @@ -1299,6 +1845,41 @@ "@esbuild/win32-x64": "0.25.8" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1336,6 +1917,76 @@ "node": ">=16.9.0" } }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz", + "integrity": "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/oxc-parser": { "version": "0.74.0", "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.74.0.tgz", @@ -1416,6 +2067,23 @@ "@oxlint-tsgolint/win32-x64": "0.0.1" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/pg": { "version": "8.16.3", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", @@ -1505,6 +2173,55 @@ "split2": "^4.1.0" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "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/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -1612,6 +2329,63 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/rollup": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -1621,6 +2395,94 @@ "node": ">= 10.x" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -1670,6 +2532,194 @@ "dev": true, "license": "MIT" }, + "node_modules/vite": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz", + "integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.6", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.14" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 12485f2..0474a38 100644 --- a/package.json +++ b/package.json @@ -14,20 +14,25 @@ "dev": "tsx watch src/app.ts", "lint": "oxlint .", "type": "tsc --noEmit", - "build": "rolldown src/app.ts --file dist/app.js --platform node --format esm --minify" + "build": "rolldown src/app.ts --file dist/app.js --platform node --format esm --minify", + "tests": "vitest run" }, "devDependencies": { "@prettier/plugin-oxc": "^0.0.4", + "@types/bcrypt": "^6.0.0", "@types/node": "^24.2.1", "@types/pg": "^8.15.5", "oxlint": "^1.11.1", "prettier": "3.6.2", "rolldown": "^1.0.0-beta.32", "tsx": "^4.20.3", - "typescript": "^5.9.2" + "typescript": "^5.9.2", + "vitest": "^3.2.4" }, "dependencies": { "@hono/node-server": "^1.18.1", + "@hono/zod-validator": "^0.7.2", + "bcrypt": "^6.0.0", "hono": "^4.9.0", "pg": "^8.16.3", "zod": "^4.0.17" diff --git a/src/app.ts b/src/app.ts index b69df53..1db30bb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,6 +4,7 @@ import loadConfiguration from "./config"; import { DatabaseInterface } from "./database/DatabaseInterface"; import PgDatabase from "./database/PgDatabase"; import { setup as setupAccounts } from "./domain/account/setup"; +import { HTTPError } from "./errors"; const config = loadConfiguration(); const database: DatabaseInterface = new PgDatabase(config.database); @@ -11,6 +12,14 @@ const app = new Hono(); app.route("/accounts", setupAccounts(database)); +app.onError((err, c) => { + if (err instanceof HTTPError) { + return c.json({ error: err.message }, err.statusCode); + } + + return c.json({ error: "Internal server error" }, 500); +}); + serve({ port: config.port, fetch: app.fetch, diff --git a/src/domain/account/controller/AccountController.ts b/src/domain/account/controller/AccountController.ts index 3fbae85..8931fd6 100644 --- a/src/domain/account/controller/AccountController.ts +++ b/src/domain/account/controller/AccountController.ts @@ -1,15 +1,40 @@ import { Hono } from "hono"; +import { validator } from "hono/validator"; import { AccountServiceInterface } from "../service/AccountServiceInterface"; +import { loginSchema, registerSchema } from "../validation/AccountValidation"; +import { BadSchemaError } from "../../../errors"; +import { createRateLimit } from "../../../middleware/rateLimiter"; + +const loginRateLimit = createRateLimit({ + windowMs: 15 * 60 * 1000, + maxAttempts: 5, + keyGenerator: (c) => { + const ip = + c.req.header("x-forwarded-for") || + c.req.header("x-real-ip") || + "127.0.0.1"; + return `login:${ip}`; + }, +}); export default function toRoutes( accountService: AccountServiceInterface, ): Hono { const app = new Hono(); - app.post("/login", async (ctx) => { - try { - const { email, password } = await ctx.req.json(); + app.post( + "/login", + loginRateLimit, + validator("json", (value) => { + const parsed = loginSchema.safeParse(value); + if (!parsed.success) { + throw new BadSchemaError(parsed.error.message); + } + return parsed.data; + }), + async (ctx) => { + const { email, password } = ctx.req.valid("json"); const account = await accountService.login(email, password); return ctx.json({ @@ -17,38 +42,30 @@ export default function toRoutes( accountId: account.id, email: account.email, }); - } catch (error) { - return ctx.json( - { - success: false, - error: error instanceof Error ? error.message : "Erreur inconnue", - }, - 400, - ); - } - }); + }, + ); - app.post("/register", async (ctx) => { - try { - const { email, password } = await ctx.req.json(); + app.post( + "/register", + validator("json", (value) => { + const parsed = registerSchema.safeParse(value); + if (!parsed.success) { + throw new BadSchemaError(parsed.error.message); + } - const account = await accountService.createAccount(email, password); + return parsed.data; + }), + async (ctx) => { + const { email, password } = ctx.req.valid("json"); + const account = await accountService.register(email, password); return ctx.json({ success: true, accountId: account.id, email: account.email, }); - } catch (error) { - return ctx.json( - { - success: false, - error: error instanceof Error ? error.message : "Erreur inconnue", - }, - 400, - ); - } - }); + }, + ); return app; } diff --git a/src/domain/account/entity/AccountEntity.ts b/src/domain/account/entity/AccountEntity.ts index 07a45aa..843d6bf 100644 --- a/src/domain/account/entity/AccountEntity.ts +++ b/src/domain/account/entity/AccountEntity.ts @@ -1,3 +1,13 @@ +import bcrypt from "bcrypt"; +import { + InvalidEmailFormatError, + WeakPasswordError, +} from "../errors/AccountErrors"; +import { + EMAIL_REGEX, + MIN_PASSWORD_LENGTH, +} from "../validation/AccountValidation"; + export class AccountEntity { constructor( public readonly id: string, @@ -8,34 +18,36 @@ export class AccountEntity { public readonly updatedAt: Date, ) { this.validateEmail(email); - this.validatePassword(password); } - // Logique métier : validation de l'email private validateEmail(email: string): void { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email)) { - throw new Error("Format d'email invalide"); + if (!EMAIL_REGEX.test(email)) { + throw new InvalidEmailFormatError(email); } } - private validatePassword(password: string): void { - if (password.length < 8) { - throw new Error("Mot de passe trop court"); + private static validatePassword(password: string): void { + if (password.length < MIN_PASSWORD_LENGTH) { + throw new WeakPasswordError(); } } - // Logique métier : vérification du mot de passe public verifyPassword(plainPassword: string): boolean { - // Dans un vrai projet, on utiliserait bcrypt - return this.password === plainPassword; + return bcrypt.compareSync(plainPassword, this.password); + } + + public get hashedPassword(): string { + return this.password; } - // Factory method pour créer un nouveau compte static create(email: string, password: string): AccountEntity { + AccountEntity.validatePassword(password); + const now = new Date(); const id = crypto.randomUUID(); - return new AccountEntity(id, email, password, 1, now, now); + const hashedPassword = bcrypt.hashSync(password, 10); + + return new AccountEntity(id, email, hashedPassword, 1, now, now); } } diff --git a/src/domain/account/errors/AccountErrors.ts b/src/domain/account/errors/AccountErrors.ts new file mode 100644 index 0000000..9840903 --- /dev/null +++ b/src/domain/account/errors/AccountErrors.ts @@ -0,0 +1,44 @@ +import { HTTPError } from "../../../errors"; +import { MIN_PASSWORD_LENGTH } from "../validation/AccountValidation"; + +export class AccountNotFoundError extends HTTPError { + constructor(identifier: string) { + super(404, `Compte non trouvé : ${identifier}`); + } +} + +export class AccountAlreadyExistsError extends HTTPError { + constructor(email: string) { + super(409, `Un compte avec cet email existe déjà : ${email}`); + } +} + +export class BadPasswordError extends HTTPError { + constructor() { + super(401, "Mot de passe incorrect"); + } +} + +export class InvalidEmailFormatError extends HTTPError { + constructor(email: string) { + super(400, `Format d'email invalide : ${email}`); + } +} + +export class WeakPasswordError extends HTTPError { + constructor() { + super( + 400, + `Le mot de passe doit contenir au moins ${MIN_PASSWORD_LENGTH} caractères`, + ); + } +} + +export class TooManyAttemptsError extends HTTPError { + constructor(retryAfter: number) { + super( + 429, + `Trop de tentatives de connexion. Réessayez dans ${retryAfter} secondes`, + ); + } +} diff --git a/src/domain/account/repository/AccountRepositoryInterface.ts b/src/domain/account/repository/AccountRepositoryInterface.ts index ef56cf9..9e8a6b2 100644 --- a/src/domain/account/repository/AccountRepositoryInterface.ts +++ b/src/domain/account/repository/AccountRepositoryInterface.ts @@ -2,6 +2,6 @@ import { AccountEntity } from "../entity/AccountEntity"; export interface AccountRepositoryInterface { findByEmail(email: string): Promise; - save(account: AccountEntity): Promise; + save(account: AccountEntity): Promise; findById(id: string): Promise; } diff --git a/src/domain/account/repository/AccoutRepository.ts b/src/domain/account/repository/AccoutRepository.ts index 681f0d6..6a30297 100644 --- a/src/domain/account/repository/AccoutRepository.ts +++ b/src/domain/account/repository/AccoutRepository.ts @@ -6,21 +6,86 @@ export default class AccountRepository implements AccountRepositoryInterface { constructor(private readonly database: DatabaseInterface) {} async findByEmail(email: string): Promise { - // Implémentation simple pour l'exemple - // Dans un vrai projet, on ferait une requête à la base de données - console.log(`Recherche du compte avec email: ${email}`); - return null; + const sql = ` + SELECT id, email, password, role_id, created_at, updated_at + FROM accounts + WHERE email = $1 + `; + + const result = await this.database.fetchOne<{ + id: string; + email: string; + password: string; + role_id: number; + created_at: Date; + updated_at: Date; + }>(sql, [email]); + + if (!result) { + return null; + } + + return new AccountEntity( + result.id, + result.email, + result.password, + result.role_id, + result.created_at, + result.updated_at, + ); } - async save(account: AccountEntity): Promise { - // Implémentation simple pour l'exemple - console.log(`Sauvegarde du compte: ${account.id}`); - return 1; + async save(account: AccountEntity): Promise { + const sql = ` + INSERT INTO accounts (id, email, password, role_id, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (id) DO UPDATE SET + email = EXCLUDED.email, + password = EXCLUDED.password, + role_id = EXCLUDED.role_id, + updated_at = EXCLUDED.updated_at + RETURNING id + `; + + const result = await this.database.fetchOne<{ id: string }>(sql, [ + account.id, + account.email, + account.hashedPassword, + account.roleId, + account.createdAt, + account.updatedAt, + ]); + + return result!.id; } async findById(id: string): Promise { - // Implémentation simple pour l'exemple - console.log(`Recherche du compte avec ID: ${id}`); - return null; + const sql = ` + SELECT id, email, password, role_id, created_at, updated_at + FROM accounts + WHERE id = $1 + `; + + const result = await this.database.fetchOne<{ + id: string; + email: string; + password: string; + role_id: number; + created_at: Date; + updated_at: Date; + }>(sql, [id]); + + if (!result) { + return null; + } + + return new AccountEntity( + result.id, + result.email, + result.password, + result.role_id, + result.created_at, + result.updated_at, + ); } } diff --git a/src/domain/account/service/AccountService.ts b/src/domain/account/service/AccountService.ts index e3e5435..e70ad53 100644 --- a/src/domain/account/service/AccountService.ts +++ b/src/domain/account/service/AccountService.ts @@ -1,36 +1,37 @@ import { AccountEntity } from "../entity/AccountEntity"; import { AccountRepositoryInterface } from "../repository/AccountRepositoryInterface"; import { AccountServiceInterface } from "./AccountServiceInterface"; +import { + AccountNotFoundError, + AccountAlreadyExistsError, + BadPasswordError, +} from "../errors/AccountErrors"; export default class AccountService implements AccountServiceInterface { constructor(private readonly accountRepository: AccountRepositoryInterface) {} async login(email: string, password: string): Promise { - // Logique métier DDD : authentification const account = await this.accountRepository.findByEmail(email); if (!account) { - throw new Error("Compte non trouvé"); + throw new AccountNotFoundError(email); } if (!account.verifyPassword(password)) { - throw new Error("Mot de passe incorrect"); + throw new BadPasswordError(); } return account; } - async createAccount(email: string, password: string): Promise { - // Logique métier DDD : vérifier que l'email n'existe pas déjà + async register(email: string, password: string): Promise { const existingAccount = await this.accountRepository.findByEmail(email); if (existingAccount) { - throw new Error("Un compte avec cet email existe déjà"); + throw new AccountAlreadyExistsError(email); } - // Utilisation de la factory method de l'entité const newAccount = AccountEntity.create(email, password); - await this.accountRepository.save(newAccount); return newAccount; diff --git a/src/domain/account/service/AccountServiceInterface.ts b/src/domain/account/service/AccountServiceInterface.ts index 8478bdd..eb53fcb 100644 --- a/src/domain/account/service/AccountServiceInterface.ts +++ b/src/domain/account/service/AccountServiceInterface.ts @@ -2,5 +2,5 @@ import { AccountEntity } from "../entity/AccountEntity"; export interface AccountServiceInterface { login(email: string, password: string): Promise; - createAccount(email: string, password: string): Promise; + register(email: string, password: string): Promise; } diff --git a/src/domain/account/validation/AccountValidation.ts b/src/domain/account/validation/AccountValidation.ts new file mode 100644 index 0000000..22ee23c --- /dev/null +++ b/src/domain/account/validation/AccountValidation.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; + +export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +export const MIN_PASSWORD_LENGTH = 8; + +export const emailSchema = z + .string() + .regex(EMAIL_REGEX, "Format d'email invalide"); + +export const passwordSchema = z + .string() + .min( + MIN_PASSWORD_LENGTH, + `Le mot de passe doit contenir au moins ${MIN_PASSWORD_LENGTH} caractères`, + ); + +export const loginSchema = z.object({ + email: emailSchema, + password: passwordSchema, +}); + +export const registerSchema = z.object({ + email: emailSchema, + password: passwordSchema, +}); diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..191687b --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,17 @@ +import { ContentfulStatusCode } from "hono/utils/http-status"; + +export class HTTPError extends Error { + constructor( + public readonly statusCode: ContentfulStatusCode, + message: string, + parent?: Error, + ) { + super(message, { cause: parent }); + } +} + +export class BadSchemaError extends HTTPError { + constructor(message: string, parent?: Error) { + super(400, message, parent); + } +} diff --git a/src/middleware/rateLimiter.ts b/src/middleware/rateLimiter.ts new file mode 100644 index 0000000..294316d --- /dev/null +++ b/src/middleware/rateLimiter.ts @@ -0,0 +1,57 @@ +import { Context, Next } from 'hono'; +import { TooManyAttemptsError } from '../domain/account/errors/AccountErrors'; + +interface RateLimitConfig { + windowMs: number; + maxAttempts: number; + keyGenerator?: (c: Context) => string; +} + +interface AttemptRecord { + count: number; + resetTime: number; +} + +const attempts = new Map(); + +function cleanupExpiredEntries(): void { + const now = Date.now(); + for (const [key, record] of attempts.entries()) { + if (now > record.resetTime) { + attempts.delete(key); + } + } +} + +export function createRateLimit(config: RateLimitConfig) { + const { windowMs, maxAttempts, keyGenerator = (c) => c.req.header('x-forwarded-for') || '127.0.0.1' } = config; + + return async (c: Context, next: Next) => { + const key = keyGenerator(c); + const now = Date.now(); + + cleanupExpiredEntries(); + + const record = attempts.get(key); + + if (!record) { + attempts.set(key, { count: 1, resetTime: now + windowMs }); + await next(); + return; + } + + if (now > record.resetTime) { + attempts.set(key, { count: 1, resetTime: now + windowMs }); + await next(); + return; + } + + if (record.count >= maxAttempts) { + const retryAfter = Math.ceil((record.resetTime - now) / 1000); + throw new TooManyAttemptsError(retryAfter); + } + + record.count++; + await next(); + }; +} \ No newline at end of file diff --git a/tests/AccountEntity.test.ts b/tests/AccountEntity.test.ts new file mode 100644 index 0000000..7d35ff0 --- /dev/null +++ b/tests/AccountEntity.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from "vitest"; +import { AccountEntity } from "../src/domain/account/entity/AccountEntity"; +import { + InvalidEmailFormatError, + WeakPasswordError, +} from "../src/domain/account/errors/AccountErrors"; +import { MIN_PASSWORD_LENGTH } from "../src/domain/account/validation/AccountValidation"; + +describe("AccountEntity", () => { + describe("create", () => { + it("should create an account with valid email and password", () => { + const email = "test@example.com"; + const password = "a".repeat(MIN_PASSWORD_LENGTH); // Use minimum length + + const account = AccountEntity.create(email, password); + + expect(account.email).toBe(email); + expect(account.roleId).toBe(1); + expect(account.id).toBeDefined(); + expect(account.createdAt).toBeInstanceOf(Date); + expect(account.updatedAt).toBeInstanceOf(Date); + }); + + it("should throw InvalidEmailFormatError for invalid email", () => { + const invalidEmail = "invalid-email"; + const password = "password123"; + + expect(() => { + AccountEntity.create(invalidEmail, password); + }).toThrow(InvalidEmailFormatError); + }); + + it("should throw WeakPasswordError for short password", () => { + const email = "test@example.com"; + const shortPassword = "a".repeat(MIN_PASSWORD_LENGTH - 1); // One less than minimum + + expect(() => { + AccountEntity.create(email, shortPassword); + }).toThrow(WeakPasswordError); + }); + }); + + describe("verifyPassword", () => { + it("should return true for correct password", () => { + const account = AccountEntity.create("test@example.com", "password123"); + + expect(account.verifyPassword("password123")).toBe(true); + }); + + it("should return false for incorrect password", () => { + const account = AccountEntity.create("test@example.com", "password123"); + + expect(account.verifyPassword("wrongpassword")).toBe(false); + }); + }); +}); diff --git a/tests/AccountRepository.test.ts b/tests/AccountRepository.test.ts new file mode 100644 index 0000000..328ce4e --- /dev/null +++ b/tests/AccountRepository.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import AccountRepository from "../src/domain/account/repository/AccoutRepository"; +import { DatabaseInterface } from "../src/database/DatabaseInterface"; +import { AccountEntity } from "../src/domain/account/entity/AccountEntity"; + +describe("AccountRepository", () => { + let accountRepository: AccountRepository; + let mockDatabase: DatabaseInterface; + + beforeEach(() => { + mockDatabase = { + ping: vi.fn(), + fetchAll: vi.fn(), + fetchOne: vi.fn(), + execute: vi.fn(), + }; + accountRepository = new AccountRepository(mockDatabase); + }); + + describe("findByEmail", () => { + it("should return account when found", async () => { + const email = "test@example.com"; + const mockResult = { + id: "123", + email: email, + password: "hashedPassword", + role_id: 1, + created_at: new Date(), + updated_at: new Date(), + }; + + vi.mocked(mockDatabase.fetchOne).mockResolvedValue(mockResult); + + const result = await accountRepository.findByEmail(email); + + expect(result).toBeInstanceOf(AccountEntity); + expect(result?.email).toBe(email); + expect(mockDatabase.fetchOne).toHaveBeenCalledWith( + expect.stringContaining( + "SELECT id, email, password, role_id, created_at, updated_at", + ), + [email], + ); + }); + + it("should return null when account not found", async () => { + const email = "nonexistent@example.com"; + + vi.mocked(mockDatabase.fetchOne).mockResolvedValue(undefined); + + const result = await accountRepository.findByEmail(email); + + expect(result).toBeNull(); + }); + }); + + describe("findById", () => { + it("should return account when found", async () => { + const id = "123"; + const mockResult = { + id: id, + email: "test@example.com", + password: "hashedPassword", + role_id: 1, + created_at: new Date(), + updated_at: new Date(), + }; + + vi.mocked(mockDatabase.fetchOne).mockResolvedValue(mockResult); + + const result = await accountRepository.findById(id); + + expect(result).toBeInstanceOf(AccountEntity); + expect(result?.id).toBe(id); + expect(mockDatabase.fetchOne).toHaveBeenCalledWith( + expect.stringContaining( + "SELECT id, email, password, role_id, created_at, updated_at", + ), + [id], + ); + }); + + it("should return null when account not found", async () => { + const id = "nonexistent"; + + vi.mocked(mockDatabase.fetchOne).mockResolvedValue(undefined); + + const result = await accountRepository.findById(id); + + expect(result).toBeNull(); + }); + }); + + describe("save", () => { + it("should save account successfully", async () => { + const account = AccountEntity.create("test@example.com", "password123"); + + vi.mocked(mockDatabase.fetchOne).mockResolvedValue({ id: account.id }); + + const result = await accountRepository.save(account); + + expect(result).toBe(account.id); + expect(mockDatabase.fetchOne).toHaveBeenCalledWith( + expect.stringContaining("INSERT INTO accounts"), + expect.arrayContaining([ + account.id, + account.email, + expect.any(String), + account.roleId, + account.createdAt, + account.updatedAt, + ]), + ); + }); + }); +}); diff --git a/tests/AccountService.test.ts b/tests/AccountService.test.ts new file mode 100644 index 0000000..3f6a137 --- /dev/null +++ b/tests/AccountService.test.ts @@ -0,0 +1,95 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import AccountService from "../src/domain/account/service/AccountService"; +import { AccountRepositoryInterface } from "../src/domain/account/repository/AccountRepositoryInterface"; +import { AccountEntity } from "../src/domain/account/entity/AccountEntity"; +import { + AccountNotFoundError, + AccountAlreadyExistsError, + BadPasswordError, +} from "../src/domain/account/errors/AccountErrors"; + +describe("AccountService", () => { + let accountService: AccountService; + let mockAccountRepository: AccountRepositoryInterface; + + beforeEach(() => { + mockAccountRepository = { + findByEmail: vi.fn(), + save: vi.fn(), + findById: vi.fn(), + }; + accountService = new AccountService(mockAccountRepository); + }); + + describe("createAccount", () => { + it("should create a new account successfully", async () => { + const email = "test@example.com"; + const password = "password123"; + + vi.mocked(mockAccountRepository.findByEmail).mockResolvedValue(null); + vi.mocked(mockAccountRepository.save).mockResolvedValue("123"); + + const result = await accountService.register(email, password); + + expect(result).toBeInstanceOf(AccountEntity); + expect(result.email).toBe(email); + expect(mockAccountRepository.findByEmail).toHaveBeenCalledWith(email); + expect(mockAccountRepository.save).toHaveBeenCalledWith( + expect.any(AccountEntity), + ); + }); + + it("should throw error if account already exists", async () => { + const email = "test@example.com"; + const password = "password123"; + const existingAccount = AccountEntity.create(email, password); + + vi.mocked(mockAccountRepository.findByEmail).mockResolvedValue( + existingAccount, + ); + + await expect(accountService.register(email, password)).rejects.toThrow( + AccountAlreadyExistsError, + ); + }); + }); + + describe("login", () => { + it("should login successfully with correct credentials", async () => { + const email = "test@example.com"; + const password = "password123"; + const account = AccountEntity.create(email, password); + + vi.mocked(mockAccountRepository.findByEmail).mockResolvedValue(account); + + const result = await accountService.login(email, password); + + expect(result).toBe(account); + expect(mockAccountRepository.findByEmail).toHaveBeenCalledWith(email); + }); + + it("should throw error if account not found", async () => { + const email = "test@example.com"; + const password = "password123"; + + vi.mocked(mockAccountRepository.findByEmail).mockResolvedValue(null); + + await expect(accountService.login(email, password)).rejects.toThrow( + AccountNotFoundError, + ); + }); + + it("should throw error if password is incorrect", async () => { + const email = "test@example.com"; + const password = "password123"; + const wrongPassword = "wrongpassword"; + const account = AccountEntity.create(email, password); + + vi.mocked(mockAccountRepository.findByEmail).mockResolvedValue(account); + + await expect(accountService.login(email, wrongPassword)).rejects.toThrow( + BadPasswordError, + ); + }); + }); +}); diff --git a/tests/AccountValidation.test.ts b/tests/AccountValidation.test.ts new file mode 100644 index 0000000..d441622 --- /dev/null +++ b/tests/AccountValidation.test.ts @@ -0,0 +1,132 @@ +import { describe, it, expect } from "vitest"; +import { + emailSchema, + passwordSchema, + loginSchema, + registerSchema, + EMAIL_REGEX, + MIN_PASSWORD_LENGTH, +} from "../src/domain/account/validation/AccountValidation"; +import { AccountEntity } from "../src/domain/account/entity/AccountEntity"; +import { + InvalidEmailFormatError, + WeakPasswordError, +} from "../src/domain/account/errors/AccountErrors"; + +describe("AccountValidation", () => { + describe("Email validation consistency", () => { + const validEmails = [ + "test@example.com", + "user.name@domain.co.uk", + "firstname+lastname@example.org", + ]; + + const invalidEmails = [ + "invalid-email", + "@example.com", + "test@", + "test", + "test@domain", + "", + ]; + + it("should validate same emails in Zod and Entity", () => { + validEmails.forEach((email) => { + expect(emailSchema.safeParse(email).success).toBe(true); + expect(() => AccountEntity.create(email, "password123")).not.toThrow( + InvalidEmailFormatError, + ); + }); + + invalidEmails.forEach((email) => { + expect(emailSchema.safeParse(email).success).toBe(false); + expect(() => AccountEntity.create(email, "password123")).toThrow( + InvalidEmailFormatError, + ); + }); + }); + + it("should use same regex pattern", () => { + validEmails.forEach((email) => { + expect(EMAIL_REGEX.test(email)).toBe(true); + }); + + invalidEmails.forEach((email) => { + expect(EMAIL_REGEX.test(email)).toBe(false); + }); + }); + }); + + describe("Password validation consistency", () => { + const validPasswords = ["password123", "abcdefgh", "12345678", "P@ssw0rd!"]; + + const invalidPasswords = ["1234567", "short", "", "abc"]; + + it("should validate same passwords in Zod and Entity", () => { + validPasswords.forEach((password) => { + expect(passwordSchema.safeParse(password).success).toBe(true); + expect(() => + AccountEntity.create("test@example.com", password), + ).not.toThrow(WeakPasswordError); + }); + + invalidPasswords.forEach((password) => { + expect(passwordSchema.safeParse(password).success).toBe(false); + expect(() => + AccountEntity.create("test@example.com", password), + ).toThrow(WeakPasswordError); + }); + }); + + it("should use same minimum length", () => { + validPasswords.forEach((password) => { + expect(password.length >= MIN_PASSWORD_LENGTH).toBe(true); + }); + + invalidPasswords.forEach((password) => { + expect(password.length >= MIN_PASSWORD_LENGTH).toBe(false); + }); + }); + }); + + describe("Complete schemas", () => { + it("should validate login schema correctly", () => { + const validLogin = { email: "test@example.com", password: "password123" }; + const invalidLogin = { email: "invalid", password: "123" }; + + expect(loginSchema.safeParse(validLogin).success).toBe(true); + expect(loginSchema.safeParse(invalidLogin).success).toBe(false); + }); + + it("should validate register schema correctly", () => { + const validRegister = { + email: "test@example.com", + password: "password123", + }; + const invalidRegister = { email: "invalid", password: "123" }; + + expect(registerSchema.safeParse(validRegister).success).toBe(true); + expect(registerSchema.safeParse(invalidRegister).success).toBe(false); + }); + }); + + describe("Error messages consistency", () => { + it("should return consistent password error message", () => { + const result = passwordSchema.safeParse("123"); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0]?.message).toContain( + `${MIN_PASSWORD_LENGTH} caractères`, + ); + } + }); + + it("should return consistent email error message", () => { + const result = emailSchema.safeParse("invalid-email"); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0]?.message).toBe("Format d'email invalide"); + } + }); + }); +}); diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..4b1f9af --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["tests/**/*.test.ts"], + }, +});