diff --git a/packages/api/src/cache.ts b/packages/api/src/cache.ts index 4ae4c0a3..3ae170f2 100644 --- a/packages/api/src/cache.ts +++ b/packages/api/src/cache.ts @@ -1,7 +1,7 @@ -import type { OASDocument } from './types'; +import type { OASDocument } from 'oas/@types/rmoas.types'; + import 'isomorphic-fetch'; import OpenAPIParser from '@readme/openapi-parser'; -import yaml from 'js-yaml'; import crypto from 'crypto'; import findCacheDir from 'find-cache-dir'; import fs from 'fs'; @@ -11,6 +11,8 @@ import makeDir from 'make-dir'; import { PACKAGE_NAME } from './packageInfo'; +import Fetcher from './fetcher'; + type CacheStore = Record; export default class Cache { @@ -26,21 +28,16 @@ export default class Cache { cached: false | CacheStore; + fetcher: Fetcher; + constructor(uri: string | OASDocument) { Cache.setCacheDir(); Cache.cacheStore = path.join(Cache.dir, 'cache.json'); Cache.specsCache = path.join(Cache.dir, 'specs'); - // Resolve OpenAPI definition shorthand accessors from within the ReadMe API Registry. - // - // Examples: - // - @petstore/v1.0#n6kvf10vakpemvplx - // - @petstore#n6kvf10vakpemvplx - this.uri = - typeof uri === 'string' - ? uri.replace(/^@[a-zA-Z0-9-_]+\/?(.+)#([a-z0-9]+)$/, 'https://dash.readme.com/api/v1/api-registry/$2') - : uri; + this.fetcher = new Fetcher(uri); + this.uri = this.fetcher.uri; this.uriHash = Cache.getCacheHash(this.uri); // This should default to false so we have awareness if we've looked at the cache yet. @@ -88,6 +85,28 @@ export default class Cache { await fs.promises.rmdir(Cache.specsCache, { recursive: true }); } + static validate(json: any) { + if (json.swagger) { + throw new Error('Sorry, this module only supports OpenAPI definitions.'); + } + + // The `validate` method handles dereferencing for us. + return OpenAPIParser.validate(json, { + dereference: { + // If circular `$refs` are ignored they'll remain in the API definition as + // `$ref: String`. This allows us to not only do easy circular reference detection but + // also stringify and save dereferenced API definitions back into the cache directory. + circular: 'ignore', + }, + }).catch(err => { + if (/is not a valid openapi definition/i.test(err.message)) { + throw new Error("Sorry, that doesn't look like a valid OpenAPI definition."); + } + + throw err; + }); + } + isCached() { const cache = this.getCache(); return cache && this.uriHash in cache; @@ -129,109 +148,36 @@ export default class Cache { return this.uri; } - try { - const url = new URL(this.uri); - this.uri = url.href; + return this.fetcher.load().then(async spec => this.save(spec)); + } - return this.saveUrl(); - } catch (err) { - return this.saveFile(); + save(spec: OASDocument) { + if (!fs.existsSync(Cache.dir)) { + fs.mkdirSync(Cache.dir, { recursive: true }); } - } - save(json: Record) { - if (json.swagger) { - throw new Error('Sorry, this module only supports OpenAPI definitions.'); + if (!fs.existsSync(Cache.specsCache)) { + fs.mkdirSync(Cache.specsCache, { recursive: true }); } - return Promise.resolve(json) - .then((res: any) => { - // The `validate` method handles dereferencing for us. - return OpenAPIParser.validate(res, { - dereference: { - // If circular `$refs` are ignored they'll remain in the API definition as - // `$ref: String`. This allows us to not only do easy circular reference detection but - // also stringify and save dereferenced API definitions back into the cache directory. - circular: 'ignore', - }, - }).catch(err => { - if (/is not a valid openapi definition/i.test(err.message)) { - throw new Error("Sorry, that doesn't look like a valid OpenAPI definition."); - } - - throw err; - }); - }) - .then(async spec => { - if (!fs.existsSync(Cache.dir)) { - fs.mkdirSync(Cache.dir, { recursive: true }); - } - - if (!fs.existsSync(Cache.specsCache)) { - fs.mkdirSync(Cache.specsCache, { recursive: true }); - } - - const cache = this.getCache(); - if (!(this.uriHash in cache)) { - const saved = JSON.stringify(spec, null, 2); - const jsonHash = crypto.createHash('md5').update(saved).digest('hex'); - - cache[this.uriHash] = { - path: path.join(Cache.specsCache, `${jsonHash}.json`), - original: this.uri, - title: 'title' in spec.info ? spec.info.title : undefined, - version: 'version' in spec.info ? spec.info.version : undefined, - }; - - fs.writeFileSync(cache[this.uriHash].path, saved); - fs.writeFileSync(Cache.cacheStore, JSON.stringify(cache, null, 2)); - - this.cached = cache; - } - - return spec; - }); - } + const cache = this.getCache(); + if (!(this.uriHash in cache)) { + const saved = JSON.stringify(spec, null, 2); + const jsonHash = crypto.createHash('md5').update(saved).digest('hex'); - saveUrl() { - const url = this.uri as string; - return fetch(url) - .then(res => { - if (!res.ok) { - throw new Error(`Unable to retrieve URL (${url}). Reason: ${res.statusText}`); - } - - if (res.headers.get('content-type') === 'application/yaml' || /\.(yaml|yml)/.test(url)) { - return res.text().then(text => { - return yaml.load(text); - }); - } - - return res.json(); - }) - .then((json: Record) => this.save(json)); - } + cache[this.uriHash] = { + path: path.join(Cache.specsCache, `${jsonHash}.json`), + original: this.uri, + title: 'title' in spec.info ? spec.info.title : undefined, + version: 'version' in spec.info ? spec.info.version : undefined, + }; - saveFile() { - // Support relative paths by resolving them against the cwd. - this.uri = path.resolve(process.cwd(), this.uri as string); + fs.writeFileSync(cache[this.uriHash].path, saved); + fs.writeFileSync(Cache.cacheStore, JSON.stringify(cache, null, 2)); - if (!fs.existsSync(this.uri)) { - throw new Error( - `Sorry, we were unable to load ${this.uri} OpenAPI definition. Please either supply a URL or a path on your filesystem.` - ); + this.cached = cache; } - const filePath = this.uri; - - return Promise.resolve(fs.readFileSync(filePath, 'utf8')) - .then((res: string) => { - if (/\.(yaml|yml)/.test(filePath)) { - return yaml.load(res); - } - - return JSON.parse(res); - }) - .then(json => this.save(json)); + return spec; } } diff --git a/packages/api/src/fetcher.ts b/packages/api/src/fetcher.ts new file mode 100644 index 00000000..c3428cbf --- /dev/null +++ b/packages/api/src/fetcher.ts @@ -0,0 +1,105 @@ +import type { OASDocument } from 'oas/@types/rmoas.types'; + +import 'isomorphic-fetch'; +import OpenAPIParser from '@readme/openapi-parser'; +import yaml from 'js-yaml'; +import fs from 'fs'; +import path from 'path'; + +export default class Fetcher { + uri: string | OASDocument; + + constructor(uri: string | OASDocument) { + /** + * Resolve OpenAPI definition shorthand accessors from within the ReadMe API Registry. + * + * @example @petstore/v1.0#n6kvf10vakpemvplx + * @example @petstore#n6kvf10vakpemvplx + */ + this.uri = + typeof uri === 'string' + ? uri.replace(/^@[a-zA-Z0-9-_]+\/?(.+)#([a-z0-9]+)$/, 'https://dash.readme.com/api/v1/api-registry/$2') + : uri; + } + + async load() { + if (typeof this.uri !== 'string') { + throw new TypeError( + "Something disastrous happened and a non-string URI was supplied to the Fetcher library. This shouldn't have happened!" + ); + } + + return Promise.resolve(this.uri) + .then(uri => { + try { + const url = new URL(uri); + + return Fetcher.getURL(url.href); + } catch (err) { + return Fetcher.getFile(uri); + } + }) + .then(res => Fetcher.validate(res)) + .then(res => res as OASDocument); + } + + static getURL(url: string) { + // @todo maybe include our user-agent here to identify our request + return fetch(url).then(res => { + if (!res.ok) { + throw new Error(`Unable to retrieve URL (${url}). Reason: ${res.statusText}`); + } + + if (res.headers.get('content-type') === 'application/yaml' || /\.(yaml|yml)/.test(url)) { + return res.text().then(text => { + return yaml.load(text); + }); + } + + return res.json(); + }); + } + + static getFile(uri: string) { + // Support relative paths by resolving them against the cwd. + const file = path.resolve(process.cwd(), uri); + + if (!fs.existsSync(file)) { + throw new Error( + `Sorry, we were unable to load an API definition from ${file}. Please either supply a URL or a path on your filesystem.` + ); + } + + return Promise.resolve(fs.readFileSync(file, 'utf8')).then((res: string) => { + if (/\.(yaml|yml)/.test(file)) { + return yaml.load(res); + } + + return JSON.parse(res); + }); + } + + static validate(json: any) { + if (json.swagger) { + throw new Error('Sorry, this module only supports OpenAPI definitions.'); + } + + // The `validate` method handles dereferencing for us. + return OpenAPIParser.validate(json, { + dereference: { + /** + * If circular `$refs` are ignored they'll remain in the API definition as `$ref: String`. + * This allows us to not only do easy circular reference detection but also stringify and + * save dereferenced API definitions back into the cache directory. + */ + circular: 'ignore', + }, + }).catch(err => { + if (/is not a valid openapi definition/i.test(err.message)) { + throw new Error("Sorry, that doesn't look like a valid OpenAPI definition."); + } + + throw err; + }); + } +} diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 06b1f1d9..77390425 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,6 +1,5 @@ import type { Operation } from 'oas'; -import type { OASDocument } from './types'; -import type { HttpMethods } from 'oas/@types/rmoas.types'; +import type { HttpMethods, OASDocument } from 'oas/@types/rmoas.types'; import type { ConfigOptions } from './core'; import Oas from 'oas'; diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts deleted file mode 100644 index e6b83745..00000000 --- a/packages/api/src/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -// We should eventually merge this in with the `OASDocument` type that `oas` provides, or use -// the `openapi-types` library, but both of those currently have a lot of issues with seemingly -// valid specs busting types because of problems in `openapi-types`. -// -// For example with the `@readme/oas-examples/3.0/json/security.json` definition it thinks that -// the `apiKey_cookie` security scheme isn't a valid apiKey security scheme despite it absolutely -// being valid. Or with `@readme/oas-examples/3.0/json/petstore.json` it thinks that the `Order` -// component isn't valid at all. -export type OASDocument = Record; diff --git a/packages/api/test/auth.test.ts b/packages/api/test/auth.test.ts index 71ba9bec..55cd6a6d 100644 --- a/packages/api/test/auth.test.ts +++ b/packages/api/test/auth.test.ts @@ -1,4 +1,6 @@ /* eslint-disable mocha/no-setup-in-describe */ +import type { OASDocument } from 'oas/@types/rmoas.types'; + import { assert, expect } from 'chai'; import nock from 'nock'; import uniqueTempDir from 'unique-temp-dir'; @@ -22,7 +24,7 @@ describe('#auth()', function () { }); beforeEach(function () { - sdk = api(securityOas); + sdk = api(securityOas as unknown as OASDocument); }); describe('API Keys', function () { diff --git a/packages/api/test/cache.test.ts b/packages/api/test/cache.test.ts index a75a1f34..f3d8279f 100644 --- a/packages/api/test/cache.test.ts +++ b/packages/api/test/cache.test.ts @@ -1,3 +1,5 @@ +import type { OASDocument } from 'oas/@types/rmoas.types'; + import chai, { assert, expect } from 'chai'; import uniqueTempDir from 'unique-temp-dir'; import nock from 'nock'; @@ -19,28 +21,10 @@ describe('cache', function () { describe('#load', function () { it('should return a raw object if a JSON object was initially supplied', async function () { - const res = await new Cache(readmeSpec).load(); + const res = await new Cache(readmeSpec as unknown as OASDocument).load(); expect(res).to.deep.equal(readmeSpec); }); - describe('shorthand accessors', function () { - it('should resolve the shorthand `@petstore/v1.0#uuid` syntax to the ReadMe API', function () { - expect(new Cache('@petstore/v1.0#n6kvf10vakpemvplx').uri).to.equal( - 'https://dash.readme.com/api/v1/api-registry/n6kvf10vakpemvplx' - ); - }); - - it('should resolve the shorthand `@petstore#uuid` syntax to the ReadMe API', function () { - expect(new Cache('@petstore#n6kvf10vakpemvplx').uri).to.equal( - 'https://dash.readme.com/api/v1/api-registry/n6kvf10vakpemvplx' - ); - }); - - it("shouldn't try to resolve improperly formatted shorthand accessors to the ReadMe API", function () { - expect(new Cache('n6kvf10vakpemvplx').uri).to.equal('n6kvf10vakpemvplx'); - }); - }); - it('should throw an error when a non-HTTP(S) url is supplied', async function () { await new Cache('htt://example.com/openapi.json') .load() @@ -58,106 +42,149 @@ describe('cache', function () { expect(err.message).to.match(/supply a URL or a path on your filesystem/); }); }); - }); - describe('#saveUrl()', function () { - it('should be able to save a definition', async function () { - const mock = nock('http://example.com').get('/readme.json').reply(200, readmeSpec); - const cacheStore = new Cache('http://example.com/readme.json'); + describe('ReadMe registry UUID', function () { + it('should resolve the shorthand `@petstore/v1.0#uuid` syntax to the ReadMe API', function () { + expect(new Cache('@petstore/v1.0#n6kvf10vakpemvplx').uri).to.equal( + 'https://dash.readme.com/api/v1/api-registry/n6kvf10vakpemvplx' + ); + }); - expect(cacheStore.isCached()).to.be.false; + it('should resolve the shorthand `@petstore#uuid` syntax to the ReadMe API', function () { + expect(new Cache('@petstore#n6kvf10vakpemvplx').uri).to.equal( + 'https://dash.readme.com/api/v1/api-registry/n6kvf10vakpemvplx' + ); + }); - expect(await cacheStore.saveUrl()).to.have.deep.property('info', { - description: 'Create beautiful product and API documentation with our developer friendly platform.', - version: '2.0.0', - title: 'API Endpoints', - contact: { - email: 'support@readme.io', - name: 'API Support', - url: 'https://docs.readme.com/docs/contact-support', - }, + it("shouldn't try to resolve improperly formatted shorthand accessors to the ReadMe API", function () { + expect(new Cache('n6kvf10vakpemvplx').uri).to.equal('n6kvf10vakpemvplx'); }); - expect(cacheStore.get().paths['/api-specification'].get.parameters).to.be.dereferenced; - expect(cacheStore.isCached()).to.be.true; - mock.done(); - }); + it('should be able to load a definition', async function () { + const mock = nock('https://dash.readme.com') + .get('/api/v1/api-registry/n6kvf10vakpemvplxn') + .reply(200, readmeSpec); - it('should error if the url cannot be reached', async function () { - const mock = nock('http://example.com').get('/unknown.json').reply(404); + const cacheStore = new Cache('@readme/v1.0#n6kvf10vakpemvplxn'); - await new Cache('http://example.com/unknown.json') - .saveUrl() - .then(() => assert.fail()) - .catch(err => { - expect(err.message).to.equal('Unable to retrieve URL (http://example.com/unknown.json). Reason: Not Found'); + expect(cacheStore.isCached()).to.be.false; + + expect(await cacheStore.load()).to.have.deep.property('info', { + description: 'Create beautiful product and API documentation with our developer friendly platform.', + version: '2.0.0', + title: 'API Endpoints', + contact: { + email: 'support@readme.io', + name: 'API Support', + url: 'https://docs.readme.com/docs/contact-support', + }, }); - mock.done(); + expect(cacheStore.get().paths['/api-specification'].get.parameters).to.be.dereferenced; + expect(cacheStore.isCached()).to.be.true; + mock.done(); + }); }); - it('should convert yaml to json', async function () { - const spec = await fs.readFile(require.resolve('@readme/oas-examples/3.0/yaml/readme.yaml'), 'utf8'); - const mock = nock('http://example.com').get('/readme.yaml').reply(200, spec); + describe('URL', function () { + it('should be able to load a definition', async function () { + const mock = nock('http://example.com').get('/readme.json').reply(200, readmeSpec); + const cacheStore = new Cache('http://example.com/readme.json'); + + expect(cacheStore.isCached()).to.be.false; + + expect(await cacheStore.load()).to.have.deep.property('info', { + description: 'Create beautiful product and API documentation with our developer friendly platform.', + version: '2.0.0', + title: 'API Endpoints', + contact: { + email: 'support@readme.io', + name: 'API Support', + url: 'https://docs.readme.com/docs/contact-support', + }, + }); + + expect(cacheStore.get().paths['/api-specification'].get.parameters).to.be.dereferenced; + expect(cacheStore.isCached()).to.be.true; + mock.done(); + }); - const definition = 'http://example.com/readme.yaml'; - const cacheStore = new Cache(definition); - const hash = Cache.getCacheHash(definition); + it('should error if the url cannot be reached', async function () { + const mock = nock('http://example.com').get('/unknown.json').reply(404); - expect(cacheStore.isCached()).to.be.false; + await new Cache('http://example.com/unknown.json') + .load() + .then(() => assert.fail()) + .catch(err => { + expect(err.message).to.equal('Unable to retrieve URL (http://example.com/unknown.json). Reason: Not Found'); + }); - await cacheStore.saveUrl(); - expect(cacheStore.get().paths['/api-specification'].get.parameters).to.be.dereferenced; - expect(cacheStore.isCached()).to.be.true; - mock.done(); + mock.done(); + }); - const cached = cacheStore.getCache(); - expect(cached).to.have.property(hash); - expect(cached[hash].path).to.match(/\.json$/); - }); - }); + it('should convert yaml to json', async function () { + const spec = await fs.readFile(require.resolve('@readme/oas-examples/3.0/yaml/readme.yaml'), 'utf8'); + const mock = nock('http://example.com').get('/readme.yaml').reply(200, spec); - describe('#saveFile()', function () { - it('should be able to save a definition', async function () { - const cacheStore = new Cache(require.resolve('@readme/oas-examples/3.0/json/readme.json')); + const definition = 'http://example.com/readme.yaml'; + const cacheStore = new Cache(definition); + const hash = Cache.getCacheHash(definition); - expect(cacheStore.isCached()).to.be.false; + expect(cacheStore.isCached()).to.be.false; - await cacheStore.saveFile(); - expect(cacheStore.get().paths['/api-specification'].get.parameters).to.be.dereferenced; - expect(cacheStore.isCached()).to.be.true; + await cacheStore.load(); + expect(cacheStore.get().paths['/api-specification'].get.parameters).to.be.dereferenced; + expect(cacheStore.isCached()).to.be.true; + mock.done(); + + const cached = cacheStore.getCache(); + expect(cached).to.have.property(hash); + expect(cached[hash].path).to.match(/\.json$/); + }); }); - it('should be able handle a relative path', async function () { - const cacheStore = new Cache('../api/test/__fixtures__/oas.json'); + describe('file', function () { + it('should be able to load a definition', async function () { + const cacheStore = new Cache(require.resolve('@readme/oas-examples/3.0/json/readme.json')); - expect(cacheStore.isCached()).to.be.false; + expect(cacheStore.isCached()).to.be.false; - await cacheStore.saveFile(); - expect(cacheStore.isCached()).to.be.true; - }); + await cacheStore.load(); + expect(cacheStore.get().paths['/api-specification'].get.parameters).to.be.dereferenced; + expect(cacheStore.isCached()).to.be.true; + }); - it('should convert yaml to json', async function () { - const file = require.resolve('@readme/oas-examples/3.0/yaml/readme.yaml'); - const cacheStore = new Cache(file); - const hash = Cache.getCacheHash(file); + it('should be able to handle a relative path', async function () { + const cacheStore = new Cache('../api/test/__fixtures__/oas.json'); - expect(cacheStore.isCached()).to.be.false; + expect(cacheStore.isCached()).to.be.false; - await cacheStore.saveFile(); - expect(cacheStore.get().paths['/api-specification'].get.parameters).to.be.dereferenced; - expect(cacheStore.isCached()).to.be.true; + await cacheStore.load(); + expect(cacheStore.isCached()).to.be.true; + }); + + it('should convert yaml to json', async function () { + const file = require.resolve('@readme/oas-examples/3.0/yaml/readme.yaml'); + const cacheStore = new Cache(file); + const hash = Cache.getCacheHash(file); + + expect(cacheStore.isCached()).to.be.false; - const cached = cacheStore.getCache(); - expect(cached).to.have.property(hash); - expect(cached[hash].path).to.match(/\.json$/); + await cacheStore.load(); + expect(cacheStore.get().paths['/api-specification'].get.parameters).to.be.dereferenced; + expect(cacheStore.isCached()).to.be.true; + + const cached = cacheStore.getCache(); + expect(cached).to.have.property(hash); + expect(cached[hash].path).to.match(/\.json$/); + }); }); }); describe('#save()', function () { it('should error if definition is a swagger file', async function () { await new Cache(require.resolve('@readme/oas-examples/2.0/json/petstore.json')) - .saveFile() + .load() .then(() => assert.fail()) .catch(err => { expect(err.message).to.equal('Sorry, this module only supports OpenAPI definitions.'); @@ -166,7 +193,7 @@ describe('cache', function () { it('should error if definition is not a valid openapi file', async function () { await new Cache(require.resolve('../package.json')) - .saveFile() + .load() .then(() => assert.fail()) .catch(err => { expect(err.message).to.equal("Sorry, that doesn't look like a valid OpenAPI definition."); @@ -181,7 +208,7 @@ describe('cache', function () { expect(cacheStore.isCached()).to.be.false; - await cacheStore.saveFile(); + await cacheStore.load(); expect(cacheStore.isCached()).to.be.true; }); @@ -192,7 +219,7 @@ describe('cache', function () { expect(cacheStore.isCached()).to.be.false; - await cacheStore.saveFile(); + await cacheStore.load(); expect(cacheStore.isCached()).to.be.true; }); @@ -203,7 +230,7 @@ describe('cache', function () { expect(cacheStore.isCached()).to.be.false; - await cacheStore.saveFile(); + await cacheStore.load(); expect(cacheStore.isCached()).to.be.true; }); @@ -211,8 +238,7 @@ describe('cache', function () { describe('#get', function () { it('should return an object if the current uri is an object (used for unit testing)', function () { - // const obj = JSON.parse(readmeExampleJson); - const loaded = new Cache(readmeSpec).get(); + const loaded = new Cache(readmeSpec as unknown as OASDocument).get(); expect(loaded).to.deep.equal(readmeSpec); }); @@ -220,7 +246,7 @@ describe('cache', function () { it('should load a file out of cache', async function () { const file = require.resolve('@readme/oas-examples/3.0/json/readme.json'); const cacheStore = new Cache(file); - await cacheStore.saveFile(); + await cacheStore.load(); const loaded = cacheStore.get(); expect(loaded).to.have.property('components'); diff --git a/packages/api/test/config.test.ts b/packages/api/test/config.test.ts index 984b2b10..1015b109 100644 --- a/packages/api/test/config.test.ts +++ b/packages/api/test/config.test.ts @@ -1,3 +1,5 @@ +import type { OASDocument } from 'oas/@types/rmoas.types'; + import { expect } from 'chai'; import nock from 'nock'; import uniqueTempDir from 'unique-temp-dir'; @@ -24,7 +26,7 @@ describe('#config()', function () { describe('parseResponse', function () { beforeEach(function () { - sdk = api(petstore); + sdk = api(petstore as unknown as OASDocument); }); it('should give access to the Response object if `parseResponse` is `false`', async function () { diff --git a/packages/api/test/core/prepareAuth.test.ts b/packages/api/test/core/prepareAuth.test.ts index 770a86cb..0f7141ba 100644 --- a/packages/api/test/core/prepareAuth.test.ts +++ b/packages/api/test/core/prepareAuth.test.ts @@ -1,10 +1,12 @@ +import type { OASDocument } from 'oas/@types/rmoas.types'; + import { expect } from 'chai'; import Oas from 'oas'; import prepareAuth from '../../src/core/prepareAuth'; import securityOas from '@readme/oas-examples/3.0/json/security.json'; -const oas = Oas.init(securityOas); +const oas = Oas.init(securityOas as unknown as OASDocument); describe('#prepareAuth()', function () { describe('apiKey', function () { diff --git a/packages/api/test/dist.test.ts b/packages/api/test/dist.test.ts index b076998a..8b870c02 100644 --- a/packages/api/test/dist.test.ts +++ b/packages/api/test/dist.test.ts @@ -1,3 +1,5 @@ +import type { OASDocument } from 'oas/@types/rmoas.types'; + import { expect } from 'chai'; import nock from 'nock'; import uniqueTempDir from 'unique-temp-dir'; @@ -21,7 +23,7 @@ describe('typescript dist verification', function () { .post('/oa_citations/v1/records') .reply(200, uri => uri); - const sdk = api(uspto); + const sdk = api(uspto as unknown as OASDocument); expect(await sdk.post('/oa_citations/v1/records')).to.equal('/ds-api/oa_citations/v1/records'); @@ -39,7 +41,7 @@ describe('typescript dist verification', function () { return this.req.headers; }); - const sdk = api(securityOas); + const sdk = api(securityOas as unknown as OASDocument); sdk.auth(user, pass); expect(await sdk.post('/anything/basic')).to.have.deep.property('authorization', [authHeader]); diff --git a/packages/api/test/fetcher.test.ts b/packages/api/test/fetcher.test.ts new file mode 100644 index 00000000..8e0276b7 --- /dev/null +++ b/packages/api/test/fetcher.test.ts @@ -0,0 +1,151 @@ +import chai, { assert, expect } from 'chai'; +import nock from 'nock'; +import chaiPlugins from './helpers/chai-plugins'; +import fs from 'fs/promises'; +import Fetcher from '../src/fetcher'; + +import readmeSpec from '@readme/oas-examples/3.0/json/readme.json'; + +chai.use(chaiPlugins); + +describe('fetcher', function () { + describe('#load', function () { + it('should throw an error when a non-HTTP(S) url is supplied', async function () { + await new Fetcher('htt://example.com/openapi.json') + .load() + .then(() => assert.fail()) + .catch(err => { + expect(err.message).to.equal('Only HTTP(S) protocols are supported'); + }); + }); + + it('should throw an error if neither a url or file are detected', async function () { + await new Fetcher('/this/is/not/a/real/path.json') + .load() + .then(() => assert.fail()) + .catch(err => { + expect(err.message).to.match(/supply a URL or a path on your filesystem/); + }); + }); + + describe('ReadMe registry UUID', function () { + it('should resolve the shorthand `@petstore/v1.0#uuid` syntax to the ReadMe API', function () { + expect(new Fetcher('@petstore/v1.0#n6kvf10vakpemvplx').uri).to.equal( + 'https://dash.readme.com/api/v1/api-registry/n6kvf10vakpemvplx' + ); + }); + + it('should resolve the shorthand `@petstore#uuid` syntax to the ReadMe API', function () { + expect(new Fetcher('@petstore#n6kvf10vakpemvplx').uri).to.equal( + 'https://dash.readme.com/api/v1/api-registry/n6kvf10vakpemvplx' + ); + }); + + it("shouldn't try to resolve improperly formatted shorthand accessors to the ReadMe API", function () { + expect(new Fetcher('n6kvf10vakpemvplx').uri).to.equal('n6kvf10vakpemvplx'); + }); + + it('should be able to load a definition', async function () { + const mock = nock('https://dash.readme.com') + .get('/api/v1/api-registry/n6kvf10vakpemvplxn') + .reply(200, readmeSpec); + + const fetcher = new Fetcher('@readme/v1.0#n6kvf10vakpemvplxn'); + + expect(await fetcher.load()).to.have.deep.property('info', { + description: 'Create beautiful product and API documentation with our developer friendly platform.', + version: '2.0.0', + title: 'API Endpoints', + contact: { + email: 'support@readme.io', + name: 'API Support', + url: 'https://docs.readme.com/docs/contact-support', + }, + }); + + mock.done(); + }); + }); + + describe('URL', function () { + it('should be able to load a definition', async function () { + const mock = nock('http://example.com').get('/readme.json').reply(200, readmeSpec); + const fetcher = new Fetcher('http://example.com/readme.json'); + + expect(await fetcher.load()).to.have.deep.property('info', { + description: 'Create beautiful product and API documentation with our developer friendly platform.', + version: '2.0.0', + title: 'API Endpoints', + contact: { + email: 'support@readme.io', + name: 'API Support', + url: 'https://docs.readme.com/docs/contact-support', + }, + }); + + mock.done(); + }); + + it('should error if the url cannot be reached', async function () { + const mock = nock('http://example.com').get('/unknown.json').reply(404); + + await new Fetcher('http://example.com/unknown.json') + .load() + .then(() => assert.fail()) + .catch(err => { + expect(err.message).to.equal('Unable to retrieve URL (http://example.com/unknown.json). Reason: Not Found'); + }); + + mock.done(); + }); + + it('should convert yaml to json', async function () { + const spec = await fs.readFile(require.resolve('@readme/oas-examples/3.0/yaml/readme.yaml'), 'utf8'); + const mock = nock('http://example.com').get('/readme.yaml').reply(200, spec); + + const definition = 'http://example.com/readme.yaml'; + const fetcher = new Fetcher(definition); + + expect(await fetcher.load()).to.have.deep.property('info', { + description: 'Create beautiful product and API documentation with our developer friendly platform.', + version: '2.0.0', + title: 'API Endpoints', + contact: { + email: 'support@readme.io', + name: 'API Support', + url: 'https://docs.readme.com/docs/contact-support', + }, + }); + + mock.done(); + }); + }); + + describe('file', function () { + it('should be able to load a definition', async function () { + const fetcher = new Fetcher(require.resolve('@readme/oas-examples/3.0/json/readme.json')); + + const res = await fetcher.load(); + expect(res.paths['/api-specification'].get.parameters).to.be.dereferenced; + }); + + it('should be able to handle a relative path', async function () { + const fetcher = new Fetcher('../api/test/__fixtures__/oas.json'); + + expect(await fetcher.load()).to.have.deep.property('info', { + version: '1.0.0', + title: 'Single Path', + description: 'This is a slimmed down single path version of the Petstore definition.', + }); + }); + + it('should convert yaml to json', async function () { + const file = require.resolve('@readme/oas-examples/3.0/yaml/readme.yaml'); + const fetcher = new Fetcher(file); + + const res = await fetcher.load(); + expect(res.paths['/api-specification'].get.parameters).to.be.dereferenced; + }); + }); + }); +}); diff --git a/packages/api/test/index.test.ts b/packages/api/test/index.test.ts index 1928502d..687c44b7 100644 --- a/packages/api/test/index.test.ts +++ b/packages/api/test/index.test.ts @@ -1,3 +1,5 @@ +import type { OASDocument } from 'oas/@types/rmoas.types'; + import { assert, expect } from 'chai'; import uniqueTempDir from 'unique-temp-dir'; import nock from 'nock'; @@ -20,11 +22,11 @@ describe('api', function () { beforeEach(async function () { const petstore = require.resolve('@readme/oas-examples/3.0/json/petstore-expanded.json'); - await new Cache(petstore).saveFile(); + await new Cache(petstore).load(); petstoreSdk = api(petstore); const readme = require.resolve('@readme/oas-examples/3.0/json/readme.json'); - await new Cache(readme).saveFile(); + await new Cache(readme).load(); readmeSdk = api(readme); }); @@ -305,7 +307,7 @@ describe('api', function () { }, }, }, - }); + } as unknown as OASDocument); }); it('should encode query parameters', async function () { diff --git a/packages/api/test/integration.test.ts b/packages/api/test/integration.test.ts index 0c5698d3..86432e4a 100644 --- a/packages/api/test/integration.test.ts +++ b/packages/api/test/integration.test.ts @@ -1,3 +1,5 @@ +import type { OASDocument } from 'oas/@types/rmoas.types'; + import chai, { expect } from 'chai'; import nock from 'nock'; import uniqueTempDir from 'unique-temp-dir'; @@ -105,7 +107,7 @@ describe('integration tests', function () { const file = `${__dirname}/__fixtures__/owlbert.png`; - const res = await api(fileUploads).post('/anything/image-png', file); + const res = await api(fileUploads as unknown as OASDocument).post('/anything/image-png', file); expect(res.uri).to.equal('/anything/image-png'); expect(res.requestBody).to.equal(await datauri(file)); expect(res.headers).to.have.deep.property('content-type', ['image/png']); @@ -137,7 +139,7 @@ describe('integration tests', function () { }, }; - const res = await api(parametersStyle).post('/anything/form-data/form', body); + const res = await api(parametersStyle as unknown as OASDocument).post('/anything/form-data/form', body); expect(res.uri).to.equal('/anything/form-data/form'); expect(res.requestBody.split(`--${res.boundary}`).filter(Boolean)).to.deep.equal([ '\r\nContent-Disposition: form-data; name="primitive"\r\n\r\nstring\r\n', @@ -172,7 +174,7 @@ describe('integration tests', function () { documentFile: `${__dirname}/__fixtures__/hello.txt`, }; - const res = await api(fileUploads).post('/anything/multipart-formdata', body); + const res = await api(fileUploads as unknown as OASDocument).post('/anything/multipart-formdata', body); expect(res.uri).to.equal('/anything/multipart-formdata'); expect(res.requestBody.split(`--${res.boundary}`).filter(Boolean)).to.deep.equal([ '\r\nContent-Disposition: form-data; name="orderId"\r\n\r\n1234\r\n', @@ -204,7 +206,7 @@ describe('integration tests', function () { documentFile: `${__dirname}/__fixtures__/hello.jp.txt`, }; - const res = await api(fileUploads).post('/anything/multipart-formdata', body); + const res = await api(fileUploads as unknown as OASDocument).post('/anything/multipart-formdata', body); expect(res.uri).to.equal('/anything/multipart-formdata'); expect(res.requestBody.split(`--${res.boundary}`).filter(Boolean)).to.deep.equal([ '\r\nContent-Disposition: form-data; name="documentFile"; filename="hello.jp.txt"\r\nContent-Type: text/plain\r\n\r\n速い茶色のキツネは怠惰な犬を飛び越えます\n\r\n',