Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
30 changes: 18 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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
```
Expand All @@ -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
Expand Down
20 changes: 13 additions & 7 deletions modules/auth/controllers/auth.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,11 @@ 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, res) => {
const checkOAuthUserProfile = async (profil, key, provider) => {
// check if user exist
try {
const query = {};
Expand All @@ -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 });
}
};
Expand All @@ -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,
});
Expand All @@ -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
Expand Down
44 changes: 31 additions & 13 deletions modules/auth/tests/auth.integration.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -486,28 +486,30 @@ 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',
email: 'invalid-oauth@test.com',
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 () => {
Expand All @@ -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();
});
Expand Down Expand Up @@ -549,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(
Expand Down Expand Up @@ -590,9 +609,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
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down