All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 26m51s
108 lines
3.9 KiB
TypeScript
108 lines
3.9 KiB
TypeScript
// 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<Request>;
|
|
let mockResponse: Partial<Response>;
|
|
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<z.ZodRawShape>);
|
|
|
|
// Act
|
|
await middleware(mockRequest as Request, mockResponse as Response, mockNext);
|
|
|
|
// Assert
|
|
expect(mockNext).toHaveBeenCalledWith(unexpectedError);
|
|
});
|
|
});
|