diff --git a/__tests__/__snapshots__/oas.test.js.snap b/__tests__/__snapshots__/oas.test.js.snap index fd370d3b..91a638b4 100644 --- a/__tests__/__snapshots__/oas.test.js.snap +++ b/__tests__/__snapshots__/oas.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`operation() should return a default when no operation 1`] = ` +exports[`class.Oas operation() should return a default when no operation 1`] = ` Operation { "method": "get", "oas": Oas { @@ -11,7 +11,7 @@ Operation { } `; -exports[`operation.prepareSecurity() should work for petstore 1`] = ` +exports[`class.operation prepareSecurity() should work for petstore 1`] = ` Object { "OAuth2": Array [ Object { diff --git a/__tests__/fixtures/multiple-securities.json b/__tests__/fixtures/multiple-securities.json index 39cf176f..634de191 100644 --- a/__tests__/fixtures/multiple-securities.json +++ b/__tests__/fixtures/multiple-securities.json @@ -140,6 +140,9 @@ }, { "oauthDiff": ["write:things", "read:things"] + }, + { + "apiKeyScheme": [] } ], "summary": "second and does not show security", diff --git a/__tests__/oas.test.js b/__tests__/oas.test.js index 47b9a150..0fb7cf41 100644 --- a/__tests__/oas.test.js +++ b/__tests__/oas.test.js @@ -3,280 +3,362 @@ const { Operation } = require('../src/oas'); const petstore = require('./fixtures/petstore.json'); const multipleSecurities = require('./fixtures/multiple-securities.json'); -describe('operation()', () => { - it('should return an operation object', () => { - const oas = { paths: { '/path': { get: { a: 1 } } } }; - const operation = new Oas(oas).operation('/path', 'get'); - expect(operation).toBeInstanceOf(Operation); - expect(operation.a).toBe(1); - expect(operation.path).toBe('/path'); - expect(operation.method).toBe('get'); - }); +describe('class.Oas', () => { + describe('operation()', () => { + it('should return an operation object', () => { + const oas = { paths: { '/path': { get: { a: 1 } } } }; + const operation = new Oas(oas).operation('/path', 'get'); + expect(operation).toBeInstanceOf(Operation); + expect(operation.a).toBe(1); + expect(operation.path).toBe('/path'); + expect(operation.method).toBe('get'); + }); - it('should return a default when no operation', () => { - expect(new Oas({}).operation('/unknown', 'get')).toMatchSnapshot(); + it('should return a default when no operation', () => { + expect(new Oas({}).operation('/unknown', 'get')).toMatchSnapshot(); + }); }); -}); -test('should remove end slash from the server URL', () => { - expect(new Oas({ servers: [{ url: 'http://example.com/' }] }).url()).toBe('http://example.com'); -}); + it('should remove end slash from the server URL', () => { + expect(new Oas({ servers: [{ url: 'http://example.com/' }] }).url()).toBe('http://example.com'); + }); -test('should default missing servers array to example.com', () => { - expect(new Oas({}).url()).toBe('https://example.com'); -}); + it('should default missing servers array to example.com', () => { + expect(new Oas({}).url()).toBe('https://example.com'); + }); -test('should default empty servers array to example.com', () => { - expect(new Oas({ servers: [] }).url()).toBe('https://example.com'); -}); + it('should default empty servers array to example.com', () => { + expect(new Oas({ servers: [] }).url()).toBe('https://example.com'); + }); -test('should default empty server object to example.com', () => { - expect(new Oas({ servers: [{}] }).url()).toBe('https://example.com'); -}); + it('should default empty server object to example.com', () => { + expect(new Oas({ servers: [{}] }).url()).toBe('https://example.com'); + }); -test('should add https:// if url starts with //', () => { - expect(new Oas({ servers: [{ url: '//example.com' }] }).url()).toBe('https://example.com'); -}); + it('should add https:// if url starts with //', () => { + expect(new Oas({ servers: [{ url: '//example.com' }] }).url()).toBe('https://example.com'); + }); -test('should add https:// if url does not start with a protocol', () => { - expect(new Oas({ servers: [{ url: 'example.com' }] }).url()).toBe('https://example.com'); -}); + it('should add https:// if url does not start with a protocol', () => { + expect(new Oas({ servers: [{ url: 'example.com' }] }).url()).toBe('https://example.com'); + }); -describe('server variables', () => { - it('should use defaults', () => { + it('should be able to access properties on oas', () => { expect( new Oas({ - servers: [{ url: 'https://example.com/{path}', variables: { path: { default: 'path' } } }], - }).url() - ).toBe('https://example.com/path'); + info: { version: '1.0' }, + }).info.version + ).toBe('1.0'); }); - it('should use user variables over defaults', () => { - expect( - new Oas( - { - servers: [{ url: 'https://{username}.example.com', variables: { username: { default: 'demo' } } }], - }, - { username: 'domh' } - ).url() - ).toBe('https://domh.example.com'); - }); + describe('server variables', () => { + it('should use defaults', () => { + expect( + new Oas({ + servers: [{ url: 'https://example.com/{path}', variables: { path: { default: 'path' } } }], + }).url() + ).toBe('https://example.com/path'); + }); - it('should fetch user variables from keys array', () => { - expect( - new Oas( - { - servers: [{ url: 'https://{username}.example.com', variables: { username: { default: 'demo' } } }], - }, - { keys: [{ name: 1, username: 'domh' }] } - ).url() - ).toBe('https://domh.example.com'); - }); + it('should use user variables over defaults', () => { + expect( + new Oas( + { + servers: [{ url: 'https://{username}.example.com', variables: { username: { default: 'demo' } } }], + }, + { username: 'domh' } + ).url() + ).toBe('https://domh.example.com'); + }); - it.skip('should fetch user variables from selected app', () => { - expect( - new Oas( - { - servers: [{ url: 'https://{username}.example.com', variables: { username: { default: 'demo' } } }], - }, - { - keys: [ - { name: 1, username: 'domh' }, - { name: 2, username: 'readme' }, - ], - }, - 2 - ).url() - ).toBe('https://readme.example.com'); - }); + it('should fetch user variables from keys array', () => { + expect( + new Oas( + { + servers: [{ url: 'https://{username}.example.com', variables: { username: { default: 'demo' } } }], + }, + { keys: [{ name: 1, username: 'domh' }] } + ).url() + ).toBe('https://domh.example.com'); + }); - // Test encodeURI - it('should pass through if no default set', () => { - expect(new Oas({ servers: [{ url: 'https://example.com/{path}' }] }).url()).toBe('https://example.com/{path}'); + it.skip('should fetch user variables from selected app', () => { + expect( + new Oas( + { + servers: [{ url: 'https://{username}.example.com', variables: { username: { default: 'demo' } } }], + }, + { + keys: [ + { name: 1, username: 'domh' }, + { name: 2, username: 'readme' }, + ], + }, + 2 + ).url() + ).toBe('https://readme.example.com'); + }); + + // Test encodeURI + it('should pass through if no default set', () => { + expect(new Oas({ servers: [{ url: 'https://example.com/{path}' }] }).url()).toBe('https://example.com/{path}'); + }); }); -}); -test('should be able to access properties on oas', () => { - expect( - new Oas({ - info: { version: '1.0' }, - }).info.version - ).toBe('1.0'); -}); + describe('findOperation', () => { + it('should return undefined if no server found', () => { + const oas = new Oas(petstore); + const uri = `http://localhost:3000/pet/1`; + const method = 'DELETE'; -describe('operation.getSecurity()', () => { - const security = [{ auth: [] }]; + const res = oas.findOperation(uri, method); + expect(res).toBeUndefined(); + }); - it('should return the security on this endpoint', () => { - expect( - new Oas({ - info: { version: '1.0' }, - paths: { - '/things': { - post: { - security, - }, + it('should return undefined if no path matches found', () => { + const oas = new Oas(petstore); + const uri = `http://petstore.swagger.io/v2/search`; + const method = 'GET'; + + const res = oas.findOperation(uri, method); + expect(res).toBeUndefined(); + }); + + it('should return undefined if no matching methods in path', () => { + const oas = new Oas(petstore); + const uri = `http://petstore.swagger.io/v2/pet/1`; + const method = 'PATCH'; + + const res = oas.findOperation(uri, method); + expect(res).toBeUndefined(); + }); + + it('should return a a result if found', () => { + const oas = new Oas(petstore); + const uri = `http://petstore.swagger.io/v2/pet/1`; + const method = 'DELETE'; + + const res = oas.findOperation(uri, method); + console.log(JSON.stringify(res)); + expect(res.logOperation).toMatchObject({ + url: { + origin: 'http://petstore.swagger.io/v2', + path: '/pet/:petId', + slugs: { + ':petId': '1', }, + method: 'DELETE', }, - }) - .operation('/things', 'post') - .getSecurity() - ).toBe(security); + }); + }); }); +}); - it('should fallback to global security', () => { - expect( - new Oas({ - info: { version: '1.0' }, - paths: { - '/things': { - post: {}, +describe('class.operation', () => { + describe('getSecurity()', () => { + const security = [{ auth: [] }]; + + it('should return the security on this endpoint', () => { + expect( + new Oas({ + info: { version: '1.0' }, + paths: { + '/things': { + post: { + security, + }, + }, }, - }, - security, - }) - .operation('/things', 'post') - .getSecurity() - ).toBe(security); + }) + .operation('/things', 'post') + .getSecurity() + ).toBe(security); + }); + + it('should fallback to global security', () => { + expect( + new Oas({ + info: { version: '1.0' }, + paths: { + '/things': { + post: {}, + }, + }, + security, + }) + .operation('/things', 'post') + .getSecurity() + ).toBe(security); + }); + + it('should default to empty array', () => { + expect( + new Oas({ + info: { version: '1.0' }, + paths: { + '/things': { + post: {}, + }, + }, + }) + .operation('/things', 'post') + .getSecurity() + ).toStrictEqual([]); + }); }); - it('should default to empty array', () => { - expect( - new Oas({ - info: { version: '1.0' }, + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securitySchemeObject + describe('prepareSecurity()', () => { + const path = '/auth'; + const method = 'get'; + + function createSecurityOas(schemes) { + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securityRequirementObject + const security = Object.keys(schemes).map(scheme => { + return { [scheme]: [] }; + }); + + return new Oas({ + components: { securitySchemes: schemes }, paths: { - '/things': { - post: {}, + [path]: { + [method]: { security }, }, }, - }) - .operation('/things', 'post') - .getSecurity() - ).toStrictEqual([]); - }); -}); - -// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securitySchemeObject -describe('operation.prepareSecurity()', () => { - const path = '/auth'; - const method = 'get'; + }); + } + + it('http/basic: should return with a type of Basic', () => { + const oas = createSecurityOas({ + securityScheme: { + type: 'http', + scheme: 'basic', + }, + }); + const operation = oas.operation(path, method); - function createSecurityOas(schemes) { - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securityRequirementObject - const security = Object.keys(schemes).map(scheme => { - return { [scheme]: [] }; + expect(operation.prepareSecurity()).toStrictEqual({ + Basic: [oas.components.securitySchemes.securityScheme], + }); }); - return new Oas({ - components: { securitySchemes: schemes }, - paths: { - [path]: { - [method]: { security }, + it('http/bearer: should return with a type of Bearer', () => { + const oas = createSecurityOas({ + securityScheme: { + type: 'http', + scheme: 'bearer', }, - }, - }); - } - - it('http/basic: should return with a type of Basic', () => { - const oas = createSecurityOas({ - securityScheme: { - type: 'http', - scheme: 'basic', - }, - }); - const operation = oas.operation(path, method); + }); + const operation = oas.operation(path, method); - expect(operation.prepareSecurity()).toStrictEqual({ - Basic: [oas.components.securitySchemes.securityScheme], + expect(operation.prepareSecurity()).toStrictEqual({ + Bearer: [oas.components.securitySchemes.securityScheme], + }); }); - }); - it('http/bearer: should return with a type of Bearer', () => { - const oas = createSecurityOas({ - securityScheme: { - type: 'http', - scheme: 'bearer', - }, - }); - const operation = oas.operation(path, method); + it('apiKey/query: should return with a type of Query', () => { + const oas = createSecurityOas({ + securityScheme: { + type: 'apiKey', + in: 'query', + }, + }); + const operation = oas.operation(path, method); - expect(operation.prepareSecurity()).toStrictEqual({ - Bearer: [oas.components.securitySchemes.securityScheme], + expect(operation.prepareSecurity()).toStrictEqual({ + Query: [oas.components.securitySchemes.securityScheme], + }); }); - }); - it('apiKey/query: should return with a type of Query', () => { - const oas = createSecurityOas({ - securityScheme: { - type: 'apiKey', - in: 'query', - }, + it('apiKey/header: should return with a type of Header', () => { + const oas = createSecurityOas({ + securityScheme: { + type: 'apiKey', + in: 'header', + }, + }); + const operation = oas.operation(path, method); + + expect(operation.prepareSecurity()).toStrictEqual({ + Header: [oas.components.securitySchemes.securityScheme], + }); }); - const operation = oas.operation(path, method); - expect(operation.prepareSecurity()).toStrictEqual({ - Query: [oas.components.securitySchemes.securityScheme], + it('should work for petstore', () => { + const operation = new Oas(petstore).operation('/pet', 'post'); + + expect(operation.prepareSecurity()).toMatchSnapshot(); }); - }); - it('apiKey/header: should return with a type of Header', () => { - const oas = createSecurityOas({ - securityScheme: { - type: 'apiKey', - in: 'header', - }, + it('should work for multiple securities (||)', () => { + const operation = new Oas(multipleSecurities).operation('/or-security', 'post'); + + expect(Object.keys(operation.prepareSecurity())).toHaveLength(2); }); - const operation = oas.operation(path, method); - expect(operation.prepareSecurity()).toStrictEqual({ - Header: [oas.components.securitySchemes.securityScheme], + it('should work for multiple securities (&&)', () => { + const operation = new Oas(multipleSecurities).operation('/and-security', 'post'); + + expect(Object.keys(operation.prepareSecurity())).toHaveLength(2); }); - }); - it('should work for petstore', () => { - const operation = new Oas(petstore).operation('/pet', 'post'); + it('should work for multiple securities (&& and ||)', () => { + const operation = new Oas(multipleSecurities).operation('/and-or-security', 'post'); - expect(operation.prepareSecurity()).toMatchSnapshot(); - }); + expect(operation.prepareSecurity().OAuth2).toHaveLength(2); + expect(operation.prepareSecurity().Header).toHaveLength(1); + }); - it('should work for multiple securities (||)', () => { - const operation = new Oas(multipleSecurities).operation('/or-security', 'post'); + it.todo('should set a `key` property'); - expect(Object.keys(operation.prepareSecurity())).toHaveLength(2); - }); + // TODO We dont currently support cookies? + it.todo('apiKey/cookie: should return with a type of Cookie'); - it('should work for multiple securities (&&)', () => { - const operation = new Oas(multipleSecurities).operation('/and-security', 'post'); + it.todo('should throw if attempting to use a non-existent scheme'); - expect(Object.keys(operation.prepareSecurity())).toHaveLength(2); - }); + it('should return empty object if no security', () => { + const operation = new Oas(multipleSecurities).operation('/no-auth', 'post'); + expect(Object.keys(operation.prepareSecurity())).toHaveLength(0); + }); - it('should work for multiple securities (&& and ||)', () => { - const operation = new Oas(multipleSecurities).operation('/and-or-security', 'post'); + it('should return empty object if security scheme doesnt exist', () => { + const operation = new Oas(multipleSecurities).operation('/unknown-scheme', 'post'); + expect(Object.keys(operation.prepareSecurity())).toHaveLength(0); + }); - expect(operation.prepareSecurity().OAuth2).toHaveLength(2); - expect(operation.prepareSecurity().Header).toHaveLength(1); + it('should return empty if security scheme type doesnt exist', () => { + const operation = new Oas(multipleSecurities).operation('/unknown-auth-type', 'post'); + expect(Object.keys(operation.prepareSecurity())).toHaveLength(0); + }); }); - it.todo('should set a `key` property'); + describe('getHeaders()', () => { + it('should return an object containing request headers if params exist', () => { + const oas = new Oas(petstore); + const uri = `http://petstore.swagger.io/v2/pet/1`; + const method = 'DELETE'; - // TODO We dont currently support cookies? - it.todo('apiKey/cookie: should return with a type of Cookie'); + const { logOperation } = oas.findOperation(uri, method); + const operation = new Operation(oas, logOperation.url.path, logOperation.url.method, logOperation.operation); - it.todo('should throw if attempting to use a non-existent scheme'); + expect(operation.getHeaders()).toMatchObject({ + request: ['api_key'], + response: [], + }); + }); - it('should return empty object if no security', () => { - const operation = new Oas(multipleSecurities).operation('/no-auth', 'post'); - expect(Object.keys(operation.prepareSecurity())).toHaveLength(0); - }); + it('should return an object containing request headers if security exists', () => { + const oas = new Oas(multipleSecurities); + const uri = 'http://example.com/multiple-combo-auths'; + const method = 'POST'; - it('should return empty object if security scheme doesnt exist', () => { - const operation = new Oas(multipleSecurities).operation('/unknown-scheme', 'post'); - expect(Object.keys(operation.prepareSecurity())).toHaveLength(0); - }); + const { logOperation } = oas.findOperation(uri, method); + const operation = new Operation(oas, logOperation.url.path, logOperation.url.method, logOperation.operation); - it('should return empty if security scheme type doesnt exist', () => { - const operation = new Oas(multipleSecurities).operation('/unknown-auth-type', 'post'); - expect(Object.keys(operation.prepareSecurity())).toHaveLength(0); + expect(operation.getHeaders()).toMatchObject({ + request: ['apiKey'], + response: [], + }); + }); }); }); diff --git a/src/oas.js b/src/oas.js index 7c01379a..5270726b 100644 --- a/src/oas.js +++ b/src/oas.js @@ -226,13 +226,15 @@ class Oas { } findOperation(url, method, index = 0) { - const { origin, pathname } = new URL(url); + const { origin } = new URL(url); + const originRegExp = new RegExp(origin); const { servers, paths } = this; - const targetServer = servers.find(s => s.url === origin); + const targetServer = servers.find(s => originRegExp.exec(s.url)); if (!targetServer) return undefined; - const annotatedPaths = generatePathMatches(paths, pathname, origin); + const [, pathName] = url.split(targetServer.url); + const annotatedPaths = generatePathMatches(paths, pathName, targetServer.url); if (!annotatedPaths.length) return undefined; const includesMethod = filterPathMethods(annotatedPaths, method);