Files
flyer-crawler.projectium.com/src/middleware/validation.middleware.test.ts
Torben Sorensen e457bbf046
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 26m51s
more req work
2026-01-09 00:18:09 -08:00

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);
});
});