181 lines
5.4 KiB
TypeScript
181 lines
5.4 KiB
TypeScript
import { Request, Response, NextFunction } from 'express';
|
|
import { z } from 'zod';
|
|
import { validate, validateMultiple } from '../../src/api/middleware/validation.middleware';
|
|
|
|
// Mock Express request, response, and next
|
|
const mockRequest = (data: any = {}): Partial<Request> => ({
|
|
body: data.body || {},
|
|
query: data.query || {},
|
|
params: data.params || {}
|
|
});
|
|
|
|
const mockResponse = (): Partial<Response> => {
|
|
const res: Partial<Response> = {};
|
|
res.status = jest.fn().mockReturnValue(res);
|
|
res.json = jest.fn().mockReturnValue(res);
|
|
return res;
|
|
};
|
|
|
|
const mockNext: NextFunction = jest.fn();
|
|
|
|
describe('Validation Middleware', () => {
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('validate', () => {
|
|
const testSchema = z.object({
|
|
name: z.string().min(1),
|
|
age: z.number().min(0).max(150)
|
|
});
|
|
|
|
it('should pass validation with valid data', async () => {
|
|
const req = mockRequest({ body: { name: 'John', age: 30 } }) as Request;
|
|
const res = mockResponse() as Response;
|
|
const middleware = validate(testSchema, 'body');
|
|
|
|
await middleware(req, res, mockNext);
|
|
|
|
expect(mockNext).toHaveBeenCalled();
|
|
expect(res.status).not.toHaveBeenCalled();
|
|
expect(req.body).toEqual({ name: 'John', age: 30 });
|
|
});
|
|
|
|
it('should fail validation with invalid data', async () => {
|
|
const req = mockRequest({ body: { name: '', age: -5 } }) as Request;
|
|
const res = mockResponse() as Response;
|
|
const middleware = validate(testSchema, 'body');
|
|
|
|
await middleware(req, res, mockNext);
|
|
|
|
expect(mockNext).not.toHaveBeenCalled();
|
|
expect(res.status).toHaveBeenCalledWith(400);
|
|
expect(res.json).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
success: false,
|
|
error: 'Validation failed',
|
|
details: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
field: expect.any(String),
|
|
message: expect.any(String)
|
|
})
|
|
])
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should validate query parameters', async () => {
|
|
const querySchema = z.object({
|
|
page: z.coerce.number().min(1),
|
|
limit: z.coerce.number().min(1).max(100)
|
|
});
|
|
|
|
const req = mockRequest({ query: { page: '2', limit: '50' } }) as Request;
|
|
const res = mockResponse() as Response;
|
|
const middleware = validate(querySchema, 'query');
|
|
|
|
await middleware(req, res, mockNext);
|
|
|
|
expect(mockNext).toHaveBeenCalled();
|
|
expect(req.query).toEqual({ page: 2, limit: 50 });
|
|
});
|
|
|
|
it('should validate params', async () => {
|
|
const paramsSchema = z.object({
|
|
id: z.string().uuid()
|
|
});
|
|
|
|
const req = mockRequest({
|
|
params: { id: '123e4567-e89b-12d3-a456-426614174000' }
|
|
}) as Request;
|
|
const res = mockResponse() as Response;
|
|
const middleware = validate(paramsSchema, 'params');
|
|
|
|
await middleware(req, res, mockNext);
|
|
|
|
expect(mockNext).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('validateMultiple', () => {
|
|
const schemas = {
|
|
body: z.object({
|
|
title: z.string().min(1)
|
|
}),
|
|
query: z.object({
|
|
page: z.coerce.number().min(1)
|
|
}),
|
|
params: z.object({
|
|
id: z.string().uuid()
|
|
})
|
|
};
|
|
|
|
it('should validate multiple sources successfully', async () => {
|
|
const req = mockRequest({
|
|
body: { title: 'Test' },
|
|
query: { page: '1' },
|
|
params: { id: '123e4567-e89b-12d3-a456-426614174000' }
|
|
}) as Request;
|
|
const res = mockResponse() as Response;
|
|
const middleware = validateMultiple(schemas);
|
|
|
|
await middleware(req, res, mockNext);
|
|
|
|
expect(mockNext).toHaveBeenCalled();
|
|
expect(res.status).not.toHaveBeenCalled();
|
|
expect(req.body).toEqual({ title: 'Test' });
|
|
expect(req.query).toEqual({ page: 1 });
|
|
});
|
|
|
|
it('should collect errors from multiple sources', async () => {
|
|
const req = mockRequest({
|
|
body: { title: '' }, // Invalid
|
|
query: { page: '0' }, // Invalid
|
|
params: { id: 'not-a-uuid' } // Invalid
|
|
}) as Request;
|
|
const res = mockResponse() as Response;
|
|
const middleware = validateMultiple(schemas);
|
|
|
|
await middleware(req, res, mockNext);
|
|
|
|
expect(mockNext).not.toHaveBeenCalled();
|
|
expect(res.status).toHaveBeenCalledWith(400);
|
|
expect(res.json).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
success: false,
|
|
error: 'Validation failed',
|
|
details: expect.arrayContaining([
|
|
expect.objectContaining({ source: 'body' }),
|
|
expect.objectContaining({ source: 'query' }),
|
|
expect.objectContaining({ source: 'params' })
|
|
])
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should handle partial schemas', async () => {
|
|
const partialSchemas = {
|
|
body: z.object({
|
|
name: z.string()
|
|
})
|
|
// No query or params schemas
|
|
};
|
|
|
|
const req = mockRequest({
|
|
body: { name: 'Test' },
|
|
query: { anything: 'goes' },
|
|
params: { whatever: 'works' }
|
|
}) as Request;
|
|
const res = mockResponse() as Response;
|
|
const middleware = validateMultiple(partialSchemas);
|
|
|
|
await middleware(req, res, mockNext);
|
|
|
|
expect(mockNext).toHaveBeenCalled();
|
|
expect(req.body).toEqual({ name: 'Test' });
|
|
// Query and params should remain unchanged
|
|
expect(req.query).toEqual({ anything: 'goes' });
|
|
expect(req.params).toEqual({ whatever: 'works' });
|
|
});
|
|
});
|
|
}); |