From 9505b4a10abe3038e27342c58aa076a0b9392ada Mon Sep 17 00:00:00 2001 From: Trygve Lie <173003+trygve-lie@users.noreply.github.com> Date: Mon, 9 Dec 2019 22:38:07 +0100 Subject: [PATCH] Add version overview (#66) * Added tests for Alias class * Added tests for HttpIncoming class * Add tests for HttpOutgoing class * Added tests for Meta class * Added version overview --- doc/file-structure.md | 6 +- doc/rest-api.md | 79 +++++++++++++++++++++++- lib/classes/http-outgoing.js | 17 +++++- lib/classes/meta.js | 2 +- lib/classes/versions.js | 64 +++++++++++++++++++ lib/handlers/map.put.js | 72 ++++++++++++++++------ lib/handlers/pkg.put.js | 69 +++++++++++++-------- lib/handlers/versions.get.js | 56 +++++++++++++++++ lib/main.js | 2 + lib/utils/path-builders-fs.js | 8 +++ package.json | 4 +- services/fastify.js | 41 ++++++++++++- test/classes/alias.js | 63 +++++++++++++++++-- test/classes/http-incoming.js | 45 ++++++++++++-- test/classes/http-outgoing.js | 89 +++++++++++++++++++++++++-- test/classes/meta.js | 34 +++++++++++ test/classes/read-file.js | 2 +- test/classes/versions.js | 112 ++++++++++++++++++++++++++++++++++ 18 files changed, 696 insertions(+), 69 deletions(-) create mode 100644 lib/classes/versions.js create mode 100644 lib/handlers/versions.get.js create mode 100644 test/classes/meta.js create mode 100644 test/classes/versions.js diff --git a/doc/file-structure.md b/doc/file-structure.md index ea05902d..cd98a708 100644 --- a/doc/file-structure.md +++ b/doc/file-structure.md @@ -8,13 +8,15 @@ The asset service stores files in the following structure: ├── map │   └── :name │   ├── :version.import-map.json - │   └── :major.alias.json + │   ├── :major.alias.json + │ └── versions.json └── pkg └── :name ├── :version │   ├── * ├── :version.package.json - └── :major.alias.json + ├── :major.alias.json + └── versions.json ``` Parameters: diff --git a/doc/rest-api.md b/doc/rest-api.md index e2e43c6b..579cc772 100644 --- a/doc/rest-api.md +++ b/doc/rest-api.md @@ -66,7 +66,7 @@ Status codes: - `303` if module is successfully uploaded. `location` is root of module - `400` if validation in URL parameters or form fields fails - `401` if user is not authorized -- `409` if module already exist +- `409` if module already exist or version in a major range is not newer than previous version in a major range - `415` if file format of the uploaded file is unsupported - `502` if package could not be written to the sink @@ -76,6 +76,58 @@ Example: curl -X PUT -i -F filedata=@archive.tgz http://localhost:4001/finn/pkg/fuzz/8.4.1 ``` +### Latest Package versions + +**Method:** `GET` + +Retrieves an overview of the latest major versions of a package. + +```bash +https://:assetServerUrl:port/:org/pkg/:name +``` + +URL parameters: + +- `:org` is the name of your organisation. Validator: [`^[a-zA-Z0-9_-]+$`](https://regexper.com/#%5E%5Ba-zA-Z0-9_-%5D%2B%24). +- `:name` is the name of the package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). + +Status codes: + +- `200` if file is successfully retrieved +- `404` if file is not found + +Example: + +```bash +curl -X GET http://localhost:4001/finn/pkg/fuzz +``` + +### Package version overview + +**Method:** `GET` + +Retrieves an overview of the files of a package version. + +```bash +https://:assetServerUrl:port/:org/pkg/:name/:version +``` + +URL parameters: + +- `:org` is the name of your organisation. Validator: [`^[a-zA-Z0-9_-]+$`](https://regexper.com/#%5E%5Ba-zA-Z0-9_-%5D%2B%24). +- `:name` is the name of the package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). +- `:version` is the version of the package. Validator: Comply with [semver validation regex](https://semver.org/). + +Status codes: + +- `200` if file is successfully retrieved +- `404` if file is not found + +Example: + +```bash +curl -X GET http://localhost:4001/finn/pkg/fuzz +``` ## Import Maps @@ -151,6 +203,31 @@ Example: curl -X PUT -i -F map=@import-map.json http://localhost:4001/finn/map/buzz/8.4.1 ``` +### Latest Import Map versions + +**Method:** `GET` + +Retrieves an overview of the latest versions of a Import Map. + +```bash +https://:assetServerUrl:port/:org/map/:name +``` + +URL parameters: + +- `:org` is the name of your organisation. Validator: [`^[a-zA-Z0-9_-]+$`](https://regexper.com/#%5E%5Ba-zA-Z0-9_-%5D%2B%24). +- `:name` is the name of the import map. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name). + +Status codes: + +- `200` if file is successfully retrieved +- `404` if file is not found + +Example: + +```bash +curl -X GET http://localhost:4001/finn/map/buzz +``` ## Aliases diff --git a/lib/classes/http-outgoing.js b/lib/classes/http-outgoing.js index 92801eef..d16bf234 100644 --- a/lib/classes/http-outgoing.js +++ b/lib/classes/http-outgoing.js @@ -1,5 +1,15 @@ 'use strict'; +const { isReadableStream } = require('../utils/utils'); + +const STATUS_CODES = [ + 100, 101, 102, 103, + 200, 201, 202, 203, 204, 205, 206, 207, 208, 226, + 300, 301, 302, 303, 304, 305, 306, 307, 308, + 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451, + 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, +]; + const HttpOutgoing = class HttpOutgoing { constructor() { this._statusCode = 200; @@ -11,7 +21,11 @@ const HttpOutgoing = class HttpOutgoing { } set statusCode(value) { - this._statusCode = value; + if (Number.isInteger(value) && STATUS_CODES.includes(value)) { + this._statusCode = value; + return; + } + throw new Error('Value is not a legal http status code'); } get statusCode() { @@ -35,6 +49,7 @@ const HttpOutgoing = class HttpOutgoing { } set stream(value) { + if (!isReadableStream(value)) throw new Error('Value is not a Readable stream'); this._stream = value; } diff --git a/lib/classes/meta.js b/lib/classes/meta.js index ea80c89e..d651c0b5 100644 --- a/lib/classes/meta.js +++ b/lib/classes/meta.js @@ -1,7 +1,7 @@ 'use strict'; const Meta = class Meta { - construct({ + constructor({ value = '', name = '', } = {}) { diff --git a/lib/classes/versions.js b/lib/classes/versions.js new file mode 100644 index 00000000..48b560b0 --- /dev/null +++ b/lib/classes/versions.js @@ -0,0 +1,64 @@ +'use strict'; + +const semver = require('semver'); + +const Versions = class Versions { + constructor({ versions = [], name = '', org = '' } = {}) { + this._versions = new Map(versions); + this._name = name; + this._org = org; + } + + get versions() { + return Array.from(this._versions.entries()).sort((a, b) => { + return a[0] > b[0] ? -1 : 1; + }); + } + + get name() { + return this._name; + } + + get org() { + return this._org; + } + + setVersion(version, integrity) { + if (!this.check(version)) { + throw new Error('Semver version is lower than previous version'); + } + const major = semver.major(version); + this._versions.set(major, { + version, + integrity, + }); + } + + getVersion(major) { + return this._versions.get(major); + } + + check(version) { + const major = semver.major(version); + const previous = this.getVersion(major); + if (previous) { + if (semver.gte(previous.version, version)) { + return false; + } + } + return true; + } + + toJSON() { + return { + versions: this.versions, + name: this.name, + org: this.org, + }; + } + + get [Symbol.toStringTag]() { + return 'Versions'; + } +} +module.exports = Versions; diff --git a/lib/handlers/map.put.js b/lib/handlers/map.put.js index 81dd7f80..5fdb0111 100644 --- a/lib/handlers/map.put.js +++ b/lib/handlers/map.put.js @@ -3,12 +3,14 @@ const { validators } = require('@eik/common'); const HttpError = require('http-errors'); const Busboy = require('busboy'); +const crypto = require('crypto'); const abslog = require('abslog'); -const { createFilePathToImportMap } = require('../utils/path-builders-fs'); +const { createFilePathToImportMap, createFilePathToVersion } = require('../utils/path-builders-fs'); const { createURIPathToImportMap } = require('../utils/path-builders-uri'); const HttpIncoming = require('../classes/http-incoming'); const HttpOutgoing = require('../classes/http-outgoing'); +const Versions = require('../classes/versions'); const utils = require('../utils/utils'); const conf = require('../utils/defaults'); @@ -21,7 +23,6 @@ const MapPut = class MapPut { _parser(incoming) { return new Promise((resolve, reject) => { - const pathname = createURIPathToImportMap(incoming); const path = createFilePathToImportMap(incoming); const busboy = new Busboy({ @@ -42,11 +43,14 @@ const MapPut = class MapPut { return; } + const hasher = crypto.createHash('sha512'); + // Buffer up the incoming file and check if we can // parse it as JSON or not. let obj = {}; try { const str = await utils.streamCollector(file); + hasher.update(str); obj = JSON.parse(str); } catch (error) { this._log.error(`map:put - Import map can not be parsed`); @@ -66,20 +70,20 @@ const MapPut = class MapPut { return; } this._log.info(`map:put - Successfully wrote import map to sink - Pathname: ${path}`); - }); - busboy.on('finish', () => { - const outgoing = new HttpOutgoing(); - outgoing.mimeType = 'text/plain'; - outgoing.statusCode = 303; - outgoing.location = pathname; - resolve(outgoing); + const integrity = `sha512-${hasher.digest('base64')}`; + + busboy.emit('completed', integrity); }); busboy.on('error', error => { reject(error); }); + busboy.once('completed', (integrity) => { + resolve(integrity); + }); + // If incoming.request is handeled by stream.pipeline, it will // close to early for the http framework to handle it. Let the // http framework handle closing incoming.request @@ -87,14 +91,30 @@ const MapPut = class MapPut { }); } - async _exist (incoming) { + async _readVersions (incoming) { + const path = createFilePathToVersion(incoming); + let versions; try { - const path = createFilePathToImportMap(incoming); - await this._sink.exist(path); - return true; + const obj = await utils.readJSON(this._sink, path); + versions = new Versions(obj); + this._log.info(`map:put - Successfully read version meta file from sink - Pathname: ${path}`); } catch (error) { - return false; + // File does not exist, its probably a new package + versions = new Versions(incoming); + this._log.info(`map:put - Version meta file did not exist in sink - Create new - Pathname: ${path}`); } + return versions; + } + + async _writeVersions (incoming, versions) { + const path = createFilePathToVersion(incoming); + await utils.writeJSON( + this._sink, + path, + versions, + 'application/json' + ); + this._log.info(`map:put - Successfully wrote version meta file to sink - Pathname: ${path}`); } async handler (req, org, name, version) { @@ -108,18 +128,34 @@ const MapPut = class MapPut { } const incoming = new HttpIncoming(req, { + type: 'map', version, name, org, }); - const exist = await this._exist(incoming); - if (exist) { - this._log.info(`map:put - Import map exists - Org: ${org} - Name: ${name} - Version: ${version}`); + const versions = await this._readVersions(incoming); + + if (!versions.check(version)) { + this._log.info(`map:put - Semver version is lower than previous version of the package - Org: ${org} - Name: ${name} - Version: ${version}`); throw new HttpError.Conflict(); } - const outgoing = await this._parser(incoming); + const integrity = await this._parser(incoming); + + versions.setVersion(version, integrity); + + try { + await this._writeVersions(incoming, versions); + } catch(error) { + throw new HttpError.BadGateway(); + } + + const outgoing = new HttpOutgoing(); + outgoing.mimeType = 'text/plain'; + outgoing.statusCode = 303; + outgoing.location = createURIPathToImportMap(incoming); + return outgoing; } } diff --git a/lib/handlers/pkg.put.js b/lib/handlers/pkg.put.js index ba2c30ed..34bbce32 100644 --- a/lib/handlers/pkg.put.js +++ b/lib/handlers/pkg.put.js @@ -8,10 +8,11 @@ const Busboy = require('busboy'); const crypto = require('crypto'); const tar = require('tar'); -const { createFilePathToPackage, createFilePathToAsset } = require('../utils/path-builders-fs'); +const { createFilePathToPackage, createFilePathToAsset, createFilePathToVersion } = require('../utils/path-builders-fs'); const { createURIPathToPkgLog } = require('../utils/path-builders-uri'); const HttpIncoming = require('../classes/http-incoming'); const HttpOutgoing = require('../classes/http-outgoing'); +const Versions = require('../classes/versions'); const Package = require('../classes/package'); const Asset = require('../classes/asset'); const utils = require('../utils/utils'); @@ -131,31 +132,20 @@ const PkgPut = class PkgPut { return pkg; }).then(async (pkg) => { const path = createFilePathToPackage(pkg); - - this._log.info(`pkg:put - Start writing package meta file to sink - Pathname: ${path}`); - await utils.writeJSON( this._sink, path, pkg, 'application/json', ); - return pkg; - }).then((pkg) => { - const pathname = createURIPathToPkgLog(pkg); - this._log.info(`pkg:put - Successfully wrote package meta file to sink - URI: ${pathname}`); + this._log.info(`pkg:put - Successfully wrote package meta file to sink - Pathname: ${path}`); - const outgoing = new HttpOutgoing(); - outgoing.mimeType = 'text/plain'; - outgoing.statusCode = 303; - outgoing.location = pathname; - - resolve(outgoing); + resolve(pkg); }).catch(err => { this._log.error('pkg:put - Failed writing package meta file to sink'); this._log.trace(err); - reject(err); + reject(HttpError.BadGateway()); }); }); @@ -166,14 +156,30 @@ const PkgPut = class PkgPut { }); } - async _exist (incoming) { + async _readVersions (incoming) { + const path = createFilePathToVersion(incoming); + let versions; try { - const path = createFilePathToPackage(incoming); - await this._sink.exist(path); - return true; + const obj = await utils.readJSON(this._sink, path); + versions = new Versions(obj); + this._log.info(`pkg:put - Successfully read version meta file from sink - Pathname: ${path}`); } catch (error) { - return false; + // File does not exist, its probably a new package + versions = new Versions(incoming); + this._log.info(`pkg:put - Version meta file did not exist in sink - Create new - Pathname: ${path}`); } + return versions; + } + + async _writeVersions (incoming, versions) { + const path = createFilePathToVersion(incoming); + await utils.writeJSON( + this._sink, + path, + versions, + 'application/json' + ); + this._log.info(`pkg:put - Successfully wrote version meta file to sink - Pathname: ${path}`); } async handler (req, org, name, version) { @@ -187,18 +193,33 @@ const PkgPut = class PkgPut { } const incoming = new HttpIncoming(req, { + type: 'pkg', version, name, org, }); - const exist = await this._exist(incoming); - if (exist) { - this._log.info(`pkg:put - Package exists - Org: ${org} - Name: ${name} - Version: ${version}`); + const versions = await this._readVersions(incoming); + + if (!versions.check(version)) { + this._log.info(`pkg:put - Semver version is lower than previous version of the package - Org: ${org} - Name: ${name} - Version: ${version}`); throw new HttpError.Conflict(); } - const outgoing = await this._parser(incoming); + const pkg = await this._parser(incoming); + versions.setVersion(version, pkg.integrity); + + try { + await this._writeVersions(incoming, versions); + } catch(error) { + throw new HttpError.BadGateway(); + } + + const outgoing = new HttpOutgoing(); + outgoing.mimeType = 'text/plain'; + outgoing.statusCode = 303; + outgoing.location = createURIPathToPkgLog(pkg); + return outgoing; } } diff --git a/lib/handlers/versions.get.js b/lib/handlers/versions.get.js new file mode 100644 index 00000000..8e48ba61 --- /dev/null +++ b/lib/handlers/versions.get.js @@ -0,0 +1,56 @@ +'use strict'; + +const { validators } = require('@eik/common'); +const HttpError = require('http-errors'); +const abslog = require('abslog'); + +const { createFilePathToVersion } = require('../utils/path-builders-fs'); +const HttpOutgoing = require('../classes/http-outgoing'); +const conf = require('../utils/defaults'); + +const VersionsGet = class VersionsGet { + constructor(sink, config = {}, logger) { + this._config = { ...conf, ...config}; + this._sink = sink; + this._log = abslog(logger); + } + + async handler (req, org, type, name) { + try { + validators.name(name); + validators.type(type); + validators.org(org); + } catch (error) { + this._log.debug(`pkg:latest - Validation failed - ${error.message}`); + throw new HttpError.NotFound(); + } + + const path = createFilePathToVersion({ org, type, name }); + + try { + const file = await this._sink.read(path); + const outgoing = new HttpOutgoing(); + outgoing.mimeType = 'application/json'; + + if (this._config.etag) { + outgoing.etag = file.etag; + } + + if (this._config.etag && req.headers['if-none-match'] === file.etag) { + outgoing.statusCode = 304; + file.stream.destroy(); + } else { + outgoing.statusCode = 200; + outgoing.stream = file.stream; + } + + this._log.debug(`pkg:latest - Package log found - Pathname: ${path}`); + + return outgoing; + } catch (error) { + this._log.debug(`pkg:latest - Package log found - Pathname: ${path}`); + throw new HttpError.NotFound(); + } + } +} +module.exports = VersionsGet; diff --git a/lib/main.js b/lib/main.js index d74f7428..267fd6e5 100644 --- a/lib/main.js +++ b/lib/main.js @@ -1,5 +1,6 @@ 'use strict'; +const VersionsGet = require('./handlers/versions.get'); const AliasPost = require('./handlers/alias.post'); const AliasPut = require('./handlers/alias.put'); const AliasGet = require('./handlers/alias.get'); @@ -16,6 +17,7 @@ const FS = require('./sinks/fs'); const globals = require('./utils/globals'); module.exports.http = { + VersionsGet, AliasPost, AliasPut, AliasGet, diff --git a/lib/utils/path-builders-fs.js b/lib/utils/path-builders-fs.js index bf1240c4..f3366c2d 100644 --- a/lib/utils/path-builders-fs.js +++ b/lib/utils/path-builders-fs.js @@ -34,3 +34,11 @@ const createFilePathToAlias = ({ org = '', type = '', name = '', alias = '' } = return path.join(ROOT, org, type, name, `${alias}.alias.json`); } module.exports.createFilePathToAlias = createFilePathToAlias; + + +// Build file system path to an version file + +const createFilePathToVersion = ({ org = '', type = '', name = '' } = {}) => { + return path.join(ROOT, org, type, name, 'versions.json'); +} +module.exports.createFilePathToVersion = createFilePathToVersion; \ No newline at end of file diff --git a/package.json b/package.json index a12c9af9..7e2d1022 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "tar": "^5.0.5" }, "devDependencies": { - "eslint": "^6.7.1", + "eslint": "^6.7.2", "eslint-config-airbnb-base": "^14.0.0", "eslint-config-prettier": "^6.7.0", "eslint-plugin-import": "^2.18.2", @@ -39,6 +39,6 @@ "mkdirp": "^0.5.1", "node-fetch": "^2.6.0", "prettier": "^1.19.1", - "tap": "^14.10.1" + "tap": "^14.10.2" } } diff --git a/services/fastify.js b/services/fastify.js index 709984ad..0d9d10a1 100644 --- a/services/fastify.js +++ b/services/fastify.js @@ -31,7 +31,7 @@ class FastifyService { // Error handling this.app.setErrorHandler((error, request, reply) => { - // app.log.error(error); + this.log.error(error); if (error.statusCode) { reply.code(error.statusCode).send(error.message); return; @@ -41,6 +41,7 @@ class FastifyService { this.routes(); + this._versionsGet = new http.VersionsGet(this.sink, config, logger); this._aliasPost = new http.AliasPost(this.sink, config, logger); this._aliasDel = new http.AliasDel(this.sink, config, logger); this._aliasGet = new http.AliasGet(this.sink, config, logger); @@ -57,6 +58,25 @@ class FastifyService { // Packages // + // curl -X GET http://localhost:4001/biz/pkg/fuzz + + this.app.get( + `/:org/${prop.base_pkg}/:name`, + async (request, reply) => { + const outgoing = await this._versionsGet.handler( + request.req, + request.params.org, + prop.base_pkg, + request.params.name, + ); + + reply.header('etag', outgoing.etag); + reply.type(outgoing.mimeType); + reply.code(outgoing.statusCode); + reply.send(outgoing.stream); + }, + ); + // curl -X GET http://localhost:4001/biz/pkg/fuzz/8.4.1 this.app.get( @@ -118,6 +138,25 @@ class FastifyService { // Import Maps // + // curl -X GET http://localhost:4001/biz/map/buzz + + this.app.get( + `/:org/${prop.base_map}/:name`, + async (request, reply) => { + const outgoing = await this._versionsGet.handler( + request.req, + request.params.org, + prop.base_map, + request.params.name, + ); + + reply.header('etag', outgoing.etag); + reply.type(outgoing.mimeType); + reply.code(outgoing.statusCode); + reply.send(outgoing.stream); + }, + ); + // curl -X GET http://localhost:4001/biz/map/buzz/4.2.2 this.app.get( diff --git a/test/classes/alias.js b/test/classes/alias.js index fb36c624..688701e2 100644 --- a/test/classes/alias.js +++ b/test/classes/alias.js @@ -1,14 +1,65 @@ 'use strict'; -const tap = require('tap'); +const { test } = require('tap'); const Alias = require('../../lib/classes/alias'); -// -// Constructor -// +test('Alias() - Object type', (t) => { + const obj = new Alias(); + t.equal(Object.prototype.toString.call(obj), '[object Alias]', 'should be Alias'); + t.end(); +}); -tap.test('Alias() - object type - should be Alias', (t) => { +test('Alias() - Default property values', (t) => { const obj = new Alias(); - t.equal(Object.prototype.toString.call(obj), '[object Alias]'); + t.equal(obj.version, '', '.version should be empty String'); + t.equal(obj.alias, '', '.alias should be empty String'); + t.equal(obj.name, '', '.name should be empty String'); + t.equal(obj.type, '', '.type should be empty String'); + t.equal(obj.org, '', '.org should be empty String'); t.end(); }); + +test('Alias() - Set values to the arguments on the constructor', (t) => { + const obj = new Alias({ + version: '1.0.0', + alias: 'v1', + name: 'buzz', + type: 'pkg', + org: 'bizz', + }); + t.equal(obj.version, '', '.version should be empty String'); + t.equal(obj.alias, 'v1', '.alias should contain value set on constructor'); + t.equal(obj.name, 'buzz', '.name should contain value set on constructor'); + t.equal(obj.type, 'pkg', '.type should contain value set on constructor'); + t.equal(obj.org, 'bizz', '.org should contain value set on constructor'); + t.end(); +}); + +test('Alias() - Set a value on the .version property', (t) => { + const obj = new Alias({ + version: '1.0.0', + }); + obj.version = '2.0.0'; + t.equal(obj.version, '2.0.0', '.version should be value set on .version'); + t.end(); +}); + +test('Alias() - Serialize object', (t) => { + const obj = new Alias({ + version: '1.0.0', + alias: 'v1', + name: 'buzz', + type: 'pkg', + org: 'bizz', + }); + obj.version = '2.0.0'; + + const o = JSON.parse(JSON.stringify(obj)); + + t.equal(o.version, '2.0.0', '.version should be value set on .version'); + t.equal(o.alias, 'v1', '.alias should contain value set on constructor'); + t.equal(o.name, 'buzz', '.name should contain value set on constructor'); + t.equal(o.type, 'pkg', '.type should contain value set on constructor'); + t.equal(o.org, 'bizz', '.org should contain value set on constructor'); + t.end(); +}); \ No newline at end of file diff --git a/test/classes/http-incoming.js b/test/classes/http-incoming.js index 43e24c0a..219c71c5 100644 --- a/test/classes/http-incoming.js +++ b/test/classes/http-incoming.js @@ -1,14 +1,47 @@ 'use strict'; -const tap = require('tap'); +const { test } = require('tap'); const HttpIncoming = require('../../lib/classes/http-incoming'); -// -// Constructor -// +test('HttpIncoming() - Object type', (t) => { + const obj = new HttpIncoming(); + t.equal(Object.prototype.toString.call(obj), '[object HttpIncoming]', 'should be HttpIncoming'); + t.end(); +}); -tap.test('HttpIncoming() - object type - should be HttpIncoming', (t) => { +test('HttpIncoming() - Default property values', (t) => { const obj = new HttpIncoming(); - t.equal(Object.prototype.toString.call(obj), '[object HttpIncoming]'); + t.type(obj.request, 'undefined', '.request should be undefined'); + t.same(obj.headers, {}, '.headers should be empty Object'); + t.equal(obj.version, '', '.version should be empty String'); + t.equal(obj.extras, '', '.extras should be empty String'); + t.equal(obj.alias, '', '.alias should be empty String'); + t.equal(obj.type, '', '.type should be empty String'); + t.equal(obj.name, '', '.name should be empty String'); + t.equal(obj.org, '', '.org should be empty String'); + t.end(); +}); + +test('HttpIncoming() - Set values to the arguments on the constructor', (t) => { + const obj = new HttpIncoming({ + headers: { + foo: 'bar' + } + }, { + version: '1.0.0', + extras: '/foo', + alias: 'v1', + name: 'buzz', + type: 'pkg', + org: 'bizz', + }); + t.type(obj.request, 'object', '.request should contain value set on constructor'); + t.same(obj.headers, { foo: 'bar' }, '.headers should be the headers from the request argument'); + t.equal(obj.version, '1.0.0', '.version should contain value set on constructor'); + t.equal(obj.extras, '/foo', '.extras should contain value set on constructor'); + t.equal(obj.alias, 'v1', '.alias should contain value set on constructor'); + t.equal(obj.type, 'pkg', '.type should contain value set on constructor'); + t.equal(obj.name, 'buzz', '.name should contain value set on constructor'); + t.equal(obj.org, 'bizz', '.org should contain value set on constructor'); t.end(); }); diff --git a/test/classes/http-outgoing.js b/test/classes/http-outgoing.js index 7c369b20..556293af 100644 --- a/test/classes/http-outgoing.js +++ b/test/classes/http-outgoing.js @@ -1,14 +1,91 @@ 'use strict'; -const tap = require('tap'); +const { Readable } = require('stream'); +const { test } = require('tap'); const HttpOutgoing = require('../../lib/classes/http-outgoing'); -// -// Constructor -// +test('HttpOutgoing() - Object type', (t) => { + const obj = new HttpOutgoing(); + t.equal(Object.prototype.toString.call(obj), '[object HttpOutgoing]', 'should be HttpIncoming'); + t.end(); +}); + +test('HttpOutgoing() - Default property values', (t) => { + const obj = new HttpOutgoing(); + t.equal(obj.statusCode, 200, '.statusCode should be the Number 200'); + t.equal(obj.location, '', '.location should be empty String'); + t.equal(obj.mimeType, 'text/plain', '.mimeType should be the String "text/plain"'); + t.type(obj.stream, 'undefined', '.stream should be undefined'); + t.type(obj.body, 'undefined', '.body should be undefined'); + t.equal(obj.etag, '', '.etag should be empty String'); + t.end(); +}); + +test('HttpOutgoing() - Set .statusCode to legal value', (t) => { + const obj = new HttpOutgoing(); + obj.statusCode = 404; + t.equal(obj.statusCode, 404, '.statusCode should be the set value'); + t.end(); +}); + +test('HttpOutgoing() - Set .statusCode to non numeric value', (t) => { + t.plan(1); + t.throws(() => { + const obj = new HttpOutgoing(); + obj.statusCode = 'fouronefour'; + }, /Value is not a legal http status code/, 'Should throw'); + t.end(); +}); + +test('HttpOutgoing() - Set .statusCode to a illegal http status code', (t) => { + t.plan(1); + t.throws(() => { + const obj = new HttpOutgoing(); + obj.statusCode = 98555555; + }, /Value is not a legal http status code/, 'Should throw'); + t.end(); +}); + +test('HttpOutgoing() - Set .location to legal value', (t) => { + const obj = new HttpOutgoing(); + obj.location = '/foo'; + t.equal(obj.location, '/foo', '.location should be the set value'); + t.end(); +}); + +test('HttpOutgoing() - Set .mimeType to legal value', (t) => { + const obj = new HttpOutgoing(); + obj.mimeType = 'application/javascript'; + t.equal(obj.mimeType, 'application/javascript', '.location should be the set value'); + t.end(); +}); + +test('HttpOutgoing() - Set .stream to legal value', (t) => { + const obj = new HttpOutgoing(); + obj.stream = new Readable(); + t.true(obj.stream instanceof Readable, '.stream should be the set value'); + t.end(); +}); + +test('HttpOutgoing() - Set a non Readable stream as value on the .stream property', (t) => { + t.plan(1); + t.throws(() => { + const obj = new HttpOutgoing(); + obj.stream = 'foo'; + }, /Value is not a Readable stream/, 'Should throw'); + t.end(); +}); + +test('HttpOutgoing() - Set .body to legal value', (t) => { + const obj = new HttpOutgoing(); + obj.body = 'foo'; + t.equal(obj.body, 'foo', '.location should be the set value'); + t.end(); +}); -tap.test('HttpOutgoing() - object type - should be HttpOutgoing', (t) => { +test('HttpOutgoing() - Set .etag to legal value', (t) => { const obj = new HttpOutgoing(); - t.equal(Object.prototype.toString.call(obj), '[object HttpOutgoing]'); + obj.etag = 'foo'; + t.equal(obj.etag, 'foo', '.location should be the set value'); t.end(); }); diff --git a/test/classes/meta.js b/test/classes/meta.js new file mode 100644 index 00000000..5bcff84a --- /dev/null +++ b/test/classes/meta.js @@ -0,0 +1,34 @@ +'use strict'; + +const { test } = require('tap'); +const Meta = require('../../lib/classes/meta'); + +test('Meta() - Object type', (t) => { + const obj = new Meta(); + t.equal(Object.prototype.toString.call(obj), '[object Meta]', 'should be Meta'); + t.end(); +}); + +test('Meta() - Default property values', (t) => { + const obj = new Meta(); + t.equal(obj.value, '', '.value should be empty String'); + t.equal(obj.name, '', '.name should be empty String'); + t.end(); +}); + +test('Meta() - Set arguments on the constructor', (t) => { + const obj = new Meta({ value: 'foo', name: 'bar' }); + t.equal(obj.value, 'foo', '.value should be the set value'); + t.equal(obj.name, 'bar', '.name should be the set value'); + t.end(); +}); + +test('Meta() - Serialize object', (t) => { + const obj = new Meta({ value: 'foo', name: 'bar' }); + + const o = JSON.parse(JSON.stringify(obj)); + + t.equal(o.value, 'foo', '.value should be the set value'); + t.equal(o.name, 'bar', '.name should be the set value'); + t.end(); +}); \ No newline at end of file diff --git a/test/classes/read-file.js b/test/classes/read-file.js index 5248e605..9af592ab 100644 --- a/test/classes/read-file.js +++ b/test/classes/read-file.js @@ -6,7 +6,7 @@ const ReadFile = require('../../lib/classes/read-file'); test('ReadFile() - Object type', (t) => { const obj = new ReadFile(); - t.equal(Object.prototype.toString.call(obj), '[object ReadFile]', 'should be HttpIncoming'); + t.equal(Object.prototype.toString.call(obj), '[object ReadFile]', 'should be ReadFile'); t.end(); }); diff --git a/test/classes/versions.js b/test/classes/versions.js new file mode 100644 index 00000000..66649e05 --- /dev/null +++ b/test/classes/versions.js @@ -0,0 +1,112 @@ +'use strict'; + +const { test } = require('tap'); +const Versions = require('../../lib/classes/versions'); + +test('Versions() - Object type', (t) => { + const obj = new Versions(); + t.equal(Object.prototype.toString.call(obj), '[object Versions]', 'should be Versions'); + t.end(); +}); + +test('Versions() - Default property values', (t) => { + const obj = new Versions(); + t.strictSame(obj.versions, [], '.version should be empty Array'); + t.equal(obj.name, '', '.name should be empty String'); + t.equal(obj.org, '', '.org should be empty String'); + t.end(); +}); + +test('Versions() - Set a value on the "name" argument on the constructor', (t) => { + const obj = new Versions({ name: 'foo' }); + t.equal(obj.name, 'foo', '.name should be value set on constructor'); + t.end(); +}); + +test('Versions() - Set a value on the "name" argument on the constructor', (t) => { + const obj = new Versions({ org: 'bar' }); + t.equal(obj.org, 'bar', '.org should be value set on constructor'); + t.end(); +}); + +test('Versions() - Set the multiple versions in the same major range', (t) => { + const obj = new Versions(); + obj.setVersion('4.3.2', 'bar'); + obj.setVersion('4.6.1', 'foo'); + t.strictSame(obj.versions, [ + [4, { integrity: 'foo', version: '4.6.1' }] + ], '.versions should have only one major version'); + t.end(); +}); + +test('Versions() - Set multiple versions with different major range', (t) => { + const obj = new Versions(); + obj.setVersion('1.7.3', 'rab'); + obj.setVersion('3.3.2', 'bar'); + obj.setVersion('4.6.1', 'foo'); + obj.setVersion('2.6.9', 'xyz'); + t.strictSame(obj.versions, [ + [4, { integrity: 'foo', version: '4.6.1' }], + [3, { integrity: 'bar', version: '3.3.2' }], + [2, { integrity: 'xyz', version: '2.6.9' }], + [1, { integrity: 'rab', version: '1.7.3' }], + ], '.versions should have multiple major version in sorted order'); + t.end(); +}); + +test('Version() - Set a version with lower semver version than latest', (t) => { + t.plan(1); + + const obj = new Versions(); + obj.setVersion('3.3.2', 'bar'); + obj.setVersion('3.4.1', 'foo'); + + t.throws(() => { + obj.setVersion('3.2.1', 'xyz'); + }, /Semver version is lower than previous version/, 'Should throw'); + t.end(); +}); + +test('Versions() - Get a version', (t) => { + const obj = new Versions(); + obj.setVersion('4.2.4', 'xyz'); + obj.setVersion('4.3.2', 'bar'); + obj.setVersion('3.6.1', 'foo'); + + const v3 = obj.getVersion(3); + const v4 = obj.getVersion(4); + + t.strictSame(v3, { integrity: 'foo', version: '3.6.1' }, 'should match values set by .setVersion()'); + t.strictSame(v4, { integrity: 'bar', version: '4.3.2' }, 'should match values set by .setVersion()'); + + t.end(); +}); + +test('Versions() - Set values to the arguments on the constructor', (t) => { + const obj = new Versions({ name: 'buzz', org: 'bizz' }); + obj.setVersion('1.7.3', 'rab'); + obj.setVersion('3.3.2', 'bar'); + obj.setVersion('4.6.1', 'foo'); + obj.setVersion('2.6.9', 'xyz'); + + const serialized = JSON.parse(JSON.stringify(obj)); + const o = new Versions(serialized); + + t.equal(o.name, obj.name, '.name should be same as in original object'); + t.equal(o.org, obj.org, '.org should be same as in original object'); + + t.strictSame(o.versions, [ + [4, { integrity: 'foo', version: '4.6.1' }], + [3, { integrity: 'bar', version: '3.3.2' }], + [2, { integrity: 'xyz', version: '2.6.9' }], + [1, { integrity: 'rab', version: '1.7.3' }], + ], '.versions should have multiple major version in sorted order'); + + const v3 = o.getVersion(3); + const v4 = o.getVersion(4); + + t.strictSame(v3, { integrity: 'bar', version: '3.3.2' }, 'should match values set by .setVersion() on the original object'); + t.strictSame(v4, { integrity: 'foo', version: '4.6.1' }, 'should match values set by .setVersion() on the original object'); + + t.end(); +});