From 599da158b54022a3365e4a685fd7dc9fe9fe79fe Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 5 Feb 2018 11:31:27 +0100 Subject: [PATCH] Bugfix Foxx API tests (#4446) --- .../Foxx/api_foxx_configuration_replace.md | 3 + .../Foxx/api_foxx_configuration_update.md | 3 + .../Foxx/api_foxx_dependencies_replace.md | 3 + .../Rest/Foxx/api_foxx_dependencies_update.md | 3 + .../Rest/Foxx/api_foxx_service_install.md | 3 +- .../Rest/Foxx/api_foxx_service_list.md | 5 + .../Rest/Foxx/api_foxx_service_replace.md | 3 +- .../Rest/Foxx/api_foxx_service_upgrade.md | 3 +- js/apps/system/_api/foxx/APP/index.js | 3 +- js/client/tests/shell/shell-foxx-api-spec.js | 619 +++++++++++++++++- js/common/test-data/apps/echo-script/echo.js | 2 + .../test-data/apps/echo-script/manifest.json | 5 + .../test-data/apps/itzpapalotl/itzpapalotl.js | 45 +- .../test-data/apps/itzpapalotl/manifest.json | 16 +- .../controller.js | 7 - .../minimal-working-setup-teardown/index.js | 7 + .../manifest.json | 7 +- .../minimal-working-setup-teardown/setup.js | 10 +- .../teardown.js | 8 +- .../test-data/apps/service-service/index.js | 19 + .../apps/with-configuration/manifest.json | 12 + .../apps/with-dependencies/manifest.json | 10 + .../test-data/apps/with-tests/manifest.json | 3 + js/common/test-data/apps/with-tests/test1.js | 20 + js/common/test-data/apps/with-tests/test2.js | 20 + js/server/modules/@arangodb/foxx/manager.js | 34 +- js/server/modules/@arangodb/foxx/service.js | 5 +- 27 files changed, 806 insertions(+), 72 deletions(-) create mode 100644 js/common/test-data/apps/echo-script/echo.js create mode 100644 js/common/test-data/apps/echo-script/manifest.json delete mode 100644 js/common/test-data/apps/minimal-working-setup-teardown/controller.js create mode 100644 js/common/test-data/apps/minimal-working-setup-teardown/index.js create mode 100644 js/common/test-data/apps/service-service/index.js create mode 100644 js/common/test-data/apps/with-configuration/manifest.json create mode 100644 js/common/test-data/apps/with-dependencies/manifest.json create mode 100644 js/common/test-data/apps/with-tests/manifest.json create mode 100644 js/common/test-data/apps/with-tests/test1.js create mode 100644 js/common/test-data/apps/with-tests/test2.js diff --git a/Documentation/DocuBlocks/Rest/Foxx/api_foxx_configuration_replace.md b/Documentation/DocuBlocks/Rest/Foxx/api_foxx_configuration_replace.md index 04d67d69ca..ab7b409e21 100644 --- a/Documentation/DocuBlocks/Rest/Foxx/api_foxx_configuration_replace.md +++ b/Documentation/DocuBlocks/Rest/Foxx/api_foxx_configuration_replace.md @@ -17,4 +17,7 @@ Any omitted options will be reset to their default values or marked as unconfigu @RESTQUERYPARAM{mount,string,required} Mount path of the installed service. +@RESTRETURNCODE{200} +Returned if the request was sucessful. + @endDocuBlock diff --git a/Documentation/DocuBlocks/Rest/Foxx/api_foxx_configuration_update.md b/Documentation/DocuBlocks/Rest/Foxx/api_foxx_configuration_update.md index b937b3aa99..f35ebaee06 100644 --- a/Documentation/DocuBlocks/Rest/Foxx/api_foxx_configuration_update.md +++ b/Documentation/DocuBlocks/Rest/Foxx/api_foxx_configuration_update.md @@ -17,4 +17,7 @@ Any omitted options will be ignored. @RESTQUERYPARAM{mount,string,required} Mount path of the installed service. +@RESTRETURNCODE{200} +Returned if the request was sucessful. + @endDocuBlock diff --git a/Documentation/DocuBlocks/Rest/Foxx/api_foxx_dependencies_replace.md b/Documentation/DocuBlocks/Rest/Foxx/api_foxx_dependencies_replace.md index 2bfca4f150..5cbea29fa7 100644 --- a/Documentation/DocuBlocks/Rest/Foxx/api_foxx_dependencies_replace.md +++ b/Documentation/DocuBlocks/Rest/Foxx/api_foxx_dependencies_replace.md @@ -17,4 +17,7 @@ Any omitted dependencies will be disabled. @RESTQUERYPARAM{mount,string,required} Mount path of the installed service. +@RESTRETURNCODE{200} +Returned if the request was sucessful. + @endDocuBlock diff --git a/Documentation/DocuBlocks/Rest/Foxx/api_foxx_dependencies_update.md b/Documentation/DocuBlocks/Rest/Foxx/api_foxx_dependencies_update.md index fd16c28a95..03b08001b7 100644 --- a/Documentation/DocuBlocks/Rest/Foxx/api_foxx_dependencies_update.md +++ b/Documentation/DocuBlocks/Rest/Foxx/api_foxx_dependencies_update.md @@ -17,4 +17,7 @@ Any omitted dependencies will be ignored. @RESTQUERYPARAM{mount,string,required} Mount path of the installed service. +@RESTRETURNCODE{200} +Returned if the request was sucessful. + @endDocuBlock diff --git a/Documentation/DocuBlocks/Rest/Foxx/api_foxx_service_install.md b/Documentation/DocuBlocks/Rest/Foxx/api_foxx_service_install.md index c2ea52a031..ab5bcc6ba9 100644 --- a/Documentation/DocuBlocks/Rest/Foxx/api_foxx_service_install.md +++ b/Documentation/DocuBlocks/Rest/Foxx/api_foxx_service_install.md @@ -28,7 +28,8 @@ in the field `main` of the service manifest. If *source* is a URL, the URL must be reachable from the server. If *source* is a file system path, the path will be resolved on the server. -In either case the path or URL is expected to resolve to a zip bundle. +In either case the path or URL is expected to resolve to a zip bundle, +JavaScript file or (in case of a file system path) directory. Note that when using file system paths in a cluster with multiple coordinators the file system path must resolve to equivalent files on every coordinator. diff --git a/Documentation/DocuBlocks/Rest/Foxx/api_foxx_service_list.md b/Documentation/DocuBlocks/Rest/Foxx/api_foxx_service_list.md index 5ca8d834f2..e782ec2196 100644 --- a/Documentation/DocuBlocks/Rest/Foxx/api_foxx_service_list.md +++ b/Documentation/DocuBlocks/Rest/Foxx/api_foxx_service_list.md @@ -18,6 +18,11 @@ Additionally the object may contain the following attributes if they have been s - *name*: a string identifying the service type - *version*: a semver-compatible version string +@RESTQUERYPARAMETERS + +@RESTQUERYPARAM{excludeSystem,boolean,optional} +Whether or not system services should be excluded from the result. + @RESTRETURNCODES @RESTRETURNCODE{200} diff --git a/Documentation/DocuBlocks/Rest/Foxx/api_foxx_service_replace.md b/Documentation/DocuBlocks/Rest/Foxx/api_foxx_service_replace.md index fe0260bea9..f088c42e53 100644 --- a/Documentation/DocuBlocks/Rest/Foxx/api_foxx_service_replace.md +++ b/Documentation/DocuBlocks/Rest/Foxx/api_foxx_service_replace.md @@ -33,7 +33,8 @@ in the field `main` of the service manifest. If *source* is a URL, the URL must be reachable from the server. If *source* is a file system path, the path will be resolved on the server. -In either case the path or URL is expected to resolve to a zip bundle. +In either case the path or URL is expected to resolve to a zip bundle, +JavaScript file or (in case of a file system path) directory. Note that when using file system paths in a cluster with multiple coordinators the file system path must resolve to equivalent files on every coordinator. diff --git a/Documentation/DocuBlocks/Rest/Foxx/api_foxx_service_upgrade.md b/Documentation/DocuBlocks/Rest/Foxx/api_foxx_service_upgrade.md index ac43debedf..1fef982fcf 100644 --- a/Documentation/DocuBlocks/Rest/Foxx/api_foxx_service_upgrade.md +++ b/Documentation/DocuBlocks/Rest/Foxx/api_foxx_service_upgrade.md @@ -33,7 +33,8 @@ in the field `main` of the service manifest. If *source* is a URL, the URL must be reachable from the server. If *source* is a file system path, the path will be resolved on the server. -In either case the path or URL is expected to resolve to a zip bundle. +In either case the path or URL is expected to resolve to a zip bundle, +JavaScript file or (in case of a file system path) directory. Note that when using file system paths in a cluster with multiple coordinators the file system path must resolve to equivalent files on every coordinator. diff --git a/js/apps/system/_api/foxx/APP/index.js b/js/apps/system/_api/foxx/APP/index.js index 87536ece70..a439e9526b 100644 --- a/js/apps/system/_api/foxx/APP/index.js +++ b/js/apps/system/_api/foxx/APP/index.js @@ -130,6 +130,7 @@ router.post(prepareServiceRequestBody, (req, res) => { )); const service = FoxxManager.lookupService(mount); res.json(serviceToJson(service)); + res.status(201); }) .body(schemas.service, ['application/javascript', 'application/zip', 'multipart/form-data', 'application/json']) .queryParam('mount', schemas.mount) @@ -284,7 +285,7 @@ depsRouter.get((req, res) => { depsRouter.patch((req, res) => { const warnings = FoxxManager.setDependencies(req.service.mount, { dependencies: req.body, - replace: true + replace: false }); const values = req.service.getDependencies(req.queryParams.minimal); if (req.queryParams.minimal) { diff --git a/js/client/tests/shell/shell-foxx-api-spec.js b/js/client/tests/shell/shell-foxx-api-spec.js index be7d4611eb..b74baa0236 100644 --- a/js/client/tests/shell/shell-foxx-api-spec.js +++ b/js/client/tests/shell/shell-foxx-api-spec.js @@ -4,9 +4,11 @@ const expect = require('chai').expect; const FoxxManager = require('@arangodb/foxx/manager'); const request = require('@arangodb/request'); +const util = require('@arangodb/util'); const fs = require('fs'); const internal = require('internal'); -const basePath = fs.makeAbsolute(fs.join(internal.startupPath, 'common', 'test-data', 'apps', 'headers')); +const path = require('path'); +const basePath = path.resolve(internal.startupPath, 'common', 'test-data', 'apps', 'headers'); const arangodb = require('@arangodb'); const db = arangodb.db; const aql = arangodb.aql; @@ -138,3 +140,618 @@ describe('FoxxApi commit', function () { expect(bundles).to.equal(1); }); }); + +describe('Foxx service', () => { + const mount = '/foxx-crud-test'; + const basePath = path.resolve(internal.startupPath, 'common', 'test-data', 'apps', 'minimal-working-service'); + const itzPath = path.resolve(internal.startupPath, 'common', 'test-data', 'apps', 'itzpapalotl'); + var utils = require('@arangodb/foxx/manager-utils'); + const servicePath = utils.zipDirectory(basePath); + + const serviceServiceMount = '/foxx-crud-test-download'; + const serviceServicePath = path.resolve(internal.startupPath, 'common', 'test-data', 'apps', 'service-service', 'index.js'); + + beforeEach(() => { + FoxxManager.install(serviceServicePath, serviceServiceMount); + }); + + afterEach(() => { + try { + FoxxManager.uninstall(serviceServiceMount, {force: true}); + } catch (e) {} + }); + + afterEach(() => { + try { + FoxxManager.uninstall(mount, {force: true}); + } catch (e) {} + }); + + const cases = [ + { + name: 'localJsFile', + request: { + qs: { + mount + }, + body: { + source: path.resolve(basePath, 'index.js') + }, + json: true + } + }, + { + name: 'localZipFile', + request: { + qs: { + mount + }, + body: { + source: servicePath + }, + json: true + } + }, + { + name: 'localDir', + request: { + qs: { + mount + }, + body: { + source: basePath + }, + json: true + } + }, + { + name: 'jsBuffer', + request: { + qs: { + mount + }, + body: fs.readFileSync(path.resolve(basePath, 'index.js')), + contentType: 'application/javascript' + } + }, + { + name: 'zipBuffer', + request: { + qs: { + mount + }, + body: fs.readFileSync(servicePath), + contentType: 'application/zip' + } + }, + { + name: 'remoteJsFile', + request: { + qs: { + mount + }, + body: { + source: `${origin}/_db/${db._name()}${serviceServiceMount}/js` + }, + json: true + } + }, + { + name: 'remoteZipFile', + request: { + qs: { + mount + }, + body: { + source: `${origin}/_db/${db._name()}${serviceServiceMount}/zip` + }, + json: true + } + } + ]; + for (const c of cases) { + it(`installed via ${c.name} should be available`, () => { + const installResp = request.post('/_api/foxx', c.request); + expect(installResp.status).to.equal(201); + const resp = request.get(c.request.qs.mount); + expect(resp.json).to.eql({hello: 'world'}); + }); + + it(`replaced via ${c.name} should be available`, () => { + FoxxManager.install(itzPath, c.request.qs.mount); + const replaceResp = request.put('/_api/foxx/service', c.request); + expect(replaceResp.status).to.equal(200); + const resp = request.get(c.request.qs.mount); + expect(resp.json).to.eql({hello: 'world'}); + }); + + it(`upgrade via ${c.name} should be available`, () => { + FoxxManager.install(itzPath, c.request.qs.mount); + const upgradeResp = request.patch('/_api/foxx/service', c.request); + expect(upgradeResp.status).to.equal(200); + const resp = request.get(c.request.qs.mount); + expect(resp.json).to.eql({hello: 'world'}); + }); + } + + it('uninstalled should not be available', () => { + FoxxManager.install(basePath, mount); + const delResp = request.delete('/_api/foxx/service', {qs: {mount}}); + expect(delResp.status).to.equal(204); + expect(delResp.body).to.equal(''); + const resp = request.get(mount); + expect(resp.status).to.equal(404); + }); + + const confPath = path.resolve(internal.startupPath, 'common', 'test-data', 'apps', 'with-configuration'); + + it('empty configuration should be available', () => { + FoxxManager.install(basePath, mount); + const resp = request.get('/_api/foxx/configuration', {qs: {mount}}); + expect(resp.status).to.equal(200); + expect(resp.json).to.eql({}); + }); + + it('configuration should be available', () => { + FoxxManager.install(confPath, mount); + const resp = request.get('/_api/foxx/configuration', {qs: {mount}}); + expect(resp.status).to.equal(200); + expect(resp.json).to.have.property('test1'); + expect(resp.json.test1).to.not.have.property('current'); + expect(resp.json).to.have.property('test2'); + expect(resp.json.test2).to.not.have.property('current'); + }); + + it('configuration should be available after update', () => { + FoxxManager.install(confPath, mount); + const updateResp = request.patch('/_api/foxx/configuration', { + qs: { + mount + }, + body: { + test1: 'test' + }, + json: true + }); + expect(updateResp.status).to.equal(200); + expect(updateResp.json).to.have.property('values'); + expect(updateResp.json.values).to.have.property('test1', 'test'); + expect(updateResp.json.values).to.not.have.property('test2'); + expect(updateResp.json).to.not.have.property('warnings'); + const resp = request.get('/_api/foxx/configuration', {qs: {mount}}); + expect(resp.status).to.equal(200); + expect(resp.json).to.have.property('test1'); + expect(resp.json.test1).to.have.property('current', 'test'); + expect(resp.json).to.have.property('test2'); + expect(resp.json.test2).to.not.have.property('current'); + }); + + it('configuration should be available after replace', () => { + FoxxManager.install(confPath, mount); + const replaceResp = request.put('/_api/foxx/configuration', { + qs: { + mount + }, + body: { + test1: 'test' + }, + json: true + }); + expect(replaceResp.status).to.equal(200); + expect(replaceResp.json).to.have.property('values'); + expect(replaceResp.json.values).to.have.property('test1', 'test'); + expect(replaceResp.json.values).to.not.have.property('test2'); + expect(replaceResp.json).to.have.property('warnings'); + expect(replaceResp.json.warnings).to.have.property('test2', 'is required'); + const resp = request.get('/_api/foxx/configuration', {qs: {mount}}); + expect(resp.status).to.equal(200); + expect(resp.json).to.have.property('test1'); + expect(resp.json.test1).to.have.property('current', 'test'); + expect(resp.json).to.have.property('test2'); + expect(resp.json.test2).to.not.have.property('current'); + }); + + it('configuration should be merged after update', () => { + FoxxManager.install(confPath, mount); + const replaceResp = request.put('/_api/foxx/configuration', { + qs: { + mount + }, + body: { + test2: 'test2' + }, + json: true + }); + expect(replaceResp.status).to.equal(200); + const updateResp = request.patch('/_api/foxx/configuration', { + qs: { + mount + }, + body: { + test1: 'test1' + }, + json: true + }); + expect(updateResp.status).to.equal(200); + const resp = request.get('/_api/foxx/configuration', {qs: {mount}}); + expect(resp.status).to.equal(200); + expect(resp.json).to.have.property('test1'); + expect(resp.json.test1).to.have.property('current', 'test1'); + expect(resp.json).to.have.property('test2'); + expect(resp.json.test2).to.have.property('current', 'test2'); + }); + + it('configuration should be overwritten after replace', () => { + FoxxManager.install(confPath, mount); + const updateResp = request.patch('/_api/foxx/configuration', { + qs: { + mount + }, + body: { + test2: 'test2' + }, + json: true + }); + expect(updateResp.status).to.equal(200); + const replaceResp = request.put('/_api/foxx/configuration', { + qs: { + mount + }, + body: { + test1: 'test' + }, + json: true + }); + expect(replaceResp.status).to.equal(200); + const resp = request.get('/_api/foxx/configuration', {qs: {mount}}); + expect(resp.status).to.equal(200); + expect(resp.json).to.have.property('test1'); + expect(resp.json.test1).to.have.property('current', 'test'); + expect(resp.json).to.have.property('test2'); + expect(resp.json.test2).to.not.have.property('current'); + }); + + const depPath = path.resolve(internal.startupPath, 'common', 'test-data', 'apps', 'with-dependencies'); + + it('empty configuration should be available', () => { + FoxxManager.install(basePath, mount); + const resp = request.get('/_api/foxx/dependencies', {qs: {mount}}); + expect(resp.status).to.equal(200); + expect(resp.json).to.eql({}); + }); + + it('dependencies should be available', () => { + FoxxManager.install(depPath, mount); + const resp = request.get('/_api/foxx/dependencies', {qs: {mount}}); + expect(resp.status).to.equal(200); + expect(resp.json).to.have.property('test1'); + expect(resp.json.test1).to.not.have.property('current'); + expect(resp.json).to.have.property('test2'); + expect(resp.json.test2).to.not.have.property('current'); + }); + + it('dependencies should be available after update', () => { + FoxxManager.install(depPath, mount); + const updateResp = request.patch('/_api/foxx/dependencies', { + qs: { + mount + }, + body: { + test1: '/test' + }, + json: true + }); + expect(updateResp.status).to.equal(200); + expect(updateResp.json).to.have.property('values'); + expect(updateResp.json.values).to.have.property('test1', '/test'); + expect(updateResp.json.values).not.to.have.property('test2'); + expect(updateResp.json).to.not.have.property('warnings'); + const resp = request.get('/_api/foxx/dependencies', {qs: {mount}}); + expect(resp.status).to.equal(200); + expect(resp.json).to.have.property('test1'); + expect(resp.json.test1).to.have.property('current', '/test'); + expect(resp.json).to.have.property('test2'); + expect(resp.json.test2).to.not.have.property('current'); + }); + + it('dependencies should be available after replace', () => { + FoxxManager.install(depPath, mount); + const replaceResp = request.put('/_api/foxx/dependencies', { + qs: { + mount + }, + body: { + test1: '/test' + }, + json: true + }); + expect(replaceResp.status).to.equal(200); + expect(replaceResp.json).to.have.property('values'); + expect(replaceResp.json.values).to.have.property('test1', '/test'); + expect(replaceResp.json.values).to.not.have.property('test2'); + expect(replaceResp.json).to.have.property('warnings'); + expect(replaceResp.json.warnings).to.have.property('test2', 'is required'); + const resp = request.get('/_api/foxx/dependencies', {qs: {mount}}); + expect(resp.status).to.equal(200); + expect(resp.json).to.have.property('test1'); + expect(resp.json.test1).to.have.property('current', '/test'); + expect(resp.json).to.have.property('test2'); + expect(resp.json.test2).to.not.have.property('current'); + }); + + it('dependencies should be merged after update', () => { + FoxxManager.install(depPath, mount); + const replaceResp = request.put('/_api/foxx/dependencies', { + qs: { + mount + }, + body: { + test2: '/test2' + }, + json: true + }); + expect(replaceResp.status).to.equal(200); + expect(replaceResp.json).to.have.property('values'); + expect(replaceResp.json.values).to.have.property('test2', '/test2'); + expect(replaceResp.json.values).to.not.have.property('test1'); + expect(replaceResp.json).to.have.property('warnings'); + expect(replaceResp.json.warnings).to.have.property('test1', 'is required'); + const updateResp = request.patch('/_api/foxx/dependencies', { + qs: { + mount + }, + body: { + test1: '/test1' + }, + json: true + }); + expect(updateResp.status).to.equal(200); + expect(updateResp.json).to.have.property('values'); + expect(updateResp.json.values).to.have.property('test1', '/test1'); + expect(updateResp.json.values).to.have.property('test2', '/test2'); + const resp = request.get('/_api/foxx/dependencies', {qs: {mount}}); + expect(resp.status).to.equal(200); + expect(resp.json).to.have.property('test1'); + expect(resp.json.test1).to.have.property('current', '/test1'); + expect(resp.json).to.have.property('test2'); + expect(resp.json.test2).to.have.property('current', '/test2'); + }); + + it('dependencies should be overwritten after replace', () => { + FoxxManager.install(depPath, mount); + const updateResp = request.patch('/_api/foxx/dependencies', { + qs: { + mount + }, + body: { + test2: '/test2' + }, + json: true + }); + expect(updateResp.status).to.equal(200); + expect(updateResp.json).to.have.property('values'); + expect(updateResp.json).to.not.have.property('warnings'); + expect(updateResp.json.values).to.have.property('test2', '/test2'); + expect(updateResp.json.values).to.not.have.property('test1'); + const replaceResp = request.put('/_api/foxx/dependencies', { + qs: { + mount + }, + body: { + test1: '/test' + }, + json: true + }); + expect(replaceResp.status).to.equal(200); + expect(replaceResp.json).to.have.property('values'); + expect(replaceResp.json.values).to.have.property('test1', '/test'); + expect(replaceResp.json.values).to.not.have.property('test2'); + expect(replaceResp.json).to.have.property('warnings'); + expect(replaceResp.json.warnings).to.have.property('test2', 'is required'); + const resp = request.get('/_api/foxx/dependencies', {qs: {mount}}); + expect(resp.status).to.equal(200); + expect(resp.json).to.have.property('test1'); + expect(resp.json.test1).to.have.property('current', '/test'); + expect(resp.json).to.have.property('test2'); + expect(resp.json.test2).to.not.have.property('current'); + }); + + it('should be downloadable', () => { + FoxxManager.install(basePath, mount); + const resp = request.post('/_api/foxx/download', { + qs: {mount}, + encoding: null + }); + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('application/zip'); + expect(util.isZipBuffer(resp.body)).to.equal(true); + }); + + it('list should allow excluding system services', () => { + FoxxManager.install(basePath, mount); + const withSystem = request.get('/_api/foxx'); + const withoutSystem = request.get('/_api/foxx', {qs: {excludeSystem: true}}); + const numSystemWithSystem = withSystem.json.map(service => service.mount).filter(mount => mount.startsWith('/_')).length; + const numSystemWithoutSystem = withoutSystem.json.map(service => service.mount).filter(mount => mount.startsWith('/_')).length; + expect(numSystemWithSystem).to.above(0); + expect(numSystemWithSystem).to.equal(withSystem.json.length - withoutSystem.json.length); + expect(numSystemWithoutSystem).to.equal(0); + }); + + it('should be contained in service list', () => { + FoxxManager.install(basePath, mount); + const resp = request.get('/_api/foxx'); + const service = resp.json.find(service => service.mount === mount); + expect(service).to.have.property('name', 'minimal-working-manifest'); + expect(service).to.have.property('version', '0.0.0'); + expect(service).to.have.property('provides'); + expect(service.provides).to.eql({}); + expect(service).to.have.property('development', false); + expect(service).to.have.property('legacy', false); + }); + + it('informations should be returned', () => { + FoxxManager.install(basePath, mount); + const resp = request.get('/_api/foxx/service', {qs: {mount}}); + const service = resp.json; + expect(service).to.have.property('mount', mount); + expect(service).to.have.property('name', 'minimal-working-manifest'); + expect(service).to.have.property('version', '0.0.0'); + expect(service).to.have.property('development', false); + expect(service).to.have.property('legacy', false); + expect(service).to.have.property('manifest'); + expect(service.manifest).to.be.an('object'); + expect(service).to.have.property('options'); + expect(service.options).to.be.an('object'); + expect(service).to.have.property('checksum'); + expect(service.checksum).to.be.a('string'); + }); + + const scriptPath = path.resolve(internal.startupPath, 'common', 'test-data', 'apps', 'minimal-working-setup-teardown'); + + it('list of scripts should be available', () => { + FoxxManager.install(scriptPath, mount); + const resp = request.get('/_api/foxx/scripts', {qs: {mount}}); + expect(resp.status).to.equal(200); + expect(resp.json).to.have.property('setup', 'Setup'); + expect(resp.json).to.have.property('teardown', 'Teardown'); + }); + + it('script should be available', () => { + FoxxManager.install(scriptPath, mount); + const col = `${mount}_setup_teardown`.replace(/\//, '').replace(/-/g, '_'); + expect(db._collection(col)).to.be.an('object'); + const resp = request.post('/_api/foxx/scripts/teardown', { + qs: {mount}, + body: {}, + json: true + }); + expect(resp.status).to.equal(200); + db._flushCache(); + expect(db._collection(col)).to.equal(null); + }); + + it('non-existing script should not be available', () => { + FoxxManager.install(scriptPath, mount); + const resp = request.post('/_api/foxx/scripts/no', { + qs: {mount}, + body: {}, + json: true + }); + expect(resp.status).to.equal(400); + }); + + const echoPath = path.resolve(internal.startupPath, 'common', 'test-data', 'apps', 'echo-script'); + + it('should pass argv to script and return exports', () => { + FoxxManager.install(echoPath, mount); + const argv = {hello: 'world'}; + const resp = request.post('/_api/foxx/scripts/echo', { + qs: {mount}, + body: argv, + json: true + }); + expect(resp.json).to.eql([argv]); + }); + + it('should treat array script argv like any other script argv', () => { + FoxxManager.install(echoPath, mount); + const argv = ['yes', 'please']; + const resp = request.post('/_api/foxx/scripts/echo', { + qs: {mount}, + body: argv, + json: true + }); + expect(resp.json).to.eql([argv]); + }); + + it('set devmode should enable devmode', () => { + FoxxManager.install(basePath, mount); + const resp = request.get('/_api/foxx/service', { + qs: {mount}, + json: true + }); + expect(resp.json.development).to.equal(false); + const devResp = request.post('/_api/foxx/development', { + qs: {mount}, + json: true + }); + expect(devResp.json.development).to.equal(true); + const respAfter = request.get('/_api/foxx/service', { + qs: {mount}, + json: true + }); + expect(respAfter.json.development).to.equal(true); + }); + + it('clear devmode should disable devmode', () => { + FoxxManager.install(basePath, mount, {development: true}); + const resp = request.get('/_api/foxx/service', { + qs: {mount}, + json: true + }); + expect(resp.json.development).to.equal(true); + const devResp = request.delete('/_api/foxx/development', { + qs: {mount}, + json: true + }); + expect(devResp.json.development).to.equal(false); + const respAfter = request.get('/_api/foxx/service', { + qs: {mount}, + json: true + }); + expect(respAfter.json.development).to.equal(false); + }); + + const routes = [ + ['GET', '/_api/foxx/service'], + ['PATCH', '/_api/foxx/service'], + ['PUT', '/_api/foxx/service'], + ['DELETE', '/_api/foxx/service'], + ['GET', '/_api/foxx/configuration'], + ['PATCH', '/_api/foxx/configuration'], + ['PUT', '/_api/foxx/configuration'], + ['GET', '/_api/foxx/dependencies'], + ['PATCH', '/_api/foxx/dependencies'], + ['PUT', '/_api/foxx/dependencies'], + ['POST', '/_api/foxx/development'], + ['DELETE', '/_api/foxx/development'], + ['GET', '/_api/foxx/scripts'], + ['POST', '/_api/foxx/scripts/xxx'], + ['POST', '/_api/foxx/tests'], + ['POST', '/_api/foxx/download'], + ['GET', '/_api/foxx/readme'], + ['GET', '/_api/foxx/swagger'] + ]; + for (const [method, url] of routes) { + it(`should return 400 when mount is omitted for ${method} ${url}`, () => { + const resp = request({ + method, + url, + json: true + }); + expect(resp.status).to.equal(400); + }); + it(`should return 400 when mount is invalid for ${method} ${url}`, () => { + const resp = request({ + method, + url, + qs: {mount: '/dev/null'}, + json: true + }); + expect(resp.status).to.equal(400); + }); + } + + it('tests should run', () => { + const testPath = path.resolve(internal.startupPath, 'common', 'test-data', 'apps', 'with-tests'); + FoxxManager.install(testPath, mount); + const resp = request.post('/_api/foxx/tests', {qs: { mount }}); + expect(resp.status).to.equal(200); + expect(resp.json).to.have.property('stats'); + expect(resp.json).to.have.property('tests'); + expect(resp.json).to.have.property('pending'); + expect(resp.json).to.have.property('failures'); + expect(resp.json).to.have.property('passes'); + }); +}); diff --git a/js/common/test-data/apps/echo-script/echo.js b/js/common/test-data/apps/echo-script/echo.js new file mode 100644 index 0000000000..0b6295d948 --- /dev/null +++ b/js/common/test-data/apps/echo-script/echo.js @@ -0,0 +1,2 @@ +'use strict'; +module.exports = module.context.argv; diff --git a/js/common/test-data/apps/echo-script/manifest.json b/js/common/test-data/apps/echo-script/manifest.json new file mode 100644 index 0000000000..f41e982d55 --- /dev/null +++ b/js/common/test-data/apps/echo-script/manifest.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "echo": "echo.js" + } +} \ No newline at end of file diff --git a/js/common/test-data/apps/itzpapalotl/itzpapalotl.js b/js/common/test-data/apps/itzpapalotl/itzpapalotl.js index aa2e706cc7..10b1613a29 100644 --- a/js/common/test-data/apps/itzpapalotl/itzpapalotl.js +++ b/js/common/test-data/apps/itzpapalotl/itzpapalotl.js @@ -1,5 +1,5 @@ /*jslint indent: 2, nomen: true, maxlen: 100, white: true, plusplus: true, unparam: true */ -/*global applicationContext */ +/*global require */ //////////////////////////////////////////////////////////////////////////////// /// @brief An example Foxx-Application for ArangoDB @@ -29,11 +29,12 @@ //////////////////////////////////////////////////////////////////////////////// (function() { - 'use strict'; + "use strict"; - // initialize a new FoxxApplication - var FoxxApplication = require("@arangodb/foxx").Controller; - var controller = new FoxxApplication(applicationContext); + const createRouter = require('@arangodb/foxx/router'); + const router = createRouter(); + + module.context.use(router); // include console module so we can log something (in the server's log) var console = require("console"); @@ -42,7 +43,7 @@ // we also need this module for custom responses var actions = require("@arangodb/actions"); - // use joi for validation + // use joi for validation var joi = require("joi"); // our app is about the following Aztec deities: @@ -79,10 +80,10 @@ // install index route (this is the default route mentioned in manifest.json) // this route will create an HTML overview page - controller.get('/index', function (req, res) { - res.contentType = "text/html"; + router.get('/index', function (req, res) { + res.set("content-type", "text/html"); - var body = "

" + applicationContext.name + " (" + applicationContext.version + ")

"; + var body = "

" + module.context.service.manifest.name + " (" + module.context.service.manifest.version + ")

"; body += "

an example application demoing a few Foxx features

"; deities.forEach(function(deity) { @@ -96,41 +97,31 @@ .summary("prints an overview page"); // install route to return a random deity name in JSON - controller.get('/random', function (req, res) { + router.get('/random', function (req, res) { var idx = Math.round(Math.random() * (deities.length - 1)); res.json({ name: deities[idx] }); }) .summary("returns a random deity name"); - + // install deity-specific route for summoning // deity name is passed as part of the URL - controller.get('/:deity/summon', function (req, res) { - var deity = req.params("deity"); + router.get('/:deity/summon', function (req, res) { + var deity = req.pathParams.deity; console.log("request to summon %s", deity); if (deities.indexOf(deity) === -1) { // unknown deity - throw new ArangoError(); + res.throw(404, "The requested deity could not be found"); } console.log("summoning %s", deity); - + res.json({ name: deity, summoned: true }); }) .summary("summons the requested deity") - .pathParam("deity", { - type: joi.string().required() - }) - .errorResponse( - Error, actions.HTTP_NOT_FOUND, "The requested deity could not be found", function(e) { - return { - error: true, - code: actions.HTTP_NOT_FOUND, - errorMessage: "The requested deity could not be found" - }; - } + .pathParam("deity", + joi.string().required() ); }()); - diff --git a/js/common/test-data/apps/itzpapalotl/manifest.json b/js/common/test-data/apps/itzpapalotl/manifest.json index ec8de9b969..629bb027cd 100644 --- a/js/common/test-data/apps/itzpapalotl/manifest.json +++ b/js/common/test-data/apps/itzpapalotl/manifest.json @@ -1,15 +1,15 @@ { "name": "itzpapalotl", - "version": "0.0.6", - "engines": { - "arangodb": "^2.8.0" - }, + "version": "1.2.0", "author": "jsteemann", - "description": "random aztec god service", + "description": "random Aztec deity service", + "license": "Apache License, Version 2.0", - "controllers": { - "/": "itzpapalotl.js" + "main": "itzpapalotl.js", + + "engines": { + "arangodb": "^3.0" }, "defaultDocument": "index" -} +} \ No newline at end of file diff --git a/js/common/test-data/apps/minimal-working-setup-teardown/controller.js b/js/common/test-data/apps/minimal-working-setup-teardown/controller.js deleted file mode 100644 index 99bae54f7d..0000000000 --- a/js/common/test-data/apps/minimal-working-setup-teardown/controller.js +++ /dev/null @@ -1,7 +0,0 @@ -var FoxxApplication = require("@arangodb/foxx").Controller; -var controller = new FoxxApplication(applicationContext); - -controller.get('/test', function (req, res) { - 'use strict'; - res.json(true); -}); diff --git a/js/common/test-data/apps/minimal-working-setup-teardown/index.js b/js/common/test-data/apps/minimal-working-setup-teardown/index.js new file mode 100644 index 0000000000..8bb41e0ce4 --- /dev/null +++ b/js/common/test-data/apps/minimal-working-setup-teardown/index.js @@ -0,0 +1,7 @@ +'use strict'; +const router = require('@arangodb/foxx/router')(); +module.context.use(router); + +router.get('/test', function (req, res) { + res.json(true); +}); diff --git a/js/common/test-data/apps/minimal-working-setup-teardown/manifest.json b/js/common/test-data/apps/minimal-working-setup-teardown/manifest.json index 76ac70dc69..e72e7ba851 100644 --- a/js/common/test-data/apps/minimal-working-setup-teardown/manifest.json +++ b/js/common/test-data/apps/minimal-working-setup-teardown/manifest.json @@ -1,14 +1,9 @@ { "name": "minimal-working-manifest", "version": "0.0.0", - "engines": { - "arangodb": "^2.8.0" - }, + "main": "index.js", "scripts": { "setup": "setup.js", "teardown": "teardown.js" - }, - "controllers": { - "/": "controller.js" } } diff --git a/js/common/test-data/apps/minimal-working-setup-teardown/setup.js b/js/common/test-data/apps/minimal-working-setup-teardown/setup.js index ed8113a120..d005c4f003 100644 --- a/js/common/test-data/apps/minimal-working-setup-teardown/setup.js +++ b/js/common/test-data/apps/minimal-working-setup-teardown/setup.js @@ -1,6 +1,8 @@ -var db = require("internal").db; -var col = applicationContext.collectionName("setup_teardown"); -if (!db._collection(col)) { - db._create(col); +'use strict'; +const db = require('@arangodb').db; +const name = module.context.collectionName('setup_teardown'); + +if (!db._collection(name)) { + db._create(name); } diff --git a/js/common/test-data/apps/minimal-working-setup-teardown/teardown.js b/js/common/test-data/apps/minimal-working-setup-teardown/teardown.js index c5abe44a86..fafec6c5a0 100644 --- a/js/common/test-data/apps/minimal-working-setup-teardown/teardown.js +++ b/js/common/test-data/apps/minimal-working-setup-teardown/teardown.js @@ -1,3 +1,5 @@ -var db = require("internal").db; -var col = applicationContext.collectionName("setup_teardown"); -db._drop(col); +'use strict'; +const db = require('@arangodb').db; +const name = module.context.collectionName('setup_teardown'); + +db._drop(name); diff --git a/js/common/test-data/apps/service-service/index.js b/js/common/test-data/apps/service-service/index.js new file mode 100644 index 0000000000..146e51373a --- /dev/null +++ b/js/common/test-data/apps/service-service/index.js @@ -0,0 +1,19 @@ +'use strict'; +const path = require('path'); +const internal = require('internal'); +const utils = require('@arangodb/foxx/manager-utils'); +const router = require('@arangodb/foxx/router')(); + +const basePath = path.resolve(internal.startupPath, 'common', 'test-data', 'apps', 'minimal-working-service'); +const servicePath = utils.zipDirectory(basePath); + +module.context.use(router); +router.get('zip', (req, res) => { + res.download(servicePath, 'service.zip'); +}) +.response(200, ['application/zip']); + +router.get('js', (req, res) => { + res.download(path.resolve(basePath, 'index.js'), 'service.js'); +}) +.response(200, ['application/javascript']); diff --git a/js/common/test-data/apps/with-configuration/manifest.json b/js/common/test-data/apps/with-configuration/manifest.json new file mode 100644 index 0000000000..87b0cc611d --- /dev/null +++ b/js/common/test-data/apps/with-configuration/manifest.json @@ -0,0 +1,12 @@ +{ + "configuration": { + "test1": { + "description": "a string", + "type": "string" + }, + "test2": { + "description": "another string", + "type": "string" + } + } +} \ No newline at end of file diff --git a/js/common/test-data/apps/with-dependencies/manifest.json b/js/common/test-data/apps/with-dependencies/manifest.json new file mode 100644 index 0000000000..33e54db7ea --- /dev/null +++ b/js/common/test-data/apps/with-dependencies/manifest.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "test1": { + "description": "a string" + }, + "test2": { + "description": "another string" + } + } +} \ No newline at end of file diff --git a/js/common/test-data/apps/with-tests/manifest.json b/js/common/test-data/apps/with-tests/manifest.json new file mode 100644 index 0000000000..024f417d9f --- /dev/null +++ b/js/common/test-data/apps/with-tests/manifest.json @@ -0,0 +1,3 @@ +{ + "tests": ["test1.js", "test2.js"] +} diff --git a/js/common/test-data/apps/with-tests/test1.js b/js/common/test-data/apps/with-tests/test1.js new file mode 100644 index 0000000000..69c487c0e7 --- /dev/null +++ b/js/common/test-data/apps/with-tests/test1.js @@ -0,0 +1,20 @@ +/* global describe, it */ +'use strict'; + +const expect = require('chai').expect; + +describe('boolean comparison', () => { + it('should succeed for identity', () => { + expect(true).to.equal(true); + }); + + it('should fail for different values', () => { + expect(true).to.equal(false); + }); +}); + +describe('another boolean comparison', () => { + it('should succeed for identity', () => { + expect(true).to.equal(true); + }); +}); diff --git a/js/common/test-data/apps/with-tests/test2.js b/js/common/test-data/apps/with-tests/test2.js new file mode 100644 index 0000000000..69c487c0e7 --- /dev/null +++ b/js/common/test-data/apps/with-tests/test2.js @@ -0,0 +1,20 @@ +/* global describe, it */ +'use strict'; + +const expect = require('chai').expect; + +describe('boolean comparison', () => { + it('should succeed for identity', () => { + expect(true).to.equal(true); + }); + + it('should fail for different values', () => { + expect(true).to.equal(false); + }); +}); + +describe('another boolean comparison', () => { + it('should succeed for identity', () => { + expect(true).to.equal(true); + }); +}); diff --git a/js/server/modules/@arangodb/foxx/manager.js b/js/server/modules/@arangodb/foxx/manager.js index af5de81f81..92c0427a17 100644 --- a/js/server/modules/@arangodb/foxx/manager.js +++ b/js/server/modules/@arangodb/foxx/manager.js @@ -545,24 +545,22 @@ function _prepareService (serviceInfo, options = {}) { fs.move(tempFile, tempBundlePath); } else if (serviceInfo instanceof Buffer) { // Buffer (js) - const manifest = JSON.stringify({main: 'index.js'}, null, 4); - fs.makeDirectoryRecursive(tempServicePath); - fs.writeFileSync(path.join(tempServicePath, 'index.js'), serviceInfo); - fs.writeFileSync(path.join(tempServicePath, 'manifest.json'), manifest); - utils.zipDirectory(tempServicePath, tempBundlePath); + _buildServiceBundleFromScript(tempServicePath, tempBundlePath, serviceInfo); } else if (/^https?:/i.test(serviceInfo)) { // Remote path const tempFile = downloadServiceBundleFromRemote(serviceInfo); - extractServiceBundle(tempFile, tempServicePath); - fs.move(tempFile, tempBundlePath); + try { + _buildServiceFromFile(tempServicePath, tempBundlePath, tempFile); + } finally { + fs.remove(tempFile); + } } else if (fs.exists(serviceInfo)) { // Local path if (fs.isDirectory(serviceInfo)) { utils.zipDirectory(serviceInfo, tempBundlePath); extractServiceBundle(tempBundlePath, tempServicePath); } else { - extractServiceBundle(serviceInfo, tempServicePath); - fs.copyFile(serviceInfo, tempBundlePath); + _buildServiceFromFile(tempServicePath, tempBundlePath, serviceInfo); } } else { // Foxx Store @@ -615,6 +613,24 @@ function _prepareService (serviceInfo, options = {}) { } } +function _buildServiceFromFile (tempServicePath, tempBundlePath, filePath) { + try { + extractServiceBundle(filePath, tempServicePath); + } catch (e) { + _buildServiceBundleFromScript(tempServicePath, tempBundlePath, fs.readFileSync(filePath)); + return; + } + fs.copyFile(filePath, tempBundlePath); +} + +function _buildServiceBundleFromScript (tempServicePath, tempBundlePath, jsBuffer) { + const manifest = JSON.stringify({main: 'index.js'}, null, 4); + fs.makeDirectoryRecursive(tempServicePath); + fs.writeFileSync(path.join(tempServicePath, 'index.js'), jsBuffer); + fs.writeFileSync(path.join(tempServicePath, 'manifest.json'), manifest); + utils.zipDirectory(tempServicePath, tempBundlePath); +} + function _buildServiceInPath (mount, tempServicePath, tempBundlePath) { const servicePath = FoxxService.basePath(mount); if (fs.exists(servicePath)) { diff --git a/js/server/modules/@arangodb/foxx/service.js b/js/server/modules/@arangodb/foxx/service.js index 57d09794c7..d1f2e995f1 100644 --- a/js/server/modules/@arangodb/foxx/service.js +++ b/js/server/modules/@arangodb/foxx/service.js @@ -103,12 +103,11 @@ module.exports = definition.mount, definition.noisy ); - FoxxService.validateServiceFiles(definition.mount, manifest); + FoxxService.validateServiceFiles(basePath, manifest); return manifest; } - static validateServiceFiles (mount, manifest, rev) { - const servicePath = FoxxService.basePath(mount); + static validateServiceFiles (servicePath, manifest, rev) { if (manifest.main) { parseFile(servicePath, manifest.main); }