From d7ea01a52ef3c95bff251c4c44603391cc52462a Mon Sep 17 00:00:00 2001 From: Viacheslav Turovskyi Date: Wed, 14 Sep 2022 14:55:49 +0300 Subject: [PATCH] feat: parseFromUrl does not resolve relative references (#572) --- .eslintrc | 1 + lib/parser.js | 21 ++++++-- lib/utils.js | 17 ++++++ package.json | 4 +- test/browser_test.js | 5 +- test/parseFromUrl_test.js | 8 +-- test/refs/refed.yaml | 2 +- test/sample_browser/asyncapi.yaml | 8 --- test/sample_browser/index.html | 75 ++++++++++++++++++-------- test/sample_browser/main/asyncapi.yaml | 25 +++++++++ test/sample_browser/refs/refed.yaml | 6 +++ test/sample_browser/refs/refed2.yaml | 1 + test/utils_test.js | 14 +++++ 13 files changed, 145 insertions(+), 42 deletions(-) delete mode 100644 test/sample_browser/asyncapi.yaml create mode 100644 test/sample_browser/main/asyncapi.yaml create mode 100644 test/sample_browser/refs/refed.yaml create mode 100644 test/sample_browser/refs/refed2.yaml create mode 100644 test/utils_test.js diff --git a/.eslintrc b/.eslintrc index 329b5967f..e17fc6c2f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,6 +2,7 @@ env: node: true es6: true mocha: true + browser: true plugins: - sonarjs diff --git a/lib/parser.js b/lib/parser.js index fe637eacd..f7c123a01 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -6,7 +6,14 @@ const $RefParser = require('@apidevtools/json-schema-ref-parser'); const mergePatch = require('tiny-merge-patch').apply; const ParserError = require('./errors/parser-error'); const { validateChannels, validateTags, validateServerVariables, validateOperationId, validateServerSecurity, validateMessageId } = require('./customValidators.js'); -const { toJS, findRefs, getLocationOf, improveAjvErrors, getDefaultSchemaFormat } = require('./utils'); +const { + toJS, + findRefs, + getLocationOf, + improveAjvErrors, + getDefaultSchemaFormat, + getBaseUrl, +} = require('./utils'); const AsyncAPIDocument = require('./models/asyncapi'); const OPERATIONS = ['publish', 'subscribe']; @@ -56,7 +63,11 @@ async function parse(asyncapiYAMLorJSON, options = {}) { let parsedJSON; let initialFormat; - options.path = options.path || `${process.cwd()}${path.sep}`; + if (typeof window !== 'undefined' && !options.hasOwnProperty('path')) { + options.path = getBaseUrl(window.location.href); + } else { + options.path = options.path || `${process.cwd()}${path.sep}`; + } try { ({ initialFormat, parsedJSON } = toJS(asyncapiYAMLorJSON)); @@ -128,12 +139,16 @@ async function parse(asyncapiYAMLorJSON, options = {}) { * @param {ParserOptions=} [options] Configuration to pass to the {@link #asyncapiparserparseroptions--object|ParserOptions} method. * @returns {Promise} The parsed AsyncAPI document. */ -function parseFromUrl(url, fetchOptions, options) { +function parseFromUrl(url, fetchOptions, options = {}) { //Why not just addinga default to the arguments list? //All function parameters with default values should be declared after the function parameters without default values. Otherwise, it makes it impossible for callers to take advantage of defaults; they must re-specify the defaulted values or pass undefined in order to "get to" the non-default parameters. //To not break the API by changing argument position and to silet the linter it is just better to move adding if (!fetchOptions) fetchOptions = {}; + if (!options.hasOwnProperty('path')) { + options = { ...options, path: getBaseUrl(url) }; + } + return new Promise((resolve, reject) => { fetch(url, fetchOptions) .then(res => res.text()) diff --git a/lib/utils.js b/lib/utils.js index 0faf2e0c0..e80b5a381 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -219,6 +219,23 @@ utils.parseUrlQueryParameters = str => { return str.match(/\?((.*=.*)(&?))/g); }; +/** + * Returns base URL parsed from location of AsyncAPI document + * + * @function getBaseUrl + * @private + * @param {String} url URL of AsyncAPI document + */ +utils.getBaseUrl = url => { + url = typeof url !== 'string' ? String(url) : url; + //URL validation is not performed because 'node-fetch' performs its own + //validation at fetch time, so no repetition of this task is made. + //Only ensuring that 'url' has type of 'string' and letting 'node-fetch' deal + //with the rest. + + return url.substring(0, url.lastIndexOf('/') + 1); +}; + /** * Returns an array of not existing properties in provided object with names specified in provided array * @function getMissingProps diff --git a/package.json b/package.json index e96059e7f..453c9d38d 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "release": "semantic-release", "lint": "eslint --max-warnings 0 --config \".eslintrc\" \".\"", "lint:fix": "eslint --max-warnings 0 --config \".eslintrc\" \".\" --fix", - "test:lib": "nyc --silent --no-clean mocha --exclude \"test/browser_test.js\" --exclude \"test/parseFromUrl_test.js\" --recursive", + "test:lib": "npm run test:browser:cleanup && nyc --silent --no-clean mocha --exclude \"test/browser_test.js\" --exclude \"test/parseFromUrl_test.js\" --recursive", "test:parseFromUrl": "nyc --silent --no-clean start-server-and-test \"http-server test/sample_browser --cors -s\" 8080 \"mocha test/parseFromUrl_test.js\"", "cover:report": "nyc report --reporter=text --reporter=html", "test:browser": "npm run test:browser:cleanup && npm run bundle && shx cp \"dist/bundle.js\" \"test/sample_browser/\" && start-server-and-test \"http-server test/sample_browser --cors -s\" 8080 \"mocha --timeout 20000 test/browser_test.js\" && npm run test:browser:cleanup", @@ -66,7 +66,7 @@ "mkdirp": "^1.0.4", "mocha": "^6.1.4", "nyc": "^15.1.0", - "puppeteer": "^7.0.1", + "puppeteer": "^17.0.0", "rimraf": "^3.0.2", "semantic-release": "19.0.3", "shx": "^0.3.3", diff --git a/test/browser_test.js b/test/browser_test.js index a32acf0af..5cd48f41f 100644 --- a/test/browser_test.js +++ b/test/browser_test.js @@ -28,8 +28,9 @@ describe('Check Parser in the browser', function() { try { console.info('getting fromString element'); const specString = await page.$('#fromString'); + const content = await page.evaluate(element => element.textContent, specString); - expect(content).to.be.equal('2.0.0'); + expect(content).to.be.equal('{"asyncapi":"2.4.0","info":{"title":"Account Service","version":"1.0.0","description":"This service is in charge of processing user signups"},"channels":{"user/signedup":{"subscribe":{"message":{"payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user","x-parser-schema-id":""},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}},"x-parser-schema-id":""}},"x-parser-schema-id":""},"x-parser-original-schema-format":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-original-payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}}}}},"schemaFormat":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-message-parsed":true,"x-parser-message-name":"UserSignedUp"}}}},"components":{"messages":{"UserSignedUp":{"payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user","x-parser-schema-id":""},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}},"x-parser-schema-id":""}},"x-parser-schema-id":""},"x-parser-original-schema-format":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-original-payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}}}}},"schemaFormat":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-message-parsed":true,"x-parser-message-name":"UserSignedUp"}}},"x-parser-spec-parsed":true}'); } catch (e) { throw new Error(e); } @@ -47,7 +48,7 @@ describe('Check Parser in the browser', function() { const specUrl = await page.$('#fromUrl'); const content = await page.evaluate(element => element.textContent, specUrl); - expect(content).to.be.equal('2.0.0'); + expect(content).to.be.equal('{"asyncapi":"2.4.0","info":{"title":"Account Service","version":"1.0.0","description":"This service is in charge of processing user signups"},"channels":{"user/signedup":{"subscribe":{"message":{"payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user","x-parser-schema-id":""},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}},"x-parser-schema-id":""}},"x-parser-schema-id":""},"x-parser-original-schema-format":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-original-payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}}}}},"schemaFormat":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-message-parsed":true,"x-parser-message-name":"UserSignedUp"}}}},"components":{"messages":{"UserSignedUp":{"payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user","x-parser-schema-id":""},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}},"x-parser-schema-id":""}},"x-parser-schema-id":""},"x-parser-original-schema-format":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-original-payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}}}}},"schemaFormat":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-message-parsed":true,"x-parser-message-name":"UserSignedUp"}}},"x-parser-spec-parsed":true}'); } catch (e) { throw new Error(e); } diff --git a/test/parseFromUrl_test.js b/test/parseFromUrl_test.js index aa4a33ca0..b3d285ff6 100644 --- a/test/parseFromUrl_test.js +++ b/test/parseFromUrl_test.js @@ -6,18 +6,18 @@ const { checkErrorWrapper } = require('./testsUtils'); chai.use(chaiAsPromised); const expect = chai.expect; -const validOutputJSON = '{"asyncapi":"2.0.0","info":{"title":"My API","version":"1.0.0"},"channels":{"/test/tester":{"subscribe":{"message":{"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-parsed":true,"x-parser-message-name":""}}}},"x-parser-spec-parsed":true}'; +const validOutputJSON = '{"asyncapi":"2.4.0","info":{"title":"Account Service","version":"1.0.0","description":"This service is in charge of processing user signups"},"channels":{"user/signedup":{"subscribe":{"message":{"payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user","x-parser-schema-id":""},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}},"x-parser-schema-id":""}},"x-parser-schema-id":""},"x-parser-original-schema-format":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-original-payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}}}}},"schemaFormat":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-message-parsed":true,"x-parser-message-name":"UserSignedUp"}}}},"components":{"messages":{"UserSignedUp":{"payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user","x-parser-schema-id":""},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}},"x-parser-schema-id":""}},"x-parser-schema-id":""},"x-parser-original-schema-format":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-original-payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}}}}},"schemaFormat":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-message-parsed":true,"x-parser-message-name":"UserSignedUp"}}},"x-parser-spec-parsed":true}'; describe('parseFromUrl()', function() { it('should parse YAML correctly from URL', async function() { - const result = await parser.parseFromUrl('http://localhost:8080/asyncapi.yaml'); + const result = await parser.parseFromUrl('http://localhost:8080/main/asyncapi.yaml'); expect(JSON.stringify(result.json())).to.equal(validOutputJSON); }); it('should parse 2 AsyncAPI specs in Promise.all() from URL', async function() { const input = [ - parser.parseFromUrl('http://localhost:8080/asyncapi.yaml'), - parser.parseFromUrl('http://localhost:8080/asyncapi.yaml') + parser.parseFromUrl('http://localhost:8080/main/asyncapi.yaml'), + parser.parseFromUrl('http://localhost:8080/main/asyncapi.yaml') ]; const result = await Promise.all(input); expect(JSON.stringify(result[0].json())).to.equal(validOutputJSON); diff --git a/test/refs/refed.yaml b/test/refs/refed.yaml index 1b912b83b..7a154eeba 100644 --- a/test/refs/refed.yaml +++ b/test/refs/refed.yaml @@ -1,4 +1,4 @@ type: object properties: testing: - $ref: 'refed2.yaml' + $ref: './refed2.yaml' diff --git a/test/sample_browser/asyncapi.yaml b/test/sample_browser/asyncapi.yaml deleted file mode 100644 index 95eaabca1..000000000 --- a/test/sample_browser/asyncapi.yaml +++ /dev/null @@ -1,8 +0,0 @@ -asyncapi: 2.0.0 -info: - title: My API - version: 1.0.0 -channels: - "/test/tester": - subscribe: - message: {} \ No newline at end of file diff --git a/test/sample_browser/index.html b/test/sample_browser/index.html index e24c90c1a..5000ba965 100644 --- a/test/sample_browser/index.html +++ b/test/sample_browser/index.html @@ -1,27 +1,58 @@ - -
-
+ + + + + + + AsyncAPI document parsing test + + - \ No newline at end of file + + + +

#fromString

+
+
+
+

#fromUrl

+ + + diff --git a/test/sample_browser/main/asyncapi.yaml b/test/sample_browser/main/asyncapi.yaml new file mode 100644 index 000000000..bf4deb469 --- /dev/null +++ b/test/sample_browser/main/asyncapi.yaml @@ -0,0 +1,25 @@ +asyncapi: '2.4.0' +info: + title: Account Service + version: 1.0.0 + description: This service is in charge of processing user signups +channels: + user/signedup: + subscribe: + message: + $ref: '#/components/messages/UserSignedUp' +components: + messages: + UserSignedUp: + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + type: string + format: email + description: Email of the user + test: + $ref: '../refs/refed.yaml' \ No newline at end of file diff --git a/test/sample_browser/refs/refed.yaml b/test/sample_browser/refs/refed.yaml new file mode 100644 index 000000000..5f6488eff --- /dev/null +++ b/test/sample_browser/refs/refed.yaml @@ -0,0 +1,6 @@ +type: object +properties: + testing1: + $ref: "./refed2.yaml" + testing2: + $ref: "http://localhost:8080/refs/refed2.yaml" diff --git a/test/sample_browser/refs/refed2.yaml b/test/sample_browser/refs/refed2.yaml new file mode 100644 index 000000000..5c21d88b9 --- /dev/null +++ b/test/sample_browser/refs/refed2.yaml @@ -0,0 +1 @@ +type: string diff --git a/test/utils_test.js b/test/utils_test.js new file mode 100644 index 000000000..352631447 --- /dev/null +++ b/test/utils_test.js @@ -0,0 +1,14 @@ +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const { getBaseUrl } = require('../lib/utils'); + +chai.use(chaiAsPromised); +const expect = chai.expect; + +describe('getBaseUrl()', function () { + it('should accept totally valid absolute URL of an AsyncAPI document', async function () { + await expect( + getBaseUrl('https://asyncapi.com/good/asyncapi.yaml') + ).to.equal('https://asyncapi.com/good/'); + }); +});