Repository: statuscompliance/status-backend
Pull request #112: test(utils): added validation
Author: login: tirirote
Created: 2025-04-01T11:59:18Z | Merged: 2025-04-15T08:47:14Z

Closes #91 

Linked issue #91: Validation Middleware – Ensure proper input validation, test invalid cases (Easy)

Diff:
diff --git a/src/index.js b/src/index.js
index a8a26900..83d06522 100644
--- a/src/index.js
+++ b/src/index.js
@@ -15,7 +15,6 @@ import pointRoutes from './routes/point.routes.js';
 import scopeRoutes from './routes/scope.routes.js';
 import cors from 'cors';
 import { verifyAuthority } from './middleware/verifyAuth.js';
-import { validateParams } from './middleware/validation.js';
 import { endpointAvailable } from './middleware/endpoint.js';
 import cookieParser from 'cookie-parser';
 import { models } from './models/models.js';
@@ -74,7 +73,6 @@ const configureApp = () => {
   app.use(endpointAvailable);
   app.use(`${API_PREFIX}`, ghAccess);
   app.use(`${API_PREFIX}/users`, userRoutes);
-  app.use(validateParams);
   app.use(`${API_PREFIX}/scripts`, scriptRoutes);
   app.use(verifyAuthority);
   app.use(`${API_PREFIX}/scopes`, scopeRoutes);
diff --git a/src/middleware/validation.js b/src/middleware/validation.js
index 6216a3f9..48da8cbd 100644
--- a/src/middleware/validation.js
+++ b/src/middleware/validation.js
@@ -1,35 +1,25 @@
-import { validationResult } from 'express-validator';
 import { validate as isUUID } from 'uuid';
 
 export const checkIdParam = (req, res, next) => {
-  if (!req.params.id) {
-    return res.status(400).json({ error: 'Missing required parameter: id' });
-  }
+  if (!req.params.id) return res.status(400).json({ error: 'Missing required parameter: id' });
   next();
 };
 
-// Middleware to validate a parameter as UUIDv4
 export const validateUUID = (paramName) => {
   return (req, res, next) => {
-    const paramValue = req.params[paramName] || req.query[paramName] || req.body[paramName];
-    if (!paramValue && !isUUID(paramValue) && !isGrafanaUID(paramValue)) {
-      console.log('HERE');
-      return res.status(400).json({ error: `Invalid or missing UUID for parameter: ${paramName}` });
+
+    const paramValue = (req.params && req.params[paramName]) || (req.query && req.query[paramName]) || (req.body && req.body[paramName]);
+
+    if (!paramValue) {
+      return res.status(400).json({ error: `Missing parameter: ${paramName}` });
+    }
+    if (!isUUID(paramValue) && !isGrafanaUID(paramValue)) {
+      return res.status(400).json({ error: `Invalid UUID for parameter: ${paramName}` });
     }
     next();
   };
 };
 
-
-export function validateParams(req, res, next) {
-  const errors = validationResult(req);
-
-  if (!errors.isEmpty()) {
-    return res.status(400).json({ errors: errors.array() });
-  }
-  next();
-};
-
-function isGrafanaUID(uid) {
+export function isGrafanaUID(uid) {
   return /^[a-zA-Z0-9]{12,24}$/.test(uid);
 }
diff --git a/src/routes/thread.routes.js b/src/routes/thread.routes.js
index 7150e83d..9ae3bbc7 100644
--- a/src/routes/thread.routes.js
+++ b/src/routes/thread.routes.js
@@ -11,23 +11,21 @@ import {
   deleteUserThreads,
   changeThreadName,
 } from '../controllers/thread.controller.js';
-import { validateParams } from '../middleware/validation.js';
 
 const router = Router();
 
-router.get('s', validateParams, verifyAuthority, getThreads);
-router.get('', validateParams, verifyAuthority, getThreadsByUserId);
+router.get('s', verifyAuthority, getThreads);
+router.get('', verifyAuthority, getThreadsByUserId);
 router.get(
   '/:gptId',
-  validateParams,
   verifyAuthority,
   getThreadMessages
 );
-router.post('', validateParams, verifyAuthority, createThread);
-router.post('/:gptId', validateParams, verifyAuthority, addNewMessage);
-router.delete('/:gptId', validateParams, verifyAuthority, deleteThread);
-router.delete('', validateParams, verifyAuthority, deleteUserThreads);
-router.put('/:gptId', validateParams, verifyAuthority, changeThreadName);
+router.post('', verifyAuthority, createThread);
+router.post('/:gptId', verifyAuthority, addNewMessage);
+router.delete('/:gptId', verifyAuthority, deleteThread);
+router.delete('', verifyAuthority, deleteUserThreads);
+router.put('/:gptId', verifyAuthority, changeThreadName);
 
 export default router;
 
diff --git a/tests/unit/middleware/validation.test.js b/tests/unit/middleware/validation.test.js
new file mode 100644
index 00000000..aa9b3115
--- /dev/null
+++ b/tests/unit/middleware/validation.test.js
@@ -0,0 +1,143 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { checkIdParam, isGrafanaUID, validateUUID } from '../../../src/middleware/validation.js';
+import { validate as isUUID } from 'uuid';
+
+vi.mock('uuid', () => ({
+  validate: vi.fn(),
+}));
+
+describe('checkIdParam Middleware', () => {
+  let mockReq;
+  let mockRes;
+  let mockNext;
+
+  beforeEach(() => {
+    vi.clearAllMocks();
+    mockReq = { params: {}, query: {}, body: {} };
+    mockRes = {
+      status: vi.fn().mockReturnThis(),
+      json: vi.fn(),
+    };
+    mockNext = vi.fn();
+  });
+
+  it('should call next if id param is present', () => {
+    mockReq.params.id = '123';
+    checkIdParam(mockReq, mockRes, mockNext);
+    expect(mockNext).toHaveBeenCalledTimes(1);
+    expect(mockRes.status).not.toHaveBeenCalled();
+    expect(mockRes.json).not.toHaveBeenCalled();
+  });
+
+  it('should return 400 if id param is missing', () => {
+    checkIdParam(mockReq, mockRes, mockNext);
+    expect(mockRes.status).toHaveBeenCalledWith(400);
+    expect(mockRes.json).toHaveBeenCalledWith({ error: 'Missing required parameter: id' });
+    expect(mockNext).not.toHaveBeenCalled();
+  });
+});
+
+describe('validateUUID Middleware', () => {
+
+  let mockReq;
+  let mockRes;
+  let mockNext;
+  const mockIsGrafanaUID = vi.fn();
+  const userId = '5f1b7114-b133-487b-9442-2b48bf60807c';
+  const dashboardUid = 'validgrafana123';
+  const invalidUuid = 'invalid-uuid';
+  const missingParamError = 'Missing parameter: userId';
+  const invalidUuidError = 'Invalid UUID for parameter: userId';
+
+  beforeEach(() => {
+    vi.clearAllMocks();
+    mockReq = { params: {}, query: {}, body: {} };
+    mockRes = {
+      status: vi.fn().mockReturnThis(),
+      json: vi.fn(),
+    };
+    mockNext = vi.fn();
+
+    vi.doMock('../../../src/middleware/validation', () => ({
+      ...vi.importActual('../../../src/middleware/validation'),
+      isGrafanaUID: mockIsGrafanaUID,
+    }));
+  });
+
+  const runUUIDValidationTest = (reqField, reqValue, expectedError) => {
+    mockReq[reqField] = reqValue;
+    validateUUID('userId')(mockReq, mockRes, mockNext);
+    expect(mockRes.status).toHaveBeenCalledWith(400);
+    expect(mockRes.json).toHaveBeenCalledWith({ error: expectedError });
+    expect(mockNext).not.toHaveBeenCalled();
+  };
+
+  it('should call next if the parameter is a valid UUID in params', () => {
+    mockReq.params.userId = userId;
+    isUUID.mockReturnValue(true);
+    mockIsGrafanaUID.mockReturnValue(false);
+    validateUUID('userId')(mockReq, mockRes, mockNext);
+    expect(mockNext).toHaveBeenCalledTimes(1);
+    expect(mockRes.status).not.toHaveBeenCalled();
+    expect(mockRes.json).not.toHaveBeenCalled();
+  });
+
+  it('should call next if the parameter is a valid Grafana UID in body', () => {
+    mockReq.body.dashboardUid = dashboardUid;
+    isUUID.mockReturnValue(false);
+    mockIsGrafanaUID.mockReturnValue(true);
+    validateUUID('dashboardUid')(mockReq, mockRes, mockNext);
+    expect(mockNext).toHaveBeenCalledTimes(1);
+    expect(mockRes.status).not.toHaveBeenCalled();
+    expect(mockRes.json).not.toHaveBeenCalled();
+  });
+
+  it('should return 400 with error if the parameter is missing (params)', () => {
+    runUUIDValidationTest('params', mockReq, missingParamError)
+  });
+
+  it('should return 400 with error if the parameter is missing (query)', () => {
+    runUUIDValidationTest('query', mockReq, missingParamError)
+  });
+
+  it('should return 400 with error if the parameter is missing (body)', () => {
+    runUUIDValidationTest('body', mockReq, missingParamError)
+  });
+
+  it('should return 400 with error if the parameter is not a valid UUID or Grafana UID', () => {
+    mockReq.body.userId = invalidUuid;
+    isUUID.mockReturnValue(false);
+    mockIsGrafanaUID.mockReturnValue(false);
+    validateUUID('userId')(mockReq, mockRes, mockNext);
+    expect(mockNext).not.toHaveBeenCalled();
+    expect(mockRes.status).toHaveBeenCalledWith(400);
+    expect(mockRes.json).toHaveBeenCalledWith({ error: invalidUuidError });
+  });
+
+  it('should return 400 with error if the parameter is present but empty', () => {
+    mockReq.body.userId = '';
+    isUUID.mockReturnValue(false);
+    mockIsGrafanaUID.mockReturnValue(false);
+    validateUUID('userId')(mockReq, mockRes, mockNext);
+    expect(mockNext).not.toHaveBeenCalled();
+    expect(mockRes.status).toHaveBeenCalledWith(400);
+    expect(mockRes.json).toHaveBeenCalledWith({ error: missingParamError });
+  });
+});
+
+describe('isGrafanaUID Function', () => {
+
+  it('should return true for a valid Grafana UID', () => {
+    expect(isGrafanaUID('validgrafana123')).toBe(true);
+    expect(isGrafanaUID('VALIDGRAFANA123')).toBe(true);
+    expect(isGrafanaUID('v4l1dgr4f4n4123')).toBe(true);
+    expect(isGrafanaUID('1234567890abcdef12345678')).toBe(true);
+  });
+
+  it('should return false for an invalid Grafana UID', () => {
+    expect(isGrafanaUID('short')).toBe(false);
+    expect(isGrafanaUID('tooloooooooooooooooooooooooooooong')).toBe(false);
+    expect(isGrafanaUID('invalid-chars')).toBe(false);
+    expect(isGrafanaUID('')).toBe(false);
+  });
+});


Reviews:
user: login: alvarobernal2412
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-01T15:32:52Z
user: login: tirirote
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T07:53:59Z
user: login: tirirote
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T07:55:19Z
user: login: alvarobernal2412
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-15T08:42:24Z

Review comments:
path: tests/unit/middleware/validation.test.js
user: login: alvarobernal2412
body: ### **1. Code Quality Improvements**
✅ **Fix incorrect `validateParams` function signature in tests**  
- `validateParams` does **not** take `mockValidationResult` as a parameter. Instead, `validationResult(req)` should be called inside the function.  
- **Fix:** Remove the extra parameter in tests.

✅ **Fix redundant `vi.mock('express-validator')` calls inside test cases**  
- The `express-validator` mock is already defined globally at the top.  
- **Fix:** Remove `vi.mock('express-validator')` inside individual test cases.

✅ **Improve error messages for better debugging**  
- The error message in `validateUUID` should specify if the invalid parameter was missing or incorrect.  
- **Fix:**  
  ```js
  expect(mockRes.json).toHaveBeenCalledWith({ 
    error: 'Invalid or missing UUID/Grafana UID for parameter: userId' 
  });
  ```

✅ **Remove unnecessary `console.log('HERE');`**  
- This debug statement in `validateUUID` should be removed.

---

### **2. Test Coverage Improvements**
✅ **Test `validateUUID` when parameter is passed via `query`**  
- The test cases cover `params` and `body`, but `query` is missing.  
- **Add this test case:**
  ```js
  it('should call next if the parameter is a valid UUID in query', () => {
    mockReq.query = { userId: '5f1b7114-b133-487b-9442-2b48bf60807c' };
    isUUID.mockReturnValue(true);
    const middleware = validateUUID('userId');
    middleware(mockReq, mockRes, mockNext);
    expect(mockNext).toHaveBeenCalledTimes(1);
    expect(mockRes.status).not.toHaveBeenCalled();
    expect(mockRes.json).not.toHaveBeenCalled();
  });
  ```

✅ **Test `validateUUID` for edge cases**  
- Cases like `null`, `undefined`, empty string (`""`), and an invalid UUID format (e.g., `"not-a-uuid"`) should be tested.

✅ **Test `validateParams` with multiple validation errors**  
- The test only verifies **one** validation error.  
- **Add test with multiple errors:**  
  ```js
  it('should return 400 with multiple validation errors', () => {
    const errorsArray = [
      { msg: 'Error 1' },
      { msg: 'Error 2' }
    ];
    validationResult.mockReturnValue({
      isEmpty: () => false,
      array: () => errorsArray,
    });

    validateParams(mockReq, mockRes, mockNext);

    expect(mockRes.status).toHaveBeenCalledWith(400);
    expect(mockRes.json).toHaveBeenCalledWith({ errors: errorsArray });
  });
  ```

---

### **3. Reduce Code Duplication**
✅ **Extract common mock objects into `beforeEach`**  
- `mockReq`, `mockRes`, and `mockNext` are **redefined in every describe block**.  
- **Fix:** Move them to a shared `beforeEach()`:
  ```js
  let mockReq, mockRes, mockNext;

  beforeEach(() => {
    vi.clearAllMocks();
    mockReq = { params: {}, query: {}, body: {} };
    mockRes = {
      status: vi.fn().mockReturnThis(),
      json: vi.fn(),
    };
    mockNext = vi.fn();
  });
  ```

✅ **Refactor repeated validation checks in `validateUUID` tests**  
- The tests for missing `params`, `query`, and `body` all follow the same structure.  
- **Fix:** Use a helper function to run these checks:
  ```js
  const runUUIDValidationTest = (reqField, reqValue, expectedError) => {
    mockReq[reqField] = reqValue;
    const middleware = validateUUID('userId');
    middleware(mockReq, mockRes, mockNext);
    expect(mockRes.status).toHaveBeenCalledWith(400);
    expect(mockRes.json).toHaveBeenCalledWith({ error: expectedError });
  };

  it('should return 400 when UUID is missing in params', () => {
    runUUIDValidationTest('params', {}, 'Missing parameter: userId');
  });

  it('should return 400 when UUID is missing in query', () => {
    runUUIDValidationTest('query', {}, 'Missing parameter: userId');
  });

  it('should return 400 when UUID is missing in body', () => {
    runUUIDValidationTest('body', {}, 'Missing parameter: userId');
  });
  ```

✅ **Use `describe.each()` for repetitive validation cases**  
- Instead of writing similar tests separately, use `describe.each()` to loop through test cases.
created_at: 2025-04-01T15:28:47Z
author_association: MEMBER
path: src/middleware/validation.js
user: login: alvarobernal2412
body:   
**Reasons why you should keep the previous version:**  

1. **Cleaner and More Readable Code**  
   - Uses consistent indentation and spacing, improving readability.
   - Uses `{}` in `if` statements, reducing ambiguity.

2. **Proper Handling of Parameter Validation**  
   - **Current version** has an unnecessary long check:  
     ```js
     if ((!req.params || req.params[paramName] === undefined) &&
      (!req.query || req.query[paramName] === undefined) &&
      (!req.body || req.body[paramName] === undefined))
     ```
     This is redundant because accessing `undefined` directly (like `req.params[paramName]`) is already a falsy check.

   - **Previous version** simplifies it:  
     ```js
     const paramValue = req.params[paramName] || req.query[paramName] || req.body[paramName];
     if (!paramValue && !isUUID(paramValue) && !isGrafanaUID(paramValue)) {
     ```
     This ensures both **presence** and **validity** checks in one step.

3. **Better Use of `express-validator`**  
   - **Current version** has a function `validateParams(req, res, next, validation)`, where it expects a `validation` function to be passed. This is unnecessary when `validationResult(req)` is already provided by `express-validator`.
   - **Previous version** correctly calls `validationResult(req)` directly in `validateParams()`.

4. **Removal of Unused/Commented Code**  
   - **Current version** has an unnecessary commented-out import:  
     ```js
     //import { validationResult } from 'express-validator';
     ```
   - **Previous version** correctly imports and uses `validationResult`.

5. **Avoids Unnecessary Function Parameters**  
   - **Current version** has an extra argument in `validateParams(req, res, next, validation)`, but **Previous version** simplifies it to `validateParams(req, res, next)`, since `express-validator` already handles validation logic.
created_at: 2025-04-01T15:31:48Z
author_association: MEMBER
path: src/middleware/validation.js
user: login: tirirote
body: Fixed, but the express validator is mocked on the tests
created_at: 2025-04-03T07:53:59Z
author_association: CONTRIBUTOR
path: tests/unit/middleware/validation.test.js
user: login: tirirote
body: Applied the changes to the tests
created_at: 2025-04-03T07:55:19Z
author_association: CONTRIBUTOR

PR comments:
user: login: coveralls
created_at: 2025-04-09T10:02:49Z
body: 
[![Coverage Status](https://coveralls.io/builds/73230449/badge)](https://coveralls.io/builds/73230449)

coverage: 28.634% (+1.1%) from 27.524%
when pulling **a55d4ae11535141e5ef28c36f05f458750e358f9 on test-validation-middleware**
into **e3345297840693d2fbd228f51e41351cc790139d on develop**.

author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-15T08:42:59Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=statuscompliance_status-backend&pullRequest=112) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=statuscompliance_status-backend&pullRequest=112&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=statuscompliance_status-backend&pullRequest=112&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=statuscompliance_status-backend&pullRequest=112&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=statuscompliance_status-backend&pullRequest=112&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=statuscompliance_status-backend&pullRequest=112&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=statuscompliance_status-backend&pullRequest=112)


author_association: NONE