From 931ecf5186e2c31ade21d5be6150815319a99d45 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Sat, 21 Feb 2026 12:42:19 +0100 Subject: [PATCH 1/3] chore(scripts): align npm scripts and docs with Vue stack - Add `dev` alias for `start` (cross-stack consistency) - Add `test:unit` alias for `test` (one-shot, mirrors Vue naming) - Add `format` script (prettier already installed, script was missing) - Document `prod`, `format`, `test:unit`, `release` strategy in CLAUDE.md - Update README and copilot-instructions to reflect all scripts --- .github/copilot-instructions.md | 6 ++++-- CLAUDE.md | 30 ++++++++++++++++++------------ README.md | 6 ++++-- package.json | 3 +++ 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4cdc446f..88a68a95 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -7,14 +7,16 @@ ## Canonical commands -- Dev: `npm start` +- Dev: `npm start` (alias: `npm run dev`) - Debug: `npm run debug` -- Tests: `npm test` (watch: `npm run test:watch`) +- Tests: `npm test` / `npm run test:unit` (one-shot) or `npm run test:watch` (watch) - Coverage: `npm run test:coverage` - Lint: `npm run lint` - Lint fix: `npm run lint:fix` +- Format: `npm run format` - Seed: `npm run seed:dev` - Commit: `npm run commit` +- Release (CI): `npm run release:auto` ## Available prompts diff --git a/CLAUDE.md b/CLAUDE.md index ad99ab52..14c1e607 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,18 +12,24 @@ The `.claude/` folder contains embedded settings, skills, and agents that are av ## Canonical commands -| Command | Script | Description | -| ------------- | ------------------------ | ---------------------------------------------- | -| **Dev** | `npm start` | Start dev server at `http://localhost:3000/` | -| **Debug** | `npm run debug` | Start with nodemon and inspector | -| **Test** | `npm test` | Run all tests | -| **Watch** | `npm run test:watch` | Run tests in watch mode | -| **Coverage** | `npm run test:coverage` | Generate test coverage | -| **Lint** | `npm run lint` | Check code quality | -| **Lint fix** | `npm run lint:fix` | Auto-fix linting issues | -| **Seed** | `npm run seed:dev` | Seed development database | -| **Commit** | `npm run commit` | Commit with commitizen | -| **Docker** | `docker-compose up` | Start with docker-compose | +| Command | Script | Description | +| ---------------- | ------------------------ | ---------------------------------------------- | +| **Dev** | `npm start` | Start dev server at `http://localhost:3000/` | +| **Dev (alias)** | `npm run dev` | Alias for `npm start` | +| **Debug** | `npm run debug` | Start with nodemon and inspector | +| **Prod** | `npm run prod` | Start in production mode | +| **Test** | `npm test` | Run all tests (one-shot) | +| **Unit test** | `npm run test:unit` | Run unit tests once (alias of `npm test`) | +| **Watch** | `npm run test:watch` | Run tests in watch mode | +| **Coverage** | `npm run test:coverage` | Generate test coverage | +| **Lint** | `npm run lint` | Check code quality | +| **Lint fix** | `npm run lint:fix` | Auto-fix linting issues | +| **Format** | `npm run format` | Format with Prettier | +| **Seed** | `npm run seed:dev` | Seed development database | +| **Commit** | `npm run commit` | Commit with commitizen | +| **Release** | `npm run release` | Manual release (standard-version) | +| **Release (CI)** | `npm run release:auto` | Semantic release for CI | +| **Docker** | `docker-compose up` | Start with docker-compose | ## Preflight diff --git a/README.md b/README.md index 90f4c271..c56f1533 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ npm install ### Development ```bash -npm start +npm start # or: npm run dev ``` Runs the server at `http://localhost:3000/`. For auto-reload during development, use `npm run debug` (nodemon). @@ -80,7 +80,8 @@ npm run prod ### Testing ```bash -npm test # Run all tests +npm test # Run all tests (one-shot) +npm run test:unit # Run unit tests once (alias of npm test) npm run test:watch # Run tests in watch mode npm run test:coverage # Generate coverage report ``` @@ -92,6 +93,7 @@ Tests are organized per module in `modules/*/tests/` ```bash npm run lint # Check code quality (read-only) npm run lint:fix # Auto-fix code quality issues +npm run format # Format code with Prettier ``` ### Database Seeding diff --git a/package.json b/package.json index c6deb579..21e4e12f 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,11 @@ }, "scripts": { "start": "node server.js", + "dev": "npm run start", "debug": "nodemon --inspect server.js", "prod": "cross-env NODE_ENV=production node server.js --name=node", "test": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --runInBand", + "test:unit": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --runInBand", "test:watch": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --watch --runInBand", "test:coverage": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --coverage --runInBand", "lint": "eslint ./modules ./lib ./config ./scripts", @@ -38,6 +40,7 @@ "seed:mongodrop": "node scripts/seed.js drop", "generate:sllCerts": "scripts/generate-ssl-certs.sh", "lint:fix": "eslint --fix ./modules ./lib ./config ./scripts", + "format": "prettier --write .", "snyk-protect": "snyk protect", "commit": "npx cz", "release": "standard-version", From 752d407a031f8b438201fa1c8cf1fa3c20ef498a Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Sat, 21 Feb 2026 15:45:55 +0100 Subject: [PATCH 2/3] fix(auth): stop writing OAuth validation errors directly to res --- modules/auth/controllers/auth.controller.js | 14 ++++++++--- modules/auth/tests/auth.integration.tests.js | 26 ++++++++++---------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/modules/auth/controllers/auth.controller.js b/modules/auth/controllers/auth.controller.js index 7373fbee..34e70b12 100644 --- a/modules/auth/controllers/auth.controller.js +++ b/modules/auth/controllers/auth.controller.js @@ -113,7 +113,7 @@ const oauthCall = (req, res, next) => { * @param {Object} providerUserProfile * @param {Function} done - done */ -const checkOAuthUserProfile = async (profil, key, provider, res) => { +const checkOAuthUserProfile = async (profil, key, provider) => { // check if user exist try { const query = {}; @@ -137,10 +137,11 @@ const checkOAuthUserProfile = async (profil, key, provider, res) => { const result = model.getResultFromZod(user, UsersSchema.User); // check error const error = model.checkError(result); - if (error) return responses.error(res, 422, 'Schema validation error', error)(result.error); + if (error) throw new AppError('Schema validation error', { code: 'VALIDATION_ERROR', details: { message: error } }); // else return req.body with the data after Zod validation return await UserService.create(result.value); } catch (err) { + if (err instanceof AppError) throw err; throw new AppError('oAuth', { code: 'CONTROLLER_ERROR', details: err.details || err }); } }; @@ -163,7 +164,7 @@ const oauthCallback = async (req, res, next) => { providerData: {}, }; user.providerData[req.body.key] = req.body.value; - user = await checkOAuthUserProfile(user, req.body.key, strategy, res); + user = await checkOAuthUserProfile(user, req.body.key, strategy); const token = jwt.sign({ userId: user.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn, }); @@ -177,7 +178,12 @@ const oauthCallback = async (req, res, next) => { message: 'oAuth Ok', }); } catch (err) { - return responses.error(res, 422, 'Unprocessable Entity', errors.getMessage(err.details || err))(err); + return responses.error( + res, + 422, + err instanceof AppError && err.code === 'VALIDATION_ERROR' ? errors.getMessage(err) : 'Unprocessable Entity', + errors.getMessage(err.details || err), + )(err); } } // classic web oAuth diff --git a/modules/auth/tests/auth.integration.tests.js b/modules/auth/tests/auth.integration.tests.js index b19061d9..94a3f883 100644 --- a/modules/auth/tests/auth.integration.tests.js +++ b/modules/auth/tests/auth.integration.tests.js @@ -486,15 +486,14 @@ describe('Auth integration tests:', () => { avatar: '', providerData: { id: 'google-fake-id-999' }, }; - const mockRes = { status() { return this; }, json() {}, cookie() { return this; } }; - const result = await AuthController.checkOAuthUserProfile(profil, 'id', 'google', mockRes); + const result = await AuthController.checkOAuthUserProfile(profil, 'id', 'google'); expect(result).toBeDefined(); expect(result.id).toBeDefined(); expect(result.email).toBe(profil.email); oauthUsers.push(result); }); - test('should return 422 when checkOAuthUserProfile receives an invalid profile', async () => { + test('should throw validation AppError when checkOAuthUserProfile receives an invalid profile', async () => { const invalidProfil = { firstName: '', // invalid — fails min(1) lastName: 'Test', @@ -502,12 +501,15 @@ describe('Auth integration tests:', () => { avatar: '', providerData: { id: 'google-invalid-999' }, }; - const errors = []; - const mockRes = { status() { return this; }, json(body) { errors.push(body); }, cookie() { return this; } }; - const result = await AuthController.checkOAuthUserProfile(invalidProfil, 'id', 'google', mockRes); - expect(result).toBeDefined(); - expect(result.type).toBe('error'); - expect(errors[0]?.message).toBe('Schema validation error'); + await expect( + AuthController.checkOAuthUserProfile(invalidProfil, 'id', 'google'), + ).rejects.toMatchObject({ + message: 'Schema validation error', + code: 'VALIDATION_ERROR', + details: { + message: expect.any(String), + }, + }); }); test('should throw AppError when create fails inside checkOAuthUserProfile', async () => { @@ -518,10 +520,9 @@ describe('Auth integration tests:', () => { avatar: '', providerData: { id: 'google-err-000' }, }; - const mockRes = { status() { return this; }, json() {}, cookie() { return this; } }; const createSpy = jest.spyOn(UserService, 'create').mockRejectedValueOnce(new Error('DB error')); await expect( - AuthController.checkOAuthUserProfile(profil, 'id', 'google', mockRes), + AuthController.checkOAuthUserProfile(profil, 'id', 'google'), ).rejects.toThrow('oAuth'); createSpy.mockRestore(); }); @@ -590,9 +591,8 @@ describe('Auth integration tests:', () => { avatar: '', providerData: { id: 'google-find-id-777' }, }; - const mockRes = { status() { return this; }, json() {}, cookie() { return this; } }; // Second call — should find the existing user (search.length === 1 branch) - const found = await AuthController.checkOAuthUserProfile(profil, 'id', 'google', mockRes); + const found = await AuthController.checkOAuthUserProfile(profil, 'id', 'google'); expect(found).toBeDefined(); // cleanup From 08a16891a1d173634857ca708f9c603fbd480b68 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Sat, 21 Feb 2026 15:52:17 +0100 Subject: [PATCH 3/3] test(auth): cover invalid OAuth callback validation path --- modules/auth/controllers/auth.controller.js | 6 +++--- modules/auth/tests/auth.integration.tests.js | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/modules/auth/controllers/auth.controller.js b/modules/auth/controllers/auth.controller.js index 34e70b12..91ed5f3d 100644 --- a/modules/auth/controllers/auth.controller.js +++ b/modules/auth/controllers/auth.controller.js @@ -109,9 +109,9 @@ const oauthCall = (req, res, next) => { /** * @desc Endpoint to save oAuthProfile - * @param {Object} req - Express request object - * @param {Object} providerUserProfile - * @param {Function} done - done + * @param {Object} profil - OAuth user profile object + * @param {string} key - Provider key to lookup `providerData` + * @param {string} provider - OAuth provider name */ const checkOAuthUserProfile = async (profil, key, provider) => { // check if user exist diff --git a/modules/auth/tests/auth.integration.tests.js b/modules/auth/tests/auth.integration.tests.js index 94a3f883..354a8c98 100644 --- a/modules/auth/tests/auth.integration.tests.js +++ b/modules/auth/tests/auth.integration.tests.js @@ -550,6 +550,24 @@ describe('Auth integration tests:', () => { } }); + test('should return 422 when client-side OAuth callback receives an invalid profile', async () => { + const result = await agent + .post('/api/auth/google/callback') + .send({ + strategy: false, + key: 'id', + value: 'cb-app-auth-id-invalid-999', + firstName: '', + lastName: 'Callback', + email: 'oauthcb-invalid@test.com', + }) + .expect(422); + + expect(result.body.type).toBe('error'); + expect(result.body.message).toMatch(/^Schema validation error/); + expect(result.body.description).toEqual(expect.any(String)); + }); + test('should set tokenCookieOptions and redirect on classic web oAuth success', async () => { const mockUserId = 'mock-oauth-user-id-123'; const authenticateSpy = jest.spyOn(passport, 'authenticate').mockImplementationOnce(