diff --git a/src/backend.test.ts b/src/backend.test.ts index 60298b5b..31bad0bd 100644 --- a/src/backend.test.ts +++ b/src/backend.test.ts @@ -415,6 +415,74 @@ describe('OpenAPIBackend', () => { expect(context.security?.authorized).toBe(true); }); + test('sets context.security.authorized=true for an api key search operation', async () => { + const xquikDefinition: OpenAPIV3_1.Document = { + ...meta, + paths: { + '/api/v1/x/tweets/search': { + get: { + operationId: 'searchTweets', + parameters: [ + { + name: 'q', + in: 'query', + required: true, + schema: { type: 'string' }, + }, + { + name: 'limit', + in: 'query', + required: false, + schema: { type: 'integer', default: 20, maximum: 200 }, + }, + ], + security: [{ apiKey: [] }, { oauthBearer: [] }, {}], + responses, + }, + }, + }, + components: { + securitySchemes: { + apiKey: { + type: 'apiKey', + in: 'header', + name: 'x-api-key', + }, + oauthBearer: { + type: 'http', + scheme: 'bearer', + }, + }, + }, + }; + const api = new OpenAPIBackend({ definition: xquikDefinition }); + let context: Partial = {}; + api.register('searchTweets', (c) => { + context = c; + return { + tweets: [], + has_next_page: false, + next_cursor: '', + }; + }); + api.registerSecurityHandler('apiKey', (c) => c.request.headers['x-api-key'] === 'test-key'); + + await api.init(); + const result = await api.handleRequest({ + method: 'get', + path: '/api/v1/x/tweets/search?q=openapi', + headers: { 'x-api-key': 'test-key' }, + }); + + expect(result).toEqual({ + tweets: [], + has_next_page: false, + next_cursor: '', + }); + expect(context.security?.apiKey).toBe(true); + expect(context.security?.authorized).toBe(true); + }); + test('sets context.security.authorized=false if security requirements not met', async () => { const api = new OpenAPIBackend({ definition }); let context: Partial = {}; diff --git a/src/validation.test.ts b/src/validation.test.ts index 1d04a516..6a4e94a7 100644 --- a/src/validation.test.ts +++ b/src/validation.test.ts @@ -497,6 +497,7 @@ describe.each([{}, { lazyCompileValidators: true }])('OpenAPIValidator with opts operationId: 'createPet', responses: { 200: { description: 'ok' } }, requestBody: { + required: true, content: { 'application/json': { schema: petSchema, @@ -519,6 +520,19 @@ describe.each([{}, { lazyCompileValidators: true }])('OpenAPIValidator with opts }, }, }, + '/pets/optional': { + post: { + operationId: 'createOptionalPet', + responses: { 200: { description: 'ok' } }, + requestBody: { + content: { + 'application/json': { + schema: petSchema, + }, + }, + }, + }, + }, '/pets/schedule': { post: { operationId: 'createPetSchedule', @@ -631,6 +645,15 @@ describe.each([{}, { lazyCompileValidators: true }])('OpenAPIValidator with opts expect(valid.errors).toHaveLength(1); }); + test('passes validation with an omitted optional request body', async () => { + const valid = validator.validateRequest({ + path: '/pets/optional', + method: 'post', + headers, + }); + expect(valid.errors).toBeFalsy(); + }); + test('fails validation for non-json data when the only media type defined is application/json', async () => { const valid = validator.validateRequest({ path: '/pets', diff --git a/src/validation.ts b/src/validation.ts index 88ed6f51..83bf6ce8 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -578,8 +578,7 @@ export class OpenAPIValidator { }, }; requestBodySchema.required = []; - if (_.keys(requestBody.content).length === 1) { - // if application/json is the only specified format, it's required + if (requestBody.required === true) { requestBodySchema.required.push('requestBody'); }