diff --git a/lib/applications.js b/lib/applications.js index 6dcae959..5ae92831 100644 --- a/lib/applications.js +++ b/lib/applications.js @@ -1,15 +1,27 @@ 'use strict'; +const semver = require('semver'); + +const common = require('./common'); const errors = require('./errors'); // Generic application class. You are free to substitute it with whatever suits // your needs. -// name - application name -// api - application API +// name - application name that may contain version after '@' +// (e.g. app@1.0.0). Version in name is preferred over +// 'version' parameter +// api - object that contains interfaces as its fields each of which +// contains functions by method names. Each method has the following +// signature (connection, <0 or more method arguments>, callback) +// version - application version of 'name' application // class Application { - constructor(name, api) { - this.name = name; + constructor(name, api, version) { + [this.name, this.version] = common.rsplit(name, '@'); + if (!this.version) this.version = version; + if (this.version && !semver.valid(this.version)) { + throw new TypeError('Invalid semver version'); + } this.api = api; } @@ -54,10 +66,41 @@ class Application { // applications - array of JSTP applications // const createAppsIndex = (applications) => { - const index = {}; + const latestApps = new Map(); + const index = new Map(); applications.forEach((application) => { - index[application.name] = application; + let appVersions = index.get(application.name); + if (!appVersions) { + appVersions = new Map(); + index.set(application.name, appVersions); + } + if (!application.version) { + // no version means latest version + if (appVersions.has('latest')) { + throw new Error( + `Multiple entries of '${application.name} without version` + ); + } else { + appVersions.set('latest', application); + } + } else { + appVersions.set(application.version, application); + + // save latest version to fill missing latest versions later + const latestApp = latestApps.get(application.name); + if (!latestApp || semver.gt(application.version, latestApp.version)) { + latestApps.set(application.name, application); + } + } + }); + + // set latest versions of apps without explicit latest version + latestApps.forEach((application, appName) => { + const appVersions = index.get(appName); + if (!appVersions.has('latest')) { + appVersions.set('latest', application); + } }); return index; diff --git a/lib/cli/command-processor.js b/lib/cli/command-processor.js index 6a525903..50f5fbf1 100644 --- a/lib/cli/command-processor.js +++ b/lib/cli/command-processor.js @@ -112,7 +112,7 @@ module.exports = class CommandProcessor extends EventEmitter { callback(); } - connect(protocol, host, port, appName, interfaces, callback) { + connect(protocol, host, port, app, interfaces, callback) { let transport; let args; @@ -139,7 +139,7 @@ module.exports = class CommandProcessor extends EventEmitter { return; } this.cli.connectInitiated = true; - transport.connectAndInspect(appName, null, interfaces, ...args, + transport.connectAndInspect(app, null, interfaces, ...args, (err, connection, api) => { if (err) { this.cli.connectInitiated = false; diff --git a/lib/cli/line-processor.js b/lib/cli/line-processor.js index c08a7d4f..6310bc72 100644 --- a/lib/cli/line-processor.js +++ b/lib/cli/line-processor.js @@ -98,14 +98,14 @@ module.exports = class LineProcessor { return; } } - const appName = args[1]; - if (appName === undefined) { + const app = args[1]; + if (app === undefined) { callback(reportMissingArgument('Application name')); return; } const interfaces = args[2] ? utils.split(args[2], ' ') : []; this.commandProcessor.connect( - scheme, host, port, appName, interfaces, + scheme, host, port, app, interfaces, (err) => { if (err) { callback(err); diff --git a/lib/common.js b/lib/common.js index a6b2b1f3..818b05a4 100644 --- a/lib/common.js +++ b/lib/common.js @@ -58,6 +58,14 @@ const extractCallback = (args) => { // const doNothing = () => {}; +// Splits string by the last occurrence of separator +// +const rsplit = (string, separator) => { + const lastIndex = string.lastIndexOf(separator); + if (lastIndex < 0) return [string]; + return [string.slice(0, lastIndex), string.slice(lastIndex + 1)]; +}; + module.exports = { forwardEvent, forwardMultipleEvents, @@ -65,4 +73,5 @@ module.exports = { mixin, extractCallback, doNothing, + rsplit, }; diff --git a/lib/connection.js b/lib/connection.js index f938e05c..b4082022 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -1,6 +1,7 @@ 'use strict'; const { EventEmitter } = require('events'); +const semver = require('semver'); const timers = require('timers'); const common = require('./common'); @@ -117,15 +118,46 @@ class Connection extends EventEmitter { } // Send a handshake message over the connection - // appName - name of an application to connect to + // app - string or object, application to connect to as 'name' or + // 'name@version' or { name, version }, where version + // must be a valid semver range // login - user name (optional) // password - user password (optional) // callback - callback function to invoke after the handshake is completed // - handshake(appName, login, password, callback) { - const message = login && password ? - this._createMessage('handshake', appName, 'login', [login, password]) : - this._createMessage('handshake', appName); + handshake(app, login, password, callback) { + let name, version; + if (typeof app === 'string') { + [name, version] = common.rsplit(app, '@'); + } else { + name = app.name; + version = app.version; + } + + if (version && !semver.validRange(version)) { + const error = new Error('Invalid semver version range'); + if (callback) { + callback(error); + } else { + this.emit('error', error); + } + return; + } + + let message; + if (version) { + message = login && password ? + this._createMessageWithArray( + 'handshake', [name, version], 'login', [login, password] + ) : + this._createMessageWithArray( + 'handshake', [name, version] + ); + } else { + message = login && password ? + this._createMessage('handshake', name, 'login', [login, password]) : + this._createMessage('handshake', name); + } const messageId = message.handshake[0]; this._callbacks[messageId] = (error, sessionId) => { @@ -216,6 +248,28 @@ class Connection extends EventEmitter { } } + // Create a JSTP message + // kind - message kind + // target - array of arguments for kind key, usually a name + // of an interface or an application (optional) + // with api version + // verb - action specific for different message kinds + // args - action arguments + // + _createMessageWithArray(kind, target, verb, args) { + const message = { + [kind]: [this._nextMessageId, ...target], + }; + + if (verb) { + message[verb] = args; + } + + this._nextMessageId += this._messageIdDelta; + + return message; + } + // Create a JSTP message // kind - message kind // target - name of an interface or an application (optional) @@ -394,7 +448,10 @@ class Connection extends EventEmitter { } const applicationName = message.handshake[1]; - const application = this.server.applications[applicationName]; + const applicationVersion = message.handshake[2]; + const application = this.server._getApplication( + applicationName, applicationVersion + ); if (!application) { this._handshakeError(errors.ERR_APP_NOT_FOUND); diff --git a/lib/net.js b/lib/net.js index 96740d34..cee15bfd 100644 --- a/lib/net.js +++ b/lib/net.js @@ -55,8 +55,8 @@ module.exports = { Server, createServer: (options, listener) => createServer(options, listener), - connect: (appName, client, ...options) => - connect(appName, client, ...options), - connectAndInspect: (appName, client, interfaces, ...options) => - connectAndInspect(appName, client, interfaces, ...options), + connect: (app, client, ...options) => + connect(app, client, ...options), + connectAndInspect: (app, client, interfaces, ...options) => + connectAndInspect(app, client, interfaces, ...options), }; diff --git a/lib/server.js b/lib/server.js index aef952ad..124f54f5 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1,5 +1,7 @@ 'use strict'; +const semver = require('semver'); + const apps = require('./applications'); const Connection = require('./connection'); const SimpleAuthPolicy = require('./simple-auth-policy'); @@ -24,6 +26,16 @@ const initServer = function( applications = apps.createAppsIndex(applications); } + // versions cached for efficient search when provided version is a range + this._cachedVersions = new Map(); + applications.forEach((appVersions, appName) => { + const versions = Array.from(appVersions) + .filter(version => version[0] !== 'latest') + .map(version => [new semver.SemVer(version[0]), version[1]]) + .sort((a, b) => semver.rcompare(a[0], b[0])); + this._cachedVersions.set(appName, versions); + }); + if (typeof(authPolicy) === 'number') { heartbeatInterval = authPolicy; authPolicy = null; @@ -84,6 +96,28 @@ class Server { }); } + _getApplication(name, version) { + const appVersions = this.applications.get(name); + if (!appVersions) return null; + if (!version) return appVersions.get('latest'); + // when version is not a range simply return matched + if (semver.valid(version)) return appVersions.get(version); + + // search matching version, first matched will be the latest + try { + const range = new semver.Range(version); + const versions = this._cachedVersions.get(name); + for (let i = 0; i < versions.length; i++) { + // version === [versionCode, app] + const version = versions[i]; + if (range.test(version[0])) return version[1]; + } + } catch (error) { + // ignored + } + return null; + } + // Client connection event handler. // connection - JSTP connection instance // diff --git a/lib/simple-connect-policy.js b/lib/simple-connect-policy.js index dc3b7163..aacdda4f 100644 --- a/lib/simple-connect-policy.js +++ b/lib/simple-connect-policy.js @@ -14,16 +14,18 @@ module.exports = class SimpleConnectPolicy { // Should send handshake message with appropriate credentials // You can get client object provided upon connection creation // with connection.client. + // app - string or object, application to connect to as 'name' or + // 'name@version' or { name, version }, where version + // must be a valid semver range // connection - JSTP connection // callback - callback function that has signature // (error, connection) // - connect(appName, connection, callback) { + connect(app, connection, callback) { connection.handshake( - appName, this.login, this.password, + app, this.login, this.password, (error) => { callback(error, connection); - } - ); + }); } }; diff --git a/lib/tls.js b/lib/tls.js index 01910635..6d8e1a0d 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -57,8 +57,8 @@ module.exports = { Server, createServer: (options, listener) => createServer(options, listener), - connect: (appName, client, ...options) => - connect(appName, client, ...options), - connectAndInspect: (appName, client, interfaces, ...options) => - connectAndInspect(appName, client, interfaces, ...options), + connect: (app, client, ...options) => + connect(app, client, ...options), + connectAndInspect: (app, client, interfaces, ...options) => + connectAndInspect(app, client, interfaces, ...options), }; diff --git a/lib/transport-common.js b/lib/transport-common.js index d56fd6bc..73620726 100644 --- a/lib/transport-common.js +++ b/lib/transport-common.js @@ -13,7 +13,9 @@ const common = require('./common'); // transportClass - class that will be instantiated with rawConnection // // returns function with arguments -// appName - remote application name to connect to +// app - string or object, application to connect to as 'name' or +// 'name@version' or { name, version }, where version +// must be a valid semver range // client - optional client object with following properties: // * application - object, optional (see jstp.Application) // * connectPolicy - function or @@ -26,7 +28,7 @@ const common = require('./common'); // const newConnectFn = ( connFactory, transportClass -) => (appName, client, ...options) => { +) => (app, client, ...options) => { const callback = common.extractCallback(options); connFactory(...options, (error, rawConnection) => { if (error) { @@ -51,7 +53,7 @@ const newConnectFn = ( client.connectPolicy.connect.bind(client.connectPolicy); } const connection = new Connection(transport, null, client); - client.connectPolicy(appName, connection, (error, connection) => { + client.connectPolicy(app, connection, (error, connection) => { if (error) { callback(error, connection); return; @@ -70,9 +72,9 @@ const newConnectFn = ( // const newConnectAndInspectFn = (connFactory, transportClass) => { const connect = newConnectFn(connFactory, transportClass); - return (appName, client, interfaces, ...options) => { + return (app, client, interfaces, ...options) => { const callback = common.extractCallback(options); - connect(appName, client, ...options, (error, connection) => { + connect(app, client, ...options, (error, connection) => { if (error) { callback(error); return; diff --git a/lib/ws-browser.js b/lib/ws-browser.js index 47a4b29e..dae8224b 100644 --- a/lib/ws-browser.js +++ b/lib/ws-browser.js @@ -113,8 +113,8 @@ const connectAndInspect = module.exports = { Transport, - connect: (appName, client, url, callback) => - connect(appName, client, url, callback), - connectAndInspect: (appName, client, interfaces, url, callback) => - connectAndInspect(appName, client, interfaces, url, callback), + connect: (app, client, url, callback) => + connect(app, client, url, callback), + connectAndInspect: (app, client, interfaces, url, callback) => + connectAndInspect(app, client, interfaces, url, callback), }; diff --git a/lib/ws-internal.js b/lib/ws-internal.js index 4be82962..58f61294 100644 --- a/lib/ws-internal.js +++ b/lib/ws-internal.js @@ -147,14 +147,14 @@ module.exports = { // see transportCommon.newConnectFn // webSocketConfig - web socket client configuration // (see connectionFactory) - connect: (appName, client, webSocketConfig, ...options) => - connect(appName, client, webSocketConfig, ...options), + connect: (app, client, webSocketConfig, ...options) => + connect(app, client, webSocketConfig, ...options), // see transportCommon.newConnectAndInspectFn // webSocketConfig - web socket client configuration // (see connectionFactory) connectAndInspect: ( - appName, client, interfaces, webSocketConfig, ...options + app, client, interfaces, webSocketConfig, ...options ) => connectAndInspect( - appName, client, interfaces, webSocketConfig, ...options + app, client, interfaces, webSocketConfig, ...options ), }; diff --git a/package-lock.json b/package-lock.json index 0b4cb66c..d3533930 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1158,7 +1158,7 @@ "color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "integrity": "sha1-k4NDeaHMmgxh+C9S8NBDIiUb1aI=", "dev": true }, "combined-stream": { @@ -1672,7 +1672,7 @@ "pluralize": "4.0.0", "progress": "2.0.0", "require-uncached": "1.0.3", - "semver": "5.3.0", + "semver": "5.4.1", "strip-json-comments": "2.0.1", "table": "4.0.1", "text-table": "0.2.0" @@ -3076,7 +3076,7 @@ "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", "dev": true, "requires": { "fs.realpath": "1.0.0", @@ -3109,7 +3109,7 @@ "globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=", "dev": true }, "globby": { @@ -4065,7 +4065,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", "dev": true, "requires": { "brace-expansion": "1.1.8" @@ -4155,7 +4155,7 @@ "requires": { "hosted-git-info": "2.5.0", "is-builtin-module": "1.0.0", - "semver": "5.3.0", + "semver": "5.4.1", "validate-npm-package-license": "3.0.1" } }, @@ -6169,7 +6169,7 @@ "randomatic": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "integrity": "sha1-x6vpzIuHwLqodrGf3oP9RkeX44w=", "dev": true, "requires": { "is-number": "3.0.0", @@ -6632,10 +6632,9 @@ "dev": true }, "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "dev": true + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" }, "set-blocking": { "version": "2.0.0", @@ -6986,7 +6985,7 @@ "tap-parser": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-5.4.0.tgz", - "integrity": "sha512-BIsIaGqv7uTQgTW1KLTMNPSEQf4zDDPgYOBRdgOfuB+JFOLRBfEu6cLa/KvMvmqggu1FKXDfitjLwsq4827RvA==", + "integrity": "sha1-aQfolyXXt/pq5B7ixGTD20MYiuw=", "dev": true, "requires": { "events-to-array": "1.1.2", diff --git a/package.json b/package.json index 226abb69..eb3407be 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "./lib/simple-auth-policy.js": false }, "dependencies": { + "semver": "^5.4.1", "uuid": "^3.1.0", "websocket": "^1.0.24" }, diff --git a/test/node/api-versions.js b/test/node/api-versions.js new file mode 100644 index 00000000..54b09a95 --- /dev/null +++ b/test/node/api-versions.js @@ -0,0 +1,230 @@ +'use strict'; + +const test = require('tap'); + +const jstp = require('../..'); + +const app = require('../fixtures/application'); + +const interfacesV1 = { + calculator: { + answer(connection, callback) { + callback(null, 42); + }, + }, +}; + +const interfacesV2 = { + calculator: { + answer(connection, callback) { + callback(null, 24); + }, + }, +}; + +const interfacesVLatest = { + calculator: { + answer(connection, callback) { + callback(null, 13); + }, + }, +}; + +const appV1 = new jstp.Application(app.name, interfacesV1, '1.0.0'); +const appV2 = new jstp.Application(app.name, interfacesV2, '2.0.0'); +const appVlatest = new jstp.Application(app.name, interfacesVLatest); + +let server; +let connection; + +test.afterEach((done) => { + if (connection) { + connection.close(); + connection.once('close', () => { + connection = null; + + if (server) server.close(); + done(); + }); + } else { + if (server) server.close(); + done(); + } +}); + +test.test('must allow to specify version in application name', (test) => { + const appV1 = new jstp.Application('app@1.0.0', interfacesV1); + test.strictSame(appV1.version, '1.0.0'); + test.end(); +}); + +test.test('must call latest version if no version specified', (test) => { + const serverConfig = { + applications: [appV1, appV2, appVlatest], authPolicy: app.authCallback, + }; + server = jstp.net.createServer(serverConfig); + server.listen(0, () => { + const port = server.address().port; + const appLatest = { name: app.name }; + jstp.net.connect(appLatest, null, port, (error, conn) => { + connection = conn; + test.assertNot(error, 'connect must not return an error'); + connection.callMethod('calculator', 'answer', [], (error, result) => { + test.assertNot(error, 'callMethod must not return an error'); + test.strictSame(result, 13); + test.end(); + }); + }); + }); +}); + +test.test('must call specific version when specified (v1)', (test) => { + const serverConfig = { + applications: [appV1, appV2, appVlatest], authPolicy: app.authCallback, + }; + server = jstp.net.createServer(serverConfig); + server.listen(0, () => { + const port = server.address().port; + const appV1 = { name: app.name, version: '1' }; + jstp.net.connect(appV1, null, port, (error, conn) => { + connection = conn; + test.assertNot(error, 'connect must not return an error'); + connection.callMethod('calculator', 'answer', [], (error, result) => { + test.assertNot(error, 'callMethod must not return an error'); + test.strictSame(result, 42); + test.end(); + }); + }); + }); +}); + +test.test('must call specific version when specified (v2)', (test) => { + const serverConfig = { + applications: [appV1, appV2, appVlatest], authPolicy: app.authCallback, + }; + server = jstp.net.createServer(serverConfig); + server.listen(0, () => { + const port = server.address().port; + const appV2 = { name: app.name, version: '2' }; + jstp.net.connect(appV2, null, port, (error, conn) => { + connection = conn; + test.assertNot(error, 'connect must not return an error'); + connection.callMethod('calculator', 'answer', [], (error, result) => { + test.assertNot(error, 'callMethod must not return an error'); + test.strictSame(result, 24); + test.end(); + }); + }); + }); +}); + +test.test('must handle version ranges (^1.0.0)', (test) => { + const serverConfig = { + applications: [appV1, appV2, appVlatest], authPolicy: app.authCallback, + }; + server = jstp.net.createServer(serverConfig); + server.listen(0, () => { + const port = server.address().port; + const appV1Compatible = { name: app.name, version: '^1.0.0' }; + // must connect to appV1 + jstp.net.connect(appV1Compatible, null, port, (error, conn) => { + connection = conn; + test.assertNot(error, 'connect must not return an error'); + connection.callMethod('calculator', 'answer', [], (error, result) => { + test.assertNot(error, 'callMethod must not return an error'); + test.strictSame(result, 42); + test.end(); + }); + }); + }); +}); + +test.test('must handle version ranges (>1.0.0)', (test) => { + const serverConfig = { + applications: [appV1, appV2, appVlatest], authPolicy: app.authCallback, + }; + server = jstp.net.createServer(serverConfig); + server.listen(0, () => { + const port = server.address().port; + const appV1Higher = { name: app.name, version: '>1.0.0' }; + // must connect to appV2 + jstp.net.connect(appV1Higher, null, port, (error, conn) => { + connection = conn; + test.assertNot(error, 'connect must not return an error'); + connection.callMethod('calculator', 'answer', [], (error, result) => { + test.assertNot(error, 'callMethod must not return an error'); + test.strictSame(result, 24); + test.end(); + }); + }); + }); +}); + +test.test('must return an error on connect to nonexistent version', (test) => { + const serverConfig = + { applications: [appV1, appV2, appVlatest], authPolicy: app.authCallback }; + server = jstp.net.createServer(serverConfig); + server.listen(0, () => { + const port = server.address().port; + const nonexistentApp = { name: app.name, version: '9999' }; + jstp.net.connect(nonexistentApp, null, port, (error, conn) => { + connection = conn; + test.assert(error, 'connect must return an error'); + test.equal(error.code, jstp.ERR_APP_NOT_FOUND, + 'error must be an ERR_APP_NOT_FOUND'); + test.end(); + }); + }); +}); + +test.test('must return an error on connect to invalid version', (test) => { + const serverConfig = + { applications: [appV1, appV2, appVlatest], authPolicy: app.authCallback }; + server = jstp.net.createServer(serverConfig); + server.listen(0, () => { + const port = server.address().port; + const application = { name: app.name, version: '__invalid_version__' }; + jstp.net.connect(application, null, port, (error, conn) => { + connection = conn; + test.assert(error, 'connect must return an error'); + test.equal(error.message, 'Invalid semver version range'); + test.end(); + }); + }); +}); + +test.test('must set biggest version as latest versions', (test) => { + const serverConfig = + { applications: [appV1, appV2], authPolicy: app.authCallback }; + server = jstp.net.createServer(serverConfig); + server.listen(0, () => { + const port = server.address().port; + jstp.net.connect(app.name, null, port, (error, conn) => { + connection = conn; + test.assertNot(error, 'connect must not return an error'); + connection.callMethod('calculator', 'answer', [], (error, result) => { + test.assertNot(error, 'callMethod must not return an error'); + test.strictSame(result, 24); + test.end(); + }); + }); + }); +}); + +test.test('must throw an error on invalid version in application name', + (test) => { + test.throws(() => { + new jstp.Application('app@__invalid__', interfacesV1); + }, TypeError, 'Invalid semver version'); + test.end(); + } +); + +test.test('must throw an error on invalid version in application parameter', + (test) => { + test.throws(() => { + new jstp.Application('app', interfacesV1, '__invalid__'); + }, TypeError, 'Invalid semver version'); + test.end(); + } +);