// src/middleware/validation.middleware.test.ts import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; import { Request, Response, NextFunction } from 'express'; import { z } from 'zod'; import { validateRequest } from './validation.middleware'; import { ValidationError } from '../services/db/errors.db'; import { createMockRequest } from '../tests/utils/createMockRequest'; describe('validateRequest Middleware', () => { let mockRequest: Partial; let mockResponse: Partial; let mockNext: NextFunction; beforeEach(() => { // Reset mocks before each test // Use `Object.create(null)` to create bare objects for params and query. // This more accurately mimics the behavior of Express's request objects // and prevents issues with inherited properties when the middleware // attempts to delete keys before merging validated data. mockRequest = createMockRequest({ params: Object.create(null), query: Object.create(null), body: {}, }); mockResponse = { status: vi.fn().mockReturnThis(), json: vi.fn(), }; mockNext = vi.fn(); }); it('should call next() and update request with parsed data on successful validation', async () => { // Arrange const schema = z.object({ params: z.object({ id: z.coerce.number() }), body: z.object({ name: z.string() }), }); mockRequest.params = { id: '123' }; mockRequest.body = { name: 'Test Name' }; const middleware = validateRequest(schema); // Act await middleware(mockRequest as Request, mockResponse as Response, mockNext); // Assert expect(mockNext).toHaveBeenCalledTimes(1); expect(mockNext).toHaveBeenCalledWith(); // Called with no arguments // Check that the request objects were updated with coerced/parsed values expect(mockRequest.params.id).toBe(123); expect(mockRequest.body.name).toBe('Test Name'); }); it('should call next() with a ValidationError on validation failure', async () => { // Arrange const schema = z.object({ body: z.object({ email: z.string().email('A valid email is required.'), age: z.number().positive(), }), }); // Invalid data: email is missing, age is a string mockRequest.body = { age: 'twenty' }; const middleware = validateRequest(schema); // Act await middleware(mockRequest as Request, mockResponse as Response, mockNext); // Assert expect(mockNext).toHaveBeenCalledTimes(1); const error = (mockNext as Mock).mock.calls[0][0]; expect(error).toBeInstanceOf(ValidationError); expect(error.validationErrors).toHaveLength(2); // Both email and age are invalid expect(error.validationErrors).toEqual( expect.arrayContaining([ // Zod reports "Required" or type mismatch for missing fields before running custom validators like .email() expect.objectContaining({ path: ['body', 'email'], message: expect.stringMatching(/required|invalid input/i), }), expect.objectContaining({ path: ['body', 'age'], message: expect.stringMatching(/expected number/i), }), ]), ); }); it('should call next() with a generic error if parsing fails unexpectedly', async () => { // Arrange const unexpectedError = new Error('Something went wrong during parsing'); const mockSchema = { parseAsync: vi.fn().mockRejectedValue(unexpectedError), }; // For this test, we only need the `parseAsync` method on our mock schema. // We cast it to `unknown` first, then to the expected ZodObject type to satisfy TypeScript. const middleware = validateRequest(mockSchema as unknown as z.ZodObject); // Act await middleware(mockRequest as Request, mockResponse as Response, mockNext); // Assert expect(mockNext).toHaveBeenCalledWith(unexpectedError); }); });