Mirror of https://github.com/roostorg/coop github.com/roostorg/coop
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 107 lines 3.6 kB view raw
1import type { Request, Response } from 'express'; 2 3import { createBodySchemaValidator } from './bodySchemaValidation.js'; 4import { CoopError } from './errors.js'; 5 6const schema: Record<string, unknown> = { 7 $schema: 'http://json-schema.org/draft-04/schema#', 8 type: 'object', 9 properties: { 10 name: { type: 'string' }, 11 count: { type: 'integer' }, 12 }, 13 required: ['name'], 14 additionalProperties: false, 15}; 16 17function invoke( 18 middleware: ReturnType<typeof createBodySchemaValidator>, 19 body: unknown, 20) { 21 const req: Partial<Request> = { body }; 22 const res: Partial<Response> = {}; 23 const next = jest.fn(); 24 middleware(req as Request, res as Response, next); 25 return { next }; 26} 27 28function firstNextArg(next: ReturnType<typeof jest.fn>): unknown { 29 return next.mock.calls[0]?.[0]; 30} 31 32describe('createBodySchemaValidator', () => { 33 test('passes valid bodies through to next()', () => { 34 const middleware = createBodySchemaValidator(schema); 35 const { next } = invoke(middleware, { name: 'ok', count: 3 }); 36 37 expect(next).toHaveBeenCalledTimes(1); 38 expect(next).toHaveBeenCalledWith(); 39 }); 40 41 test('allows optional fields to be omitted', () => { 42 const middleware = createBodySchemaValidator(schema); 43 const { next } = invoke(middleware, { name: 'ok' }); 44 45 expect(next).toHaveBeenCalledTimes(1); 46 expect(next).toHaveBeenCalledWith(); 47 }); 48 49 test('forwards a BadRequestError when a required field is missing', () => { 50 const middleware = createBodySchemaValidator(schema); 51 const { next } = invoke(middleware, { count: 3 }); 52 53 expect(next).toHaveBeenCalledTimes(1); 54 const err = firstNextArg(next); 55 expect(err).toBeInstanceOf(CoopError); 56 expect(err).toMatchObject({ 57 name: 'BadRequestError', 58 status: 400, 59 title: 'Request body failed schema validation.', 60 }); 61 // Error message should reference the missing field, not crash. 62 expect((err as CoopError).detail).toContain('name'); 63 }); 64 65 test('forwards a BadRequestError when a field has the wrong type', () => { 66 const middleware = createBodySchemaValidator(schema); 67 const { next } = invoke(middleware, { name: 'ok', count: 'not-a-number' }); 68 69 expect(next).toHaveBeenCalledTimes(1); 70 const err = firstNextArg(next); 71 expect(err).toBeInstanceOf(CoopError); 72 expect(err).toMatchObject({ 73 name: 'BadRequestError', 74 status: 400, 75 pointer: '/count', 76 }); 77 }); 78 79 test('rejects unknown additional properties when the schema forbids them', () => { 80 const middleware = createBodySchemaValidator(schema); 81 const { next } = invoke(middleware, { name: 'ok', surprise: true }); 82 83 expect(next).toHaveBeenCalledTimes(1); 84 const err = firstNextArg(next); 85 expect(err).toBeInstanceOf(CoopError); 86 expect(err).toMatchObject({ name: 'BadRequestError', status: 400 }); 87 }); 88 89 test('rejects non-object bodies (e.g., undefined from a request with no body)', () => { 90 const middleware = createBodySchemaValidator(schema); 91 const { next } = invoke(middleware, undefined); 92 93 expect(next).toHaveBeenCalledTimes(1); 94 const err = firstNextArg(next); 95 expect(err).toBeInstanceOf(CoopError); 96 expect(err).toMatchObject({ name: 'BadRequestError', status: 400 }); 97 }); 98 99 test('does not leak Ajv internals (schemaPath / params) in the error detail', () => { 100 const middleware = createBodySchemaValidator(schema); 101 const { next } = invoke(middleware, { name: 42 }); 102 103 const err = firstNextArg(next) as CoopError; 104 expect(err.detail ?? '').not.toContain('schemaPath'); 105 expect(err.detail ?? '').not.toContain('params'); 106 }); 107});