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/local-link.json b/__tests__/fixtures/local-link.json new file mode 100644 index 00000000..0c1007e9 --- /dev/null +++ b/__tests__/fixtures/local-link.json @@ -0,0 +1,369 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Link Example", + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://local-link.com" + } + ], + "paths": { + "/2.0/users/{username}": { + "get": { + "operationId": "getUserByName", + "security": [ + { + "cookieAuth": [] + }, + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The User", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/user" + } + } + }, + "links": { + "userRepositories": { + "$ref": "#/components/links/UserRepositories" + } + } + } + } + } + }, + "/2.0/repositories/{username}": { + "get": { + "operationId": "getRepositoriesByOwner", + "parameters": [ + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "repositories owned by the supplied user", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/repository" + } + } + } + }, + "links": { + "userRepository": { + "$ref": "#/components/links/UserRepository" + } + } + } + } + } + }, + "/2.0/repositories/{username}/{slug}": { + "get": { + "operationId": "getRepository", + "parameters": [ + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The repository", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/repository" + } + } + }, + "links": { + "repositoryPullRequests": { + "$ref": "#/components/links/RepositoryPullRequests" + } + } + } + } + } + }, + "/2.0/repositories/{username}/{slug}/pullrequests": { + "get": { + "operationId": "getPullRequestsByRepository", + "parameters": [ + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "state", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "open", + "merged", + "declined" + ] + } + }, + { + "$ref": "#/components/parameters/host" + } + ], + "responses": { + "200": { + "description": "an array of pull request objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pullrequest" + } + } + } + } + } + } + } + }, + "/2.0/repositories/{username}/{slug}/pullrequests/{pid}": { + "get": { + "operationId": "getPullRequestsById", + "parameters": [ + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "pid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "a pull request object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pullrequest" + } + } + }, + "links": { + "pullRequestMerge": { + "$ref": "#/components/links/PullRequestMerge" + } + } + } + } + } + }, + "/2.0/repositories/{username}/{slug}/pullrequests/{pid}/merge": { + "post": { + "operationId": "mergePullRequest", + "parameters": [ + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "pid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "the PR was successfully merged" + } + } + } + } + }, + "components": { + "links": { + "UserRepositories": { + "operationId": "getRepositoriesByOwner", + "parameters": { + "username": "$response.body#/username" + } + }, + "UserRepository": { + "operationId": "getRepository", + "parameters": { + "username": "$response.body#/owner/username", + "slug": "$response.body#/slug" + } + }, + "RepositoryPullRequests": { + "operationId": "getPullRequestsByRepository", + "parameters": { + "username": "$response.body#/owner/username", + "slug": "$response.body#/slug" + } + }, + "PullRequestMerge": { + "operationId": "mergePullRequest", + "parameters": { + "username": "$response.body#/author/username", + "slug": "$response.body#/repository/slug", + "pid": "$response.body#/id" + } + } + }, + "schemas": { + "user": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "repository": { + "type": "object", + "properties": { + "slug": { + "type": "string" + }, + "owner": { + "$ref": "#/components/schemas/user" + } + } + }, + "pullrequest": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "repository": { + "$ref": "#/components/schemas/repository" + }, + "author": { + "$ref": "#/components/schemas/user" + } + } + } + }, + "securitySchemes": { + "apiKey": { + "name": "X-API-KEY", + "type": "apiKey", + "in": "header" + }, + "basicAuth": { + "type": "http", + "scheme": "basic" + }, + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + }, + "cookieAuth": { + "type": "apiKey", + "in": "cookie", + "name": "cookieSessionId" + } + }, + "parameters": { + "host": { + "in": "header", + "name": "hostname", + "schema": { + "type": "string" + } + } + } + } +} diff --git a/__tests__/fixtures/multiple-securities.json b/__tests__/fixtures/multiple-securities.json index 39cf176f..1a556413 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", @@ -234,7 +237,7 @@ }, "apiKeyScheme": { "type": "apiKey", - "name": "apiKey", + "name": "testKey", "in": "header" }, "unknownAuthType": { diff --git a/__tests__/fixtures/petstore.json b/__tests__/fixtures/petstore.json index eef09161..39d0b3be 100644 --- a/__tests__/fixtures/petstore.json +++ b/__tests__/fixtures/petstore.json @@ -303,7 +303,7 @@ "operationId": "deletePet", "parameters": [ { - "name": "api_key", + "name": "testKey", "in": "header", "required": false, "schema": { @@ -984,7 +984,7 @@ }, "api_key": { "type": "apiKey", - "name": "api_key", + "name": "testKey", "in": "header" } }, diff --git a/__tests__/oas.test.js b/__tests__/oas.test.js index 47b9a150..5a712f94 100644 --- a/__tests__/oas.test.js +++ b/__tests__/oas.test.js @@ -2,281 +2,408 @@ const Oas = require('../src/oas'); const { Operation } = require('../src/oas'); const petstore = require('./fixtures/petstore.json'); const multipleSecurities = require('./fixtures/multiple-securities.json'); +const referenceSpec = require('./fixtures/local-link.json'); + +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'); + }); -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(); + it('should remove end slash from the server URL', () => { + expect(new Oas({ servers: [{ url: 'http://example.com/' }] }).url()).toBe('http://example.com'); }); -}); -test('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('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.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}'); + }); }); - 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' }, - ], + 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'; + + const res = oas.findOperation(uri, method); + expect(res).toBeUndefined(); + }); + + 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 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); + expect(res).toMatchObject({ + url: { + origin: 'http://petstore.swagger.io/v2', + path: '/pet/:petId', + slugs: { + ':petId': '1', + }, + method: 'DELETE', }, - 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}'); + it('should return normally if path is formatted poorly', () => { + const oas = new Oas(petstore); + const uri = `http://petstore.swagger.io/v2/pet/1/`; + const method = 'DELETE'; + + const res = oas.findOperation(uri, method); + expect(res).toMatchObject({ + url: { + origin: 'http://petstore.swagger.io/v2', + path: '/pet/:petId', + slugs: { + ':petId': '1', + }, + method: 'DELETE', + }, + }); + }); }); }); -test('should be able to access properties on oas', () => { - expect( - new Oas({ - info: { version: '1.0' }, - }).info.version - ).toBe('1.0'); -}); - -describe('operation.getSecurity()', () => { - const security = [{ auth: [] }]; +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, + }, + }, + }, + }) + .operation('/things', 'post') + .getSecurity() + ).toBe(security); + }); - it('should return the security on this endpoint', () => { - expect( - new Oas({ - info: { version: '1.0' }, - paths: { - '/things': { - post: { - security, + it('should fallback to global security', () => { + expect( + new Oas({ + info: { version: '1.0' }, + paths: { + '/things': { + post: {}, }, }, - }, - }) - .operation('/things', 'post') - .getSecurity() - ).toBe(security); - }); + security, + }) + .operation('/things', 'post') + .getSecurity() + ).toBe(security); + }); - it('should fallback to global security', () => { - expect( - new Oas({ - info: { version: '1.0' }, - paths: { - '/things': { - post: {}, + it('should default to empty array', () => { + expect( + new Oas({ + info: { version: '1.0' }, + paths: { + '/things': { + post: {}, + }, }, - }, - security, - }) - .operation('/things', 'post') - .getSecurity() - ).toBe(security); + }) + .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', - }, + 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({ + Query: [oas.components.securitySchemes.securityScheme], + }); }); - const operation = oas.operation(path, method); - expect(operation.prepareSecurity()).toStrictEqual({ - Bearer: [oas.components.securitySchemes.securityScheme], + 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], + }); }); - }); - it('apiKey/query: should return with a type of Query', () => { - const oas = createSecurityOas({ - securityScheme: { - type: 'apiKey', - in: 'query', - }, + it('should work for petstore', () => { + const operation = new Oas(petstore).operation('/pet', 'post'); + + expect(operation.prepareSecurity()).toMatchSnapshot(); }); - const operation = oas.operation(path, method); - expect(operation.prepareSecurity()).toStrictEqual({ - Query: [oas.components.securitySchemes.securityScheme], + it('should work for multiple securities (||)', () => { + const operation = new Oas(multipleSecurities).operation('/or-security', 'post'); + + expect(Object.keys(operation.prepareSecurity())).toHaveLength(2); }); - }); - 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('/and-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 (&& and ||)', () => { + const operation = new Oas(multipleSecurities).operation('/and-or-security', 'post'); + + expect(operation.prepareSecurity().OAuth2).toHaveLength(2); + expect(operation.prepareSecurity().Header).toHaveLength(1); }); - }); - it('should work for petstore', () => { - const operation = new Oas(petstore).operation('/pet', 'post'); + it.todo('should set a `key` property'); - expect(operation.prepareSecurity()).toMatchSnapshot(); - }); + // 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('/or-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 (&&)', () => { - const operation = new Oas(multipleSecurities).operation('/and-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(Object.keys(operation.prepareSecurity())).toHaveLength(2); + 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('should work for multiple securities (&& and ||)', () => { - const operation = new Oas(multipleSecurities).operation('/and-or-security', 'post'); + 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'; - expect(operation.prepareSecurity().OAuth2).toHaveLength(2); - expect(operation.prepareSecurity().Header).toHaveLength(1); - }); + const logOperation = oas.findOperation(uri, method); + const operation = new Operation(oas, logOperation.url.path, logOperation.url.method, logOperation.operation); - it.todo('should set a `key` property'); + expect(operation.getHeaders()).toMatchObject({ + request: ['testKey'], + response: [], + }); + }); - // TODO We dont currently support cookies? - it.todo('apiKey/cookie: should return with a type of Cookie'); + 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.todo('should throw if attempting to use a non-existent scheme'); + const logOperation = oas.findOperation(uri, method); + const operation = new Operation(oas, logOperation.url.path, logOperation.url.method, logOperation.operation); - it('should return empty object if no security', () => { - const operation = new Oas(multipleSecurities).operation('/no-auth', 'post'); - expect(Object.keys(operation.prepareSecurity())).toHaveLength(0); - }); + expect(operation.getHeaders()).toMatchObject({ + request: ['testKey'], + response: [], + }); + }); - 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); - }); + it('should return a Cookie header if security is located in cookie scheme', () => { + const oas = new Oas(referenceSpec); + const uri = 'http://local-link.com/2.0/users/johnSmith'; + const method = 'GET'; + + const logOperation = oas.findOperation(uri, method); + const operation = new Operation(oas, logOperation.url.path, logOperation.url.method, logOperation.operation); + console.log(operation.getHeaders()); + expect(operation.getHeaders()).toMatchObject({ + request: ['Cookie', 'Authorization'], + response: [], + }); + }); - 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('should target parameter refs and return names if applicable', () => { + const oas = new Oas(referenceSpec); + const uri = 'http://local-link.com/2.0/repositories/janeDoe/oas/pullrequests'; + const method = 'GET'; + + const logOperation = oas.findOperation(uri, method); + const operation = new Operation(oas, logOperation.url.path, logOperation.url.method, logOperation.operation); + expect(operation.getHeaders()).toMatchObject({ + request: ['hostname'], + response: [], + }); + }); }); }); diff --git a/package-lock.json b/package-lock.json index 41478feb..0c453053 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6863,6 +6863,23 @@ "just-extend": "^4.0.2", "lolex": "^5.0.1", "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } } }, "node-fetch": { @@ -7575,21 +7592,9 @@ "dev": true }, "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dev": true, - "requires": { - "isarray": "0.0.1" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - } - } + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.1.0.tgz", + "integrity": "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==" }, "path-type": { "version": "3.0.0", diff --git a/package.json b/package.json index 34dc5ea1..068b1c1a 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "node-status": "^1.0.0", "oas-normalize": "1.0.0", "open": "^7.0.0", + "path-to-regexp": "^6.1.0", "prompt-sync": "^4.1.4", "request": "^2.88.0", "swagger-inline": "3.0.0", diff --git a/src/lib/get-user-variable.js b/src/lib/get-user-variable.js index 276781dc..787a399e 100644 --- a/src/lib/get-user-variable.js +++ b/src/lib/get-user-variable.js @@ -2,7 +2,7 @@ function getKey(user, property) { return user[property] || null; } -function getUserVariable(user, property, selectedApp = false) { +module.exports = function getUserVariable(user, property, selectedApp = false) { if (user.keys) { if (selectedApp) { return getKey( @@ -15,6 +15,4 @@ function getUserVariable(user, property, selectedApp = false) { } return getKey(user, property); -} - -module.exports = getUserVariable; +}; diff --git a/src/oas.js b/src/oas.js index c9b2e908..5643dec8 100644 --- a/src/oas.js +++ b/src/oas.js @@ -1,6 +1,8 @@ /* eslint-disable max-classes-per-file */ +const { pathToRegexp, match } = require('path-to-regexp'); const getPathOperation = require('./lib/get-path-operation'); const getUserVariable = require('./lib/get-user-variable'); +const findSchemaDefinition = require('./lib/find-schema-definition'); class Operation { constructor(oas, path, method, operation) { @@ -41,10 +43,9 @@ class Operation { if (security.scheme === 'bearer') type = 'Bearer'; } else if (security.type === 'oauth2') { type = 'OAuth2'; - } else if (security.type === 'apiKey' && security.in === 'query') { - type = 'Query'; - } else if (security.type === 'apiKey' && security.in === 'header') { - type = 'Header'; + } else if (security.type === 'apiKey') { + if (security.in === 'query') type = 'Query'; + else if (security.in === 'header' || security.in === 'cookie') type = 'Header'; } else { return false; } @@ -65,6 +66,46 @@ class Operation { return prev; }, {}); } + + getHeaders() { + this.headers = { + request: [], + response: [], + }; + + const security = this.prepareSecurity(); + if (security.Header) { + this.headers.request = security.Header.map(h => { + if (h.in === 'cookie') return 'Cookie'; + return h.name; + }); + } + if (security.Bearer || security.Basic) { + this.headers.request.push('Authorization'); + } + + if (this.parameters) { + this.headers.request = this.headers.request.concat( + this.parameters + .map(p => { + if (p.in && p.in === 'header') return p.name; + if (p.$ref) { + const { name } = findSchemaDefinition(p.$ref, this.oas); + return name; + } + return undefined; + }) + .filter(p => p) + ); + } + + this.headers.response = Object.keys(this.responses) + .filter(r => this.responses[r].headers) + .map(r => Object.keys(this.responses[r].headers)) + .reduce((a, b) => a.concat(b), []); + + return this.headers; + } } function ensureProtocol(url) { @@ -101,6 +142,73 @@ function normalizedUrl(oas) { return ensureProtocol(url); } +function normalizePath(path) { + return path.replace(/{(.*?)}/g, ':$1'); +} + +function generatePathMatches(paths, pathName, origin) { + return Object.keys(paths) + .map(path => { + const cleanedPath = normalizePath(path); + const matchStatement = match(cleanedPath, { decode: decodeURIComponent }); + const matchResult = matchStatement(pathName); + const slugs = {}; + + if (matchResult && Object.keys(matchResult.params).length) { + Object.keys(matchResult.params).forEach(param => { + slugs[`:${param}`] = matchResult.params[param]; + }); + } + + return { + url: { + origin, + path: cleanedPath, + slugs, + }, + operation: paths[path], + match: matchResult, + }; + }) + .filter(p => p.match); +} + +function filterPathMethods(pathMatches, targetMethod) { + const regExp = pathToRegexp(targetMethod); + return pathMatches + .map(p => { + const captures = Object.keys(p.operation).filter(r => regExp.exec(r)); + + if (captures.length) { + const method = captures[0]; + p.url.method = method.toUpperCase(); + + return { + url: p.url, + operation: p.operation[method], + }; + } + return undefined; + }) + .filter(p => p); +} + +function findTargetPath(pathMatches) { + let minCount = Object.keys(pathMatches[0].url.slugs).length; + let operation; + + for (let m = 0; m < pathMatches.length; m += 1) { + const selection = pathMatches[m]; + const paramCount = Object.keys(selection.url.slugs).length; + if (paramCount <= minCount) { + minCount = paramCount; + operation = selection; + } + } + + return operation; +} + class Oas { constructor(oas, user) { Object.assign(this, oas); @@ -128,6 +236,24 @@ class Oas { const operation = getPathOperation(this, { swagger: { path }, api: { method } }); return new Operation(this, path, method, operation); } + + findOperation(url, method) { + const { origin } = new URL(url); + const originRegExp = new RegExp(origin); + const { servers, paths } = this; + + const targetServer = servers.find(s => originRegExp.exec(s.url)); + if (!targetServer) return undefined; + + const [, pathName] = url.split(targetServer.url); + const annotatedPaths = generatePathMatches(paths, pathName, targetServer.url); + if (!annotatedPaths.length) return undefined; + + const includesMethod = filterPathMethods(annotatedPaths, method); + if (!includesMethod.length) return undefined; + + return findTargetPath(includesMethod); + } } module.exports = Oas;