├── .gitignore ├── test ├── fake-working-dir │ ├── invalid.json │ ├── incomplete2.json │ ├── incomplete1.json │ ├── extension.json │ └── other-extension │ │ └── my-extension.json ├── helper.js ├── .eslintrc ├── integration │ ├── helpers │ │ └── command.js │ ├── http-server.js │ ├── descriptor-file-test.js │ └── commands-test.js └── unit │ ├── api │ └── api-test.js │ └── cli │ ├── maybe-read-descriptor-file-test.js │ └── widget-test.js ├── bin ├── .eslintrc ├── contentful-extension-list ├── contentful-extension-read ├── contentful-extension-create ├── contentful-extension-delete ├── contentful-extension-update └── contentful-extension ├── .eslintrc ├── lib ├── context.js ├── command │ ├── utils │ │ ├── maybe-read-srcdoc-file.js │ │ ├── maybe-extend-options.js │ │ ├── maybe-read-descriptor-file.js │ │ └── error.js │ ├── read.js │ ├── list.js │ ├── create.js │ ├── delete.js │ └── update.js ├── http.js ├── api.js ├── bin-helpers │ ├── command.js │ └── flags.js └── extension.js ├── .travis.yml ├── LICENSE ├── CHANGELOG.md ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /test/fake-working-dir/invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "value 3 | } 4 | -------------------------------------------------------------------------------- /test/fake-working-dir/incomplete2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "incomplete2", 3 | "name": "incomplete2" 4 | } 5 | -------------------------------------------------------------------------------- /bin/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.eslintrc", 3 | "rules": { 4 | "semi": [2, "always"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fake-working-dir/incomplete1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "incomplete1", 3 | "srcdoc": "build/index.html" 4 | } 5 | -------------------------------------------------------------------------------- /bin/contentful-extension-list: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | require('../lib/bin-helpers/command')('list'); 6 | -------------------------------------------------------------------------------- /bin/contentful-extension-read: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | require('../lib/bin-helpers/command')('read'); 6 | -------------------------------------------------------------------------------- /bin/contentful-extension-create: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | require('../lib/bin-helpers/command')('create'); 6 | -------------------------------------------------------------------------------- /bin/contentful-extension-delete: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | require('../lib/bin-helpers/command')('delete'); 6 | -------------------------------------------------------------------------------- /bin/contentful-extension-update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | require('../lib/bin-helpers/command')('update'); 6 | -------------------------------------------------------------------------------- /test/fake-working-dir/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "lol", 3 | "name": "lol", 4 | "fieldTypes": ["Symbol"], 5 | "srcdoc": "build/extension.html" 6 | } 7 | -------------------------------------------------------------------------------- /test/fake-working-dir/other-extension/my-extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "my-extension", 3 | "name": "My extension", 4 | "fieldTypes": ["Symbol"], 5 | "srcdoc": "my-extension.html" 6 | } 7 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var dirtyChai = require('dirty-chai'); 3 | var sinonChai = require('sinon-chai'); 4 | 5 | chai.use(dirtyChai); 6 | chai.use(sinonChai); 7 | 8 | module.exports = chai; 9 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.eslintrc", 3 | "rules": { 4 | "max-len": 0 5 | }, 6 | "globals": { 7 | "describe": true, 8 | "it": true, 9 | "beforeEach": true, 10 | "afterEach": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "rules": { 4 | "no-multiple-empty-lines": [2, {"max": 3, "maxEOF": 1}], 5 | "no-trailing-spaces": 2, 6 | "max-len": [2, 100, 4], 7 | "semi": [2, "always"], 8 | "newline-after-var": [2, "always"] 9 | }, 10 | "env": { 11 | "node": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var url = require('url'); 4 | var Contentful = require('contentful-management'); 5 | var http = require('./http'); 6 | 7 | module.exports = function setupContext (options) { 8 | let urlInfo = url.parse(options.host || 'https://api.contentful.com'); 9 | let host = urlInfo.host; 10 | let isSecure = urlInfo.protocol === 'https:'; 11 | 12 | let client = Contentful.createClient({ 13 | accessToken: options.token, 14 | host: host, 15 | secure: isSecure 16 | }); 17 | 18 | return { 19 | client: client, 20 | http: http 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /lib/command/utils/maybe-read-srcdoc-file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Bluebird = require('bluebird'); 4 | var fs = require('fs'); 5 | 6 | var readFile = Bluebird.promisify(fs.readFile); 7 | 8 | module.exports = function (options) { 9 | if (!options.srcdoc) { 10 | return Bluebird.resolve(options); 11 | } 12 | 13 | return readFile(options.srcdoc) 14 | .then(function (contents) { 15 | options.srcdoc = contents.toString(); 16 | return options; 17 | }, function (err) { 18 | throw new Error( 19 | `Cannot read the file defined as the "srcdoc" property (${err.path}).` 20 | ); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /test/integration/helpers/command.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var Bluebird = require('bluebird'); 5 | var exec = require('child_process').exec; 6 | 7 | module.exports = function command (subcommand, options) { 8 | let binary = path.resolve(__dirname, '../../../bin/contentful-extension'); 9 | 10 | // TODO incllude the --host option in the exec call 11 | // to remove duplication from the tests 12 | return new Bluebird(function (resolve, reject) { 13 | exec(`${binary} ${subcommand}`, options, function (error, stdout, stderr) { 14 | if (error) { 15 | return reject({error: error, stderr: stderr}); 16 | } 17 | 18 | resolve(stdout); 19 | }); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /bin/contentful-extension: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var path = require('path'); 6 | var fs = require('fs'); 7 | var _ = require('lodash'); 8 | var yargs = require('yargs'); 9 | 10 | yargs.command('read') 11 | .command('update') 12 | .command('delete') 13 | .command('create') 14 | .command('list'); 15 | 16 | var argv = yargs.argv; 17 | var argvCommand = argv._[0]; 18 | var binDir = path.resolve(__dirname); 19 | var commands = fs.readdirSync(`${binDir}`); 20 | var command = _.find(commands, function (file) { 21 | let l = path.basename(file, '.js'); 22 | return l.match(new RegExp(`^contentful-extension-${argvCommand}$`)); 23 | }); 24 | 25 | if (command) { 26 | require(`${binDir}/${command}`); 27 | } else { 28 | yargs.showHelp(); 29 | } 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - iojs-v2 5 | 6 | script: 7 | - npm run test 8 | 9 | notifications: 10 | slack: 11 | secure: AuOBG/XLd3byfSLZ0qcb8vr/JZ6KAj+hcRGvXNi3c6vXgARyqGMzyOJLg9kws3Bvzvr89GKNSM0t98Tyq7tL4i0BlXy4Z4TJS3gCoqYX7LJ3iNl8aThO9+wZ+gkcgU7Hc82pBd3bBxvCNnIQ+m6WBzkGd0OswVQon3JlmM44Mpzz2q4M0/ENt7u7Dgwx6dDj2gVijpDpC3eZJrrP4loXjsVH0SuMmmpW9pbgME4Z7pMSpECZDWHe2aO8jssgToAgWGAdQTJ6jApL2umPHHdTToDBEsQSm3K+a0wbYBVs6Vskj2RIS1D294Q4k4a+7sSlZzh1q+T35kFXEwvv6nkrHl6W1uw8E0WWrzpB5+o085IHAO4DrZKksHyZYFlBqU3T3hiws3dUJXDpOCsR7auFf7B3BKEpzrC8OXYCcpb/+KdbJ/amGYP4zizVq0UbcoL3htNdtuyamXOU3GeduTOd3+b57egJohvpTatU23RfYmOKb0mGkXTcDxND3LCqANzSNRUq6J4Aa2K4K3dGDMYPhQvx15/kgIEcgEQ9E5VKUBhE6mmQXS3RVulrY5clueHJ3QTfPiWGMPH85kcVtolhJ0EqsiBo1/zvvNjPfbcmYbkFnNQKO33Qgc4esq2we/D4egwPXNcSkNu8gXgAtJbsmzC7VWNg5RkEhiqKfg8MppI= 12 | -------------------------------------------------------------------------------- /lib/command/read.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Extension = require('../extension'); 4 | var error = require('./utils/error'); 5 | 6 | var formatError = error.format('read'); 7 | 8 | module.exports = function (options, context) { 9 | if (options.id) { 10 | return new Extension(options, context).read() 11 | .then(function (extension) { 12 | console.log(JSON.stringify(extension)); 13 | }) 14 | .catch(function (err) { 15 | console.error(formatError(err)); 16 | process.exit(1); 17 | }); 18 | } else if (options.all) { 19 | return Extension.all(options, context) 20 | .then(function (extension) { 21 | console.log(JSON.stringify(extension)); 22 | }) 23 | .catch(function (err) { 24 | console.error(formatError(err)); 25 | process.exit(1); 26 | }); 27 | } else { 28 | console.error(formatError('missing one of --id or --all options')); 29 | process.exit(1); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /lib/http.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function path (spaceId, id) { 4 | // @todo maybe in the future we should rename this endpoint 5 | let base = `/spaces/${spaceId}/extensions`; 6 | 7 | if (id) { 8 | return `${base}/${id}`; 9 | } else { 10 | return base; 11 | } 12 | } 13 | 14 | function request (method) { 15 | return function (options, context) { 16 | let requestOpts = { 17 | method: method 18 | }; 19 | let resourcePath = path(options.spaceId, options.id); 20 | 21 | if (options.payload) { 22 | requestOpts.data = JSON.stringify(options.payload); 23 | } 24 | 25 | if (options.version) { 26 | requestOpts.headers = {}; 27 | requestOpts.headers['X-Contentful-Version'] = options.version; 28 | } 29 | 30 | return context.client.request(resourcePath, requestOpts); 31 | }; 32 | } 33 | 34 | 35 | ['post', 'put', 'get', 'delete'].forEach(function (method) { 36 | exports[method] = request(method); 37 | }); 38 | -------------------------------------------------------------------------------- /lib/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const setupContext = require('./context'); 4 | const Extension = require('./extension'); 5 | const _ = require('lodash'); 6 | 7 | module.exports = {createClient}; 8 | 9 | function createClient (options) { 10 | options = options || {}; 11 | const context = setupContext({ 12 | token: options.accessToken, 13 | host: options.host 14 | }); 15 | 16 | function get (id) { 17 | return prepareExtension({id}).read(); 18 | } 19 | 20 | function getAll () { 21 | return Extension.all(getOptions(), context); 22 | } 23 | 24 | function save (opts) { 25 | return prepareExtension(opts).save(); 26 | } 27 | 28 | function del (id, version) { 29 | return prepareExtension({id, version}).delete(); 30 | } 31 | 32 | function prepareExtension (opts) { 33 | return new Extension(getOptions(opts), context); 34 | } 35 | 36 | function getOptions (opts) { 37 | return _.extend({spaceId: options.spaceId}, opts || {}); 38 | } 39 | 40 | return {get: get, getAll, save, delete: del}; 41 | } 42 | -------------------------------------------------------------------------------- /lib/command/list.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Extension = require('../extension'); 4 | var Table = require('cli-table'); 5 | var error = require('./utils/error'); 6 | var formatError = error.format('create'); 7 | 8 | module.exports = function (options, context) { 9 | return Extension.all(options, context) 10 | .then(function (extensions) { 11 | if (extensions.items.length) { 12 | console.log('\nCreated extensions are:'); 13 | var table = extensions.items.reduce(function (table, extension) { 14 | table.push( 15 | [ 16 | extension.extension.name, extension.sys.id, 17 | extension.sys.createdAt, extension.sys.updatedAt 18 | ] 19 | ); 20 | 21 | return table; 22 | }, new Table({ head: ['Name', 'Id', 'CreatedAt', 'UpdatedAt'] })); 23 | 24 | console.log(table.toString()); 25 | } else { 26 | console.log('\nNo extensions for this space created yet.\n'); 27 | } 28 | }) 29 | .catch(function (err) { 30 | console.error(formatError(err.message)); 31 | process.exit(1); 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Contentful 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /lib/command/create.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Extension = require('../extension'); 4 | var error = require('./utils/error'); 5 | var maybeReadDescriptor = require('./utils/maybe-read-descriptor-file'); 6 | var maybeExtendOptions = require('./utils/maybe-extend-options'); 7 | var maybeReadSrcdocFile = require('./utils/maybe-read-srcdoc-file'); 8 | 9 | var formatError = error.format('create'); 10 | 11 | module.exports = function (options, context) { 12 | return maybeReadDescriptor(options) 13 | .then(function (descriptor) { 14 | let required = ['name', 'fieldTypes', {or: ['src', 'srcdoc']}]; 15 | 16 | return maybeExtendOptions(options, descriptor, required); 17 | }) 18 | .then(function () { 19 | return maybeReadSrcdocFile(options); 20 | }) 21 | .then(function () { 22 | return new Extension(options, context).save() 23 | .catch(function (e) { 24 | console.error(formatError(e)); 25 | process.exit(1); 26 | }); 27 | }) 28 | .then(function (extension) { 29 | console.log( 30 | 'Successfully created extension, ' + 31 | `id: ${extension.sys.id} name: ${extension.extension.name}` 32 | ); 33 | }) 34 | .catch(function (err) { 35 | console.error(formatError(err.message)); 36 | process.exit(1); 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /lib/command/delete.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Extension = require('../extension'); 4 | var error = require('./utils/error'); 5 | 6 | var formatError = error.format('delete'); 7 | 8 | module.exports = function (options, context) { 9 | if (!options.version) { 10 | if (!options.force) { 11 | console.error(formatError('to delete without version use the --force flag')); 12 | process.exit(1); 13 | } 14 | 15 | return new Extension(options, context).read() 16 | .catch(function (err) { 17 | console.error(formatError(err)); 18 | process.exit(1); 19 | }) 20 | .then(function (response) { 21 | let version = response.sys.version; 22 | 23 | options.version = version; 24 | 25 | return new Extension(options, context).delete() 26 | .then(function () { 27 | console.log('Successfully deleted extension'); 28 | }) 29 | .catch(function (err) { 30 | console.error(formatError(err)); 31 | process.exit(1); 32 | }); 33 | }); 34 | } else { 35 | return new Extension(options, context).delete() 36 | .then(function () { 37 | console.log('Successfully deleted extension'); 38 | }) 39 | .catch(function (err) { 40 | console.error(formatError(err)); 41 | process.exit(1); 42 | }); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /lib/bin-helpers/command.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var error = require('../command/utils/error'); 4 | var flags = require('./flags'); 5 | var setupContext = require('../context'); 6 | 7 | const DOCS_URL = 8 | 'https://www.contentful.com/developers/docs/references/authentication/'; 9 | 10 | module.exports = function (name) { 11 | let command = require(`../command/${name}`); 12 | let env = fetchEnvironmentOptions(); 13 | let defaults = { host: env.host }; 14 | let options = flags.for(name, defaults); 15 | 16 | if (options.help) { 17 | console.log(flags.helpFor(name)); 18 | process.exit(0); 19 | } 20 | 21 | let token = ensureCMAtoken(env, name); 22 | let context = setupContext({token: token, host: options.host}); 23 | 24 | command(options, context); 25 | }; 26 | 27 | function ensureCMAtoken (env, command) { 28 | let token = env.token; 29 | 30 | if (!token) { 31 | let msg = error.format(command)( 32 | 'Environment variable CONTENTFUL_MANAGEMENT_ACCESS_TOKEN ' + 33 | `is undefined or empty. Visit ${DOCS_URL} to obtain your CMA token.` 34 | ); 35 | 36 | console.error(msg); 37 | process.exit(1); 38 | } 39 | 40 | return token; 41 | } 42 | 43 | function fetchEnvironmentOptions () { 44 | return { 45 | token: process.env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN, 46 | host: process.env.CONTENTFUL_MANAGEMENT_HOST 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## v2.0.0 - 2016-07-06 5 | ### Changed 6 | - Mention "extension.json" file in error messages 7 | - Adopt the new payload and endpoints 8 | 9 | ## v1.3.0 - 2016-06-24 10 | ### Added 11 | - Enable CI 12 | 13 | ### Changed 14 | - Use "extension" in code and as a class name 15 | - Use "extension.json" instead of "widget.json" 16 | - Use "extension" in commands (names, help, messages) 17 | - Update readme to use "extension" 18 | - Update project and command names 19 | 20 | ## v1.2.0 - 2016-06-23 21 | ### Added 22 | - Document programmatic usage 23 | - Expose API for programmatic usage 24 | 25 | ### Changed 26 | - Resolve srcdoc relatively from descriptor's directory 27 | - Improve error messages 28 | - Update README.md (fieldTypes and srcdoc properties) 29 | 30 | ## v1.1.2 - 2016-02-08 31 | ### Changed 32 | - Release the package to the public 33 | 34 | ## v1.1.1 - 2016-01-08 35 | ### Changed 36 | - Update contentful-management 37 | 38 | ## v1.1.0 - 2015-12-21 39 | ### Changed 40 | - Display friendly error messages from server 41 | - Success messages for create, update and delete actions 42 | 43 | ## v1.0.1 - 2015-12-11 44 | ### Added 45 | - Travis config file 46 | 47 | ### Changed 48 | - Configure eslint to complain on no newline after var definition 49 | - Update README with installation instruction 50 | -------------------------------------------------------------------------------- /lib/command/utils/maybe-extend-options.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Bluebird = require('bluebird'); 5 | 6 | function findMissingOptions (options, specs) { 7 | let missing = []; 8 | 9 | _.forEach(specs, function (spec) { 10 | if (_.isPlainObject(spec)) { 11 | if (spec.or) { 12 | let deep = findMissingOptions(options, spec.or); 13 | 14 | if (deep.length === spec.or.length) { 15 | deep = deep.join(' or '); 16 | missing = missing.concat(deep); 17 | } 18 | } 19 | } else if (options[spec] === undefined) { 20 | missing.push(spec); 21 | } 22 | }); 23 | 24 | return missing; 25 | } 26 | 27 | module.exports = function (options, descriptor, required) { 28 | // --src and --srcdoc options exclude src and srdoc 29 | // properties in descriptor file 30 | 31 | if (options.src) { 32 | descriptor = _.omit(descriptor, 'srcdoc'); 33 | } 34 | 35 | if (options.srcdoc) { 36 | descriptor = _.omit(descriptor, 'src'); 37 | } 38 | 39 | return Bluebird.try(function () { 40 | options = _.defaults(options, descriptor); 41 | 42 | let missing = findMissingOptions(options, required); 43 | 44 | if (missing.length > 0) { 45 | let keys = missing.join(', '); 46 | 47 | throw new Error( 48 | `you're missing the following parameters: ${keys}. ` + 49 | `Please provide either a valid extension.json descriptor file ` + 50 | `or use the according command line arguments.` 51 | ); 52 | } 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /lib/command/utils/maybe-read-descriptor-file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Bluebird = require('bluebird'); 4 | var fs = require('fs'); 5 | var nodePath = require('path'); 6 | 7 | var readFileAsync = Bluebird.promisify(fs.readFile); 8 | var statFileAsync = Bluebird.promisify(fs.stat); 9 | var DEFAULT_DESCRIPTOR_FILE = 'extension.json'; 10 | 11 | module.exports = function (options) { 12 | if (options.descriptor) { 13 | return processFile(options.descriptor, options); 14 | } 15 | 16 | return processFile(DEFAULT_DESCRIPTOR_FILE, options) 17 | .catch(function (error) { 18 | if (error.code === 'ENOENT') { 19 | return; 20 | } 21 | 22 | throw error; 23 | }); 24 | }; 25 | 26 | function statFile (path) { 27 | return statFileAsync(path).return(path); 28 | } 29 | 30 | function processFile (path, options) { 31 | options.descriptor = nodePath.resolve(process.cwd(), path); 32 | 33 | return statFile(path) 34 | .then(function () { 35 | return readFileAsync(path); 36 | }) 37 | .then(function (contents) { 38 | let descriptor; 39 | 40 | try { 41 | descriptor = JSON.parse(contents.toString()); 42 | } catch (e) { 43 | throw new Error(`In file ${path}: ${e.message}`); 44 | } 45 | 46 | if (!descriptor.id) { 47 | throw new Error('Missing extension ID in descriptor file.'); 48 | } 49 | 50 | if (!descriptor.src && !descriptor.srcdoc) { 51 | throw new Error('Missing "src" or "srcdoc" property in descriptor file.'); 52 | } 53 | 54 | if (descriptor.srcdoc) { 55 | let projectRoot = nodePath.dirname(options.descriptor); 56 | 57 | descriptor.srcdoc = nodePath.resolve(projectRoot, descriptor.srcdoc); 58 | } 59 | 60 | return descriptor; 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contentful-extension-cli", 3 | "author": "Contentful GmbH", 4 | "version": "2.0.0", 5 | "scripts": { 6 | "test": "npm run lint && npm run test-unit && npm run test-integration", 7 | "lint": "eslint lib/ bin/ test/", 8 | "test-integration": "_mocha test/integration/**/*-test.js", 9 | "test-unit": "_mocha test/unit/**/*-test.js" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/contentful/contentful-extension-cli/issues" 13 | }, 14 | "description": "CLI to manage UI Extensions on Contentful", 15 | "main": "lib/api.js", 16 | "bin": { 17 | "contentful-extension": "./bin/contentful-extension", 18 | "contentful-extension-create": "./bin/contentful-extension-create", 19 | "contentful-extension-delete": "./bin/contentful-extension-delete", 20 | "contentful-extension-read": "./bin/contentful-extension-read", 21 | "contentful-extension-update": "./bin/contentful-extension-update" 22 | }, 23 | "devDependencies": { 24 | "body-parser": "^1.14.1", 25 | "chai": "^3.4.0", 26 | "dirty-chai": "^1.2.2", 27 | "eslint": "^1.8.0", 28 | "eslint-config-standard": "^4.4.0", 29 | "eslint-plugin-standard": "^1.3.1", 30 | "express": "^4.13.3", 31 | "mocha": "^2.3.3", 32 | "proxyquire": "^1.7.9", 33 | "sinon": "^1.17.2", 34 | "sinon-chai": "^2.8.0", 35 | "temp": "^0.8.3" 36 | }, 37 | "homepage": "https://github.com/contentful/contentful-extension-cli#readme", 38 | "license": "MIT", 39 | "repository": { 40 | "type": "git", 41 | "url": "git+https://github.com/contentful/contentful-extension-cli.git" 42 | }, 43 | "dependencies": { 44 | "bluebird": "^3.0.5", 45 | "cli-table": "^0.3.1", 46 | "contentful-management": "^0.8.3", 47 | "lodash": "^3.10.1", 48 | "yargs": "^3.29.0" 49 | }, 50 | "engines": { 51 | "iojs": ">=2.5.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/extension.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | module.exports = class Extension { 6 | constructor (options, context) { 7 | this.options = options; 8 | this.context = context; 9 | } 10 | 11 | static all (options, context) { 12 | return context.http.get(options, context); 13 | } 14 | 15 | save () { 16 | let verb = 'post'; 17 | let options = _.pick(this.options, ['spaceId', 'id', 'version']); 18 | 19 | let payloadProperties = ['src', 'srcdoc', 'name', 'fieldTypes', 'sidebar']; 20 | let payloadData = _.pick(this.options, payloadProperties); 21 | 22 | options.payload = buildAPIPayload(payloadData); 23 | 24 | if (options.id) { 25 | verb = 'put'; 26 | } 27 | 28 | return this.context.http[verb](options, this.context); 29 | } 30 | 31 | read () { 32 | return this.context.http.get(this.options, this.context); 33 | } 34 | 35 | delete () { 36 | return this.context.http.delete(this.options, this.context); 37 | } 38 | }; 39 | 40 | function buildAPIPayload (data) { 41 | let extension = data; 42 | 43 | if (data.fieldTypes) { 44 | extension.fieldTypes = data.fieldTypes.map(fieldType); 45 | } 46 | 47 | return { extension }; 48 | } 49 | 50 | function fieldType (type) { 51 | type = _.capitalize(type.toLowerCase()); 52 | 53 | if (type === 'Assets') { 54 | return arrayFieldType('Link', 'Asset'); 55 | } 56 | 57 | if (type === 'Entries') { 58 | return arrayFieldType('Link', 'Entry'); 59 | } 60 | 61 | if (type === 'Asset' || type === 'Entry') { 62 | return {type: 'Link', linkType: type}; 63 | } 64 | 65 | if (type === 'Symbols') { 66 | return arrayFieldType('Symbol'); 67 | } 68 | 69 | return {type: type}; 70 | } 71 | 72 | function arrayFieldType (type, linkType) { 73 | let array = {type: 'Array', items: {type: type}}; 74 | 75 | if (linkType) { 76 | array.items.linkType = linkType; 77 | } 78 | 79 | return array; 80 | } 81 | -------------------------------------------------------------------------------- /lib/command/utils/error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const DOCS_LINE = 6 | 'See https://www.contentful.com/developers/docs/references/errors for more information.'; 7 | 8 | exports.format = function (command) { 9 | return function (res) { 10 | if (typeof res === 'object' && res.request) { 11 | return serverError(res); 12 | } else { 13 | return textError(res); 14 | } 15 | }; 16 | 17 | function textError (err) { 18 | return `Failed to ${command} the extension: ${err.toString()}`; 19 | } 20 | 21 | function serverError (res) { 22 | const err = processServerError(res); 23 | 24 | return `${err.method} (${command}) ` + 25 | `request failed because of ${err.id} error.` + 26 | `\n${err.reasons.join('\n')}\n${DOCS_LINE}`; 27 | } 28 | }; 29 | 30 | function processServerError (res) { 31 | const errorId = _.get(res, 'error.sys.id', 'Unknown'); 32 | const error = _.get(res, 'error.details.errors[0]', {}); 33 | const method = res.request.method.toUpperCase(); 34 | let reasons = []; 35 | 36 | if (errorId === 'NotFound') { 37 | reasons.push('Check used CMA access token / space ID combination.'); 38 | if (method !== 'POST') { 39 | reasons.push('Check the extension ID.'); 40 | } 41 | } else if (errorId === 'ValidationFailed') { 42 | reasons.push(getValidationErrorReason((error))); 43 | } else { 44 | reasons.push(res.error.message || errorId); 45 | } 46 | 47 | return {id: errorId, method, reasons}; 48 | } 49 | 50 | function getValidationErrorReason (error) { 51 | const property = _.get(error, 'path[1]'); 52 | 53 | if (property === 'name') { 54 | return 'Provide a valid extension name (1-255 characters).'; 55 | } else if (error.expected) { 56 | return `The "${property}" extension property expects: ${error.expected}`; 57 | } else if (error.max) { 58 | return `The "${property}" extension property must have at most ${error.max} characters.`; 59 | } else { 60 | return 'An unknown validation error occurred.'; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/command/update.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Extension = require('../extension'); 4 | var error = require('./utils/error'); 5 | var maybeReadDescriptor = require('./utils/maybe-read-descriptor-file'); 6 | var maybeExtendOptions = require('./utils/maybe-extend-options'); 7 | var maybeReadSrcdocFile = require('./utils/maybe-read-srcdoc-file'); 8 | 9 | var formatError = error.format('update'); 10 | 11 | module.exports = function (options, context) { 12 | return maybeReadDescriptor(options) 13 | .then(function (descriptor) { 14 | let required = ['id', 'fieldTypes', 'name', { or: ['src', 'srcdoc'] }]; 15 | 16 | return maybeExtendOptions(options, descriptor, required); 17 | }) 18 | .then(function () { 19 | return maybeReadSrcdocFile(options); 20 | }) 21 | .then(function () { 22 | if (options.version) { 23 | return options; 24 | } else { 25 | if (!options.force) { 26 | throw new Error('to update without version use the --force flag'); 27 | } 28 | 29 | return loadCurrentVersion(options, context) 30 | .catch(function (err) { 31 | console.error(formatError(err)); 32 | process.exit(1); 33 | }); 34 | } 35 | }).then(function (options) { 36 | return new Extension(options, context).save() 37 | .catch(function (err) { 38 | console.error(formatError(err)); 39 | process.exit(1); 40 | }) 41 | .then(function (extension) { 42 | console.log( 43 | 'Successfully updated extension, ' + 44 | `id: ${extension.sys.id} name: ${extension.extension.name}` 45 | ); 46 | }); 47 | }) 48 | .catch(function (err) { 49 | console.error(formatError(err.message)); 50 | process.exit(1); 51 | }); 52 | }; 53 | 54 | /** 55 | * GETs the extension from the server and extends `options` with the 56 | * current version. 57 | */ 58 | function loadCurrentVersion (options, context) { 59 | return new Extension(options, context).read() 60 | .then(function (response) { 61 | let version = response.sys.version; 62 | 63 | options.version = version; 64 | return options; 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /test/unit/api/api-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const sinon = require('sinon'); 5 | const proxyquire = require('proxyquire'); 6 | 7 | const context = {http: {}, client: {}}; 8 | const setupContextStub = sinon.stub().returns(context); 9 | const extensionMethodSpies = {}; 10 | const extensionConstructorSpy = sinon.spy(); 11 | 12 | function ExtensionStub () { 13 | extensionConstructorSpy.apply(null, arguments); 14 | _.extend(this, extensionMethodSpies); 15 | } 16 | 17 | ExtensionStub.all = sinon.spy(); 18 | 19 | const api = proxyquire('../../..', { 20 | './extension': ExtensionStub, 21 | './context': setupContextStub 22 | }); 23 | 24 | describe('API', function () { 25 | describe('initialization', function () { 26 | it('passes token and host to context creation fn', function () { 27 | api.createClient({ 28 | accessToken: 'token-lol', 29 | host: 'http://api.test.com' 30 | }); 31 | 32 | sinon.assert.calledOnce(setupContextStub.withArgs({ 33 | token: 'token-lol', 34 | host: 'http://api.test.com' 35 | })); 36 | }); 37 | }); 38 | 39 | describe('instance methods', function () { 40 | beforeEach(function () { 41 | extensionMethodSpies.read = sinon.spy(); 42 | extensionMethodSpies.save = sinon.spy(); 43 | extensionMethodSpies.delete = sinon.spy(); 44 | this.client = api.createClient({spaceId: 'spaceid'}); 45 | }); 46 | 47 | it('#getAll()', function () { 48 | this.client.getAll(); 49 | sinon.assert.calledOnce(ExtensionStub.all.withArgs({spaceId: 'spaceid'})); 50 | }); 51 | 52 | it('#get(id)', function () { 53 | const options = {spaceId: 'spaceid', id: 'tid'}; 54 | 55 | this.client.get('tid'); 56 | sinon.assert.calledOnce(extensionConstructorSpy.withArgs(options, context)); 57 | sinon.assert.calledOnce(extensionMethodSpies.read); 58 | }); 59 | 60 | it('#save(extension)', function () { 61 | const extension = {id: 'tid', name: 'test', version: 123}; 62 | const options = _.extend({spaceId: 'spaceid'}, extension); 63 | 64 | this.client.save(extension); 65 | sinon.assert.calledOnce(extensionConstructorSpy.withArgs(options, context)); 66 | sinon.assert.calledOnce(extensionMethodSpies.save); 67 | }); 68 | 69 | it('#delete(id, version)', function () { 70 | const options = {spaceId: 'spaceid', id: 'tid', version: 123}; 71 | 72 | this.client.delete('tid', 123); 73 | sinon.assert.calledOnce(extensionConstructorSpy.withArgs(options, context)); 74 | sinon.assert.calledOnce(extensionMethodSpies.delete); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/unit/cli/maybe-read-descriptor-file-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var process = require('process'); 4 | var path = require('path'); 5 | 6 | var expect = require('../../helper').expect; 7 | var maybeRead = require('../../../lib/command/utils/maybe-read-descriptor-file'); 8 | 9 | describe('Reading descriptor', function () { 10 | let originalWd; 11 | 12 | beforeEach(function () { 13 | originalWd = process.cwd(); 14 | process.chdir(path.resolve(__dirname, '../../fake-working-dir')); 15 | }); 16 | 17 | afterEach(function () { 18 | process.chdir(originalWd); 19 | }); 20 | 21 | it('extends options with an absolute path of the default "extension.json" file', function () { 22 | let options = {}; 23 | 24 | return maybeRead(options).then(function () { 25 | expect(options.descriptor).to.eq(path.resolve(process.cwd(), 'extension.json')); 26 | }); 27 | }); 28 | 29 | it('resolves an absolute path of the provided descriptor file', function () { 30 | let options = {descriptor: 'other-extension/my-extension.json'}; 31 | 32 | return maybeRead(options).then(function () { 33 | expect(options.descriptor).to.eq(path.resolve(process.cwd(), 'other-extension/my-extension.json')); 34 | }); 35 | }); 36 | 37 | it('reads the default "extension.json" file', function () { 38 | return maybeRead({}).then(function (descriptor) { 39 | expect(descriptor.id).to.eq('lol'); 40 | expect(descriptor.name).to.eq('lol'); 41 | expect(descriptor.fieldTypes[0]).to.eq('Symbol'); 42 | expect(descriptor.fieldTypes.length).to.eq(1); 43 | }); 44 | }); 45 | 46 | it('reads the provided descriptor file', function () { 47 | return maybeRead({descriptor: 'other-extension/my-extension.json'}) 48 | .then(function (descriptor) { 49 | expect(descriptor.id).to.eq('my-extension'); 50 | expect(descriptor.name).to.eq('My extension'); 51 | expect(descriptor.fieldTypes[0]).to.eq('Symbol'); 52 | expect(descriptor.fieldTypes.length).to.eq(1); 53 | }); 54 | }); 55 | 56 | it('resolves "srcdoc" property relatively to the descriptor file', function () { 57 | let options = {descriptor: 'other-extension/my-extension.json'}; 58 | 59 | return maybeRead(options).then(function (descriptor) { 60 | let resolved = path.resolve(path.dirname(options.descriptor), 'my-extension.html'); 61 | 62 | expect(descriptor.srcdoc).to.eq(resolved); 63 | }); 64 | }); 65 | 66 | it('fails on invalid JSON', function () { 67 | return maybeRead({descriptor: 'invalid.json'}).catch(function (err) { 68 | expect(err.message).to.have.string('In file invalid.json: Unexpected token'); 69 | }); 70 | }); 71 | 72 | it('fails on lack of extension ID', function () { 73 | return maybeRead({descriptor: 'incomplete1.json'}).catch(function (err) { 74 | expect(err.message).to.have.string('Missing extension ID'); 75 | }); 76 | }); 77 | 78 | it('fails when both src and srcdoc properties are not provided', function () { 79 | return maybeRead({descriptor: 'incomplete2.json'}).catch(function (err) { 80 | expect(err.message).to.have.string('Missing "src" or "srcdoc" property'); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /lib/bin-helpers/flags.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var yargs = require('yargs'); 5 | 6 | // Override OS locale and LC_ALL 7 | yargs.locale('en'); 8 | 9 | let OPTIONS = { 10 | 'help': { 11 | description: 'Show this help', 12 | type: 'boolean', 13 | for: 'all' 14 | }, 15 | 'space-id': { 16 | required: true, 17 | description: 'Id of a space in Contentful', 18 | type: 'string', 19 | requiresArg: true, 20 | for: ['all'] 21 | }, 22 | 'host': { 23 | description: 'API host', 24 | type: 'string', 25 | requiresArg: true, 26 | for: ['all'] 27 | }, 28 | 'srcdoc': { 29 | description: 'Path to extension bundle', 30 | type: 'string', 31 | requiresArg: true, 32 | for: ['create', 'update'] 33 | }, 34 | 'src': { 35 | description: 'URL to extension bundle', 36 | type: 'string', 37 | requiresArg: true, 38 | for: ['create', 'update'] 39 | }, 40 | 'id': { 41 | description: 'Extension ID', 42 | type: 'string', 43 | requiresArg: true, 44 | for: ['all'] 45 | }, 46 | 'name': { 47 | description: 'Extension name', 48 | type: 'string', 49 | requiresArg: true, 50 | for: ['create', 'update'] 51 | }, 52 | 'descriptor': { 53 | description: 'Path to an extension descriptor file', 54 | type: 'string', 55 | requiresArg: true, 56 | for: ['create', 'update'] 57 | }, 58 | 'field-types': { 59 | description: 'List of field types where to use the extension', 60 | type: 'array', 61 | requiresArg: true, 62 | for: ['create', 'update'] 63 | }, 64 | 'sidebar': { 65 | description: 'Render the extension in the sidebar', 66 | type: 'boolean', 67 | default: undefined, 68 | for: ['create', 'update'] 69 | }, 70 | 'force': { 71 | description: 'Force operation without explicit version', 72 | type: 'boolean', 73 | for: ['update', 'delete'] 74 | }, 75 | 'version': { 76 | description: 'Current version of the extension', 77 | type: 'string', 78 | requiresArg: true, 79 | for: ['update', 'delete'] 80 | }, 81 | 'all': { 82 | description: 'Read all the extensions in the space', 83 | type: 'boolean', 84 | for: ['read'] 85 | } 86 | }; 87 | 88 | let optionsRefinements = { 89 | delete: { 90 | 'id': { 91 | required: true 92 | } 93 | } 94 | }; 95 | 96 | exports.options = OPTIONS; 97 | exports.for = function (command, defaults) { 98 | let optionDescriptors = optionDescriptorsForCommand(command); 99 | let argv = yargs.options(optionDescriptors).argv; 100 | let ARGVValues = ARGVValuesForCommand(argv, optionDescriptors, command, defaults); 101 | 102 | return ARGVValues; 103 | }; 104 | 105 | exports.helpFor = function (command) { 106 | let optionDescriptors = optionDescriptorsForCommand(command); 107 | let argv = yargs.options(optionDescriptors); 108 | 109 | return argv.help(); 110 | }; 111 | 112 | function optionDescriptorsForCommand (command) { 113 | return _.transform(OPTIONS, function (acc, value, key) { 114 | let applicableTo = value.for; 115 | let refinement; 116 | 117 | if (optionsRefinements[command]) { 118 | refinement = optionsRefinements[command][key]; 119 | } 120 | 121 | if (_.contains(applicableTo, command) || _.contains(applicableTo, 'all')) { 122 | let clone = _.omit(value, 'for'); 123 | 124 | _.extend(clone, refinement); 125 | acc[key] = clone; 126 | } 127 | }, {}); 128 | } 129 | 130 | function ARGVValuesForCommand (argv, optionDescriptors, command, defaults) { 131 | let keys = Object.keys(optionDescriptors); 132 | let camelcasedKeys = _.map(keys, _.camelCase); 133 | 134 | return _.defaults(_.pick(argv, camelcasedKeys), defaults); 135 | } 136 | -------------------------------------------------------------------------------- /test/integration/http-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var express = require('express'); 5 | var bodyParser = require('body-parser'); 6 | var app = express(); 7 | 8 | var store = {}; 9 | 10 | app.use(bodyParser.json({ type: 'application/vnd.contentful.management.v1+json' })); 11 | app.use(function (req, res, next) { 12 | let accessToken = req.query.access_token; 13 | 14 | if (accessToken !== 'lol-token') { 15 | res.status(404); 16 | res.end(); 17 | } else { 18 | next(); 19 | } 20 | }); 21 | 22 | app.all('/spaces/:space/extensions/:id', function (req, res, next) { 23 | if (req.params.id === 'not-found') { 24 | let error = buildError('NotFound', 'The resource can\'t be found.'); 25 | 26 | res.status(404); 27 | res.send(error); 28 | res.end(); 29 | return; 30 | } 31 | 32 | if (req.params.id === 'fail') { 33 | let error = buildError(); 34 | 35 | res.status(500); 36 | res.send(error); 37 | res.end(); 38 | return; 39 | } 40 | 41 | next(); 42 | }); 43 | 44 | app.post('/spaces/:space/extensions', function (req, res) { 45 | if (_.get(req, 'body.extension.fieldTypes[0].type') === 'Lol') { 46 | return respondWithValidationError(res, { 47 | path: ['extension', 'fieldTypes'], 48 | expected: ['Symbol', 'Yolo'] 49 | }); 50 | } 51 | 52 | let extension = createExtension(req.params.space, req.params.id, req.body); 53 | 54 | res.status(201); 55 | res.json(extension); 56 | res.end(); 57 | }); 58 | 59 | app.put('/spaces/:space/extensions/:id', function (req, res) { 60 | let extension = store[req.params.id]; 61 | let versionInHeader = req.headers['x-contentful-version']; 62 | let xVersion = versionInHeader ? parseInt(versionInHeader, 10) : undefined; 63 | 64 | if (!extension) { 65 | if (req.params.id === 'too-long-name') { 66 | return respondWithValidationError(res, {path: ['extension', 'name']}); 67 | } else if (req.params.id === 'so-invalid') { 68 | return respondWithValidationError(res); 69 | } else if (req.params.id === 'too-big') { 70 | return respondWithValidationError(res, { 71 | path: ['extension', 'srcdoc'], 72 | max: 7777 73 | }); 74 | } 75 | 76 | let extension = createExtension(req.params.space, req.params.id, req.body); 77 | 78 | store[req.params.id] = extension; 79 | res.status(201); 80 | res.json(extension); 81 | res.end(); 82 | } else { 83 | if (req.params.id === 'fail-update') { 84 | let error = buildError(); 85 | 86 | res.status(500); 87 | res.send(error); 88 | res.end(); 89 | return; 90 | } 91 | 92 | if (xVersion !== extension.sys.version) { 93 | res.status(409); 94 | res.end(); 95 | } else { 96 | let sys = extension.sys; 97 | 98 | extension = req.body; 99 | extension.sys = sys; 100 | extension.sys.version = extension.sys.version + 1; 101 | store[req.params.id] = extension; // Update the store 102 | 103 | res.json(extension); 104 | res.status(200); 105 | res.end(); 106 | } 107 | } 108 | }); 109 | 110 | app.get('/spaces/:space/extensions', function (req, res) { 111 | let extensions = _.filter(store, {sys: {space: {sys: {id: req.params.space}}}}); 112 | let response = { sys: {type: 'Array'}, total: extensions.length, items: extensions }; 113 | 114 | if (req.params.space === 'fail') { 115 | let error = buildError(); 116 | 117 | res.status(500); 118 | res.send(error); 119 | res.end(); 120 | return; 121 | } 122 | 123 | res.status(200); 124 | res.json(response); 125 | res.end(); 126 | }); 127 | 128 | app.get('/spaces/:space/extensions/:id', function (req, res) { 129 | let extension = store[req.params.id]; 130 | 131 | res.status(200); 132 | res.json(extension); 133 | res.end(); 134 | }); 135 | 136 | app.delete('/spaces/:space/extensions/:id', function (req, res) { 137 | let extension = store[req.params.id]; 138 | let xVersion = parseInt(req.headers['x-contentful-version'], 10); 139 | 140 | if (req.params.id === 'fail-delete') { 141 | let error = buildError(); 142 | 143 | res.status(500); 144 | res.send(error); 145 | res.end(); 146 | return; 147 | } 148 | 149 | if (xVersion !== extension.sys.version) { 150 | res.status(409); 151 | res.end(); 152 | } else { 153 | delete store[req.params.id]; 154 | res.status(204); 155 | res.end(); 156 | } 157 | }); 158 | 159 | function createExtension (spaceId, id, payload) { 160 | return _.extend(payload, { 161 | sys: { 162 | version: 1, 163 | id: id || _.random(1000), 164 | space: { 165 | sys: { 166 | id: spaceId 167 | } 168 | }, 169 | createdAt: (new Date()).toString(), 170 | updatedAt: (new Date()).toString() 171 | } 172 | }); 173 | } 174 | 175 | function buildError (id, message, error) { 176 | return _.extend({ 177 | sys: { 178 | id: id || 'ServerError' 179 | }, 180 | message: message || 'Server failed to fulfill the request.', 181 | details: {errors: [error || {}]} 182 | }); 183 | } 184 | 185 | function respondWithValidationError (res, err) { 186 | res.status(422); 187 | res.send(buildError('ValidationFailed', null, err)); 188 | res.end(); 189 | } 190 | 191 | var server; 192 | 193 | exports.start = function start () { 194 | server = app.listen(3000); 195 | }; 196 | 197 | exports.stop = function stop () { 198 | store = {}; 199 | server.close(); 200 | }; 201 | -------------------------------------------------------------------------------- /test/unit/cli/widget-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sinon = require('sinon'); 4 | var Bluebird = require('bluebird'); 5 | var _ = require('lodash'); 6 | 7 | var expect = require('../../helper').expect; 8 | var Extension = require('../../../lib/extension'); 9 | 10 | function buildExtensionPayload (options) { 11 | var extension = {}; 12 | 13 | _.extend(extension, _.pick(options, ['src', 'srcdoc', 'fieldTypes'])); 14 | 15 | if (options.fieldTypes) { 16 | extension.fieldTypes = []; 17 | 18 | options.fieldTypes.forEach(function (fieldType) { 19 | if (fieldType === 'Entries') { 20 | extension.fieldTypes.push({type: 'Array', items: {type: 'Link', linkType: 'Entry'}}); 21 | return; 22 | } 23 | 24 | if (fieldType === 'Assets') { 25 | extension.fieldTypes.push({type: 'Array', items: {type: 'Link', linkType: 'Asset'}}); 26 | return; 27 | } 28 | 29 | if (fieldType === 'Symbols') { 30 | extension.fieldTypes.push({type: 'Array', items: {type: 'Symbol'}}); 31 | return; 32 | } 33 | 34 | if (fieldType === 'Entry' || fieldType === 'Asset') { 35 | extension.fieldTypes.push({type: 'Link', linkType: fieldType}); 36 | return; 37 | } 38 | 39 | extension.fieldTypes.push({type: fieldType}); 40 | }); 41 | } 42 | 43 | return { extension }; 44 | } 45 | 46 | describe('Extension', function () { 47 | let context, options, extension, http; 48 | 49 | beforeEach(function () { 50 | http = { 51 | post: sinon.stub().returns(Bluebird.resolve()), 52 | get: sinon.stub().returns(Bluebird.resolve()), 53 | delete: sinon.stub().returns(Bluebird.resolve()), 54 | put: sinon.stub().returns(Bluebird.resolve()) 55 | }; 56 | 57 | context = {http: http}; 58 | }); 59 | 60 | describe('#save', function () { 61 | describe('when an id has been provided', function () { 62 | beforeEach(function () { 63 | options = { 64 | spaceId: 123, 65 | src: 'the-src', 66 | id: 456 67 | }; 68 | 69 | extension = new Extension(options, context); 70 | }); 71 | 72 | it('it calls the http.put method with the expected arguments', function () { 73 | let payload = buildExtensionPayload({src: options.src}); 74 | 75 | return extension.save().then(function () { 76 | expect(http.put).to.have.been.calledWith( 77 | { 78 | spaceId: options.spaceId, 79 | payload: payload, 80 | id: options.id 81 | }, 82 | context 83 | ); 84 | }); 85 | }); 86 | 87 | describe('when a version has been provided', function () { 88 | it('it calls the http.put method including the version', function () { 89 | let payload = buildExtensionPayload({src: options.src}); 90 | 91 | options = _.extend(options, {version: 66}); 92 | 93 | return extension.save().then(function () { 94 | expect(http.put).to.have.been.calledWith( 95 | { 96 | spaceId: options.spaceId, 97 | payload: payload, 98 | id: options.id, 99 | version: options.version 100 | }, 101 | context 102 | ); 103 | }); 104 | }); 105 | }); 106 | }); 107 | 108 | describe('when a srcdoc has been provided', function () { 109 | beforeEach(function () { 110 | options = { 111 | spaceId: 123, 112 | srcdoc: 'the-bundle' 113 | }; 114 | 115 | extension = new Extension(options, context); 116 | }); 117 | 118 | it('it saves a extension with the srcdoc property set', function () { 119 | let payload = buildExtensionPayload({srcdoc: options.srcdoc}); 120 | 121 | return extension.save().then(function () { 122 | expect(http.post).to.have.been.calledWith( 123 | { 124 | spaceId: options.spaceId, 125 | payload: payload 126 | }, 127 | context 128 | ); 129 | }); 130 | }); 131 | }); 132 | 133 | describe('when a URL has been provided', function () { 134 | beforeEach(function () { 135 | options = { 136 | spaceId: 123, 137 | src: 'the-url' 138 | }; 139 | 140 | extension = new Extension(options, context); 141 | }); 142 | 143 | it('it saves a extension with the src property set', function () { 144 | let payload = buildExtensionPayload({src: options.src}); 145 | 146 | return extension.save().then(function () { 147 | expect(http.post).to.have.been.calledWith( 148 | { 149 | spaceId: options.spaceId, 150 | payload: payload 151 | }, 152 | context 153 | ); 154 | }); 155 | }); 156 | }); 157 | 158 | describe('when fieldTypes have been provided', function () { 159 | beforeEach(function () { 160 | options = { 161 | spaceId: 123, 162 | src: 'the-url' 163 | }; 164 | }); 165 | 166 | [ 167 | 'Symbol', 'Text', 'Date', 'Integer', 'Number', 'Location', 'Boolean', 'Object', 168 | 'Entry', 'Asset', 'Symbols', 'Assets', 'Entries' 169 | ].forEach(function (fieldType) { 170 | it(`saves the extension with the fieldType ${fieldType}`, function () { 171 | options.fieldTypes = [fieldType]; 172 | let payload = buildExtensionPayload(options); 173 | 174 | extension = new Extension(options, context); 175 | 176 | return extension.save().then(function () { 177 | expect(http.post).to.have.been.calledWith( 178 | { 179 | spaceId: options.spaceId, 180 | payload: payload 181 | }, 182 | context 183 | ); 184 | }); 185 | }); 186 | }); 187 | 188 | it('saves the extension with multiple fieldTypes', function () { 189 | options.fieldTypes = ['Symbol', 'Date', 'Symbols', 'Asset', 'Entries']; 190 | 191 | let payload = buildExtensionPayload(options); 192 | 193 | extension = new Extension(options, context); 194 | 195 | return extension.save().then(function () { 196 | expect(http.post).to.have.been.calledWith( 197 | { 198 | spaceId: options.spaceId, 199 | payload: payload 200 | }, 201 | context 202 | ); 203 | }); 204 | }); 205 | 206 | it('saves the extension with multiple fieldTypes (capitalizes lowercase)', function () { 207 | options.fieldTypes = ['symbol', 'entries']; 208 | 209 | extension = new Extension(options, context); 210 | 211 | return extension.save().then(function () { 212 | expect(http.post).to.have.been.calledWith( 213 | { 214 | spaceId: options.spaceId, 215 | payload: { 216 | extension: { 217 | src: 'the-url', 218 | fieldTypes: [ 219 | {type: 'Symbol'}, 220 | {type: 'Array', items: {type: 'Link', linkType: 'Entry'}} 221 | ] 222 | } 223 | } 224 | }, 225 | context 226 | ); 227 | }); 228 | }); 229 | }); 230 | }); 231 | 232 | describe('#read', function () { 233 | beforeEach(function () { 234 | options = { 235 | spaceId: 123, 236 | id: 456 237 | }; 238 | 239 | extension = new Extension(options, context); 240 | }); 241 | 242 | it('calls the http module with the expected arguments', function () { 243 | return extension.read().then(function () { 244 | expect(http.get).to.have.been.calledWith( 245 | { 246 | spaceId: options.spaceId, 247 | id: options.id 248 | }, 249 | context 250 | ); 251 | }); 252 | }); 253 | }); 254 | 255 | describe('#delete', function () { 256 | beforeEach(function () { 257 | options = { 258 | spaceId: 123, 259 | id: 456 260 | }; 261 | 262 | extension = new Extension(options, context); 263 | }); 264 | 265 | it('calls the http module with the expected arguments', function () { 266 | return extension.delete().then(function () { 267 | expect(http.delete).to.have.been.calledWith( 268 | { 269 | spaceId: options.spaceId, 270 | id: options.id 271 | }, 272 | context 273 | ); 274 | }); 275 | }); 276 | 277 | it('returns the return value from the http.delete method', function () { 278 | http.delete.returns(Bluebird.resolve('delete-response')); 279 | 280 | return extension.delete().then(function (response) { 281 | expect(response).to.eql('delete-response'); 282 | }); 283 | }); 284 | }); 285 | }); 286 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecation notice 2 | 3 | This repository was deprecated as all functionality was migrated to the [Contentful CLI](https://github.com/contentful/contentful-cli). 4 | 5 | --- 6 | 7 | ## Introduction [![Build Status](https://travis-ci.org/contentful/contentful-extension-cli.svg?branch=master)](https://travis-ci.org/contentful/contentful-extension-cli) 8 | 9 | Contentful allows customers to customize and tailor the UI using custom made extensions. Extensions have to be uploaded to Contentful in order to be able to use them in the UI. 10 | 11 | This repo hosts `contentful-extension` a Command Line Tool (CLI) developed to simplify the management tasks associated with custom extensions. With the CLI you can: 12 | 13 | - Create extensions 14 | - Update existing extensions 15 | - Read extensions 16 | - Delete extensions 17 | 18 | ## Installation 19 | 20 | ``` 21 | npm install -g contentful-extension-cli 22 | ``` 23 | 24 | 25 | ## Available commands 26 | 27 | `contentful-extension` is composed of 4 subcommands that you can use to manage the extensions. 28 | 29 | **create an extension** 30 | 31 | ``` 32 | contentful-extension create [options] 33 | ``` 34 | Use this subcommand to create the extension for the first time. Succesive modifications made to the extension will be have to be using the `update` subcommand. 35 | 36 | **read an extension** 37 | 38 | ``` 39 | contentful-extension read [options] 40 | ``` 41 | Use this subcommand to read the extension payload from Contentful. With this subcommand you can also list all the extensions in one space. 42 | 43 | **update an extension** 44 | 45 | ``` 46 | contentful-extension update [options] 47 | ``` 48 | Use this subcommand to modify an existing extension. 49 | 50 | **delete an extension** 51 | 52 | ``` 53 | contentful-extension delete [options] 54 | ``` 55 | 56 | Use this subcommand to permanently delete an extension from Contentful. 57 | 58 | For a full list of all the options available on every subcommand use the `--help` option. 59 | 60 | **list all extensions** 61 | 62 | ``` 63 | contentful-extension list [options] 64 | ``` 65 | 66 | Use this subcommand to see what extensions are created for a given space. 67 | 68 | ## Misc 69 | 70 | The following sections describe a series of concepts around the extensions and how the CLI deals with them. 71 | 72 | ### Extension properties 73 | 74 | The following table describes the properties that can be set on an extension. 75 | 76 | Property | Required| Type | Description 77 | ---------|---------|------|------------ 78 | name | yes | String | Extension name 79 | fieldTypes | yes | Array\ * | Field types where an extension can be used 80 | src | ** | String | URL where the root HTML document of the extension can be found 81 | srcdoc | ** | String | Path to the local extension HTML document 82 | sidebar | no | Boolean | Controls the location of the extension. If `true` it will be rendered on the sidebar 83 | 84 | \* Valid field types are: `Symbol`, `Symbols`, `Text`, `Integer`, `Number`, `Date`, `Boolean`, `Object`, `Entry`, `Entries`, `Asset`, `Assets` 85 | 86 | \** One of `src` or `srcdoc` have to be present 87 | 88 | #### Difference between `src` and `srcdoc` properties 89 | 90 | When using `src` property, an extension is considered 3rd party hosted. Relative links in the root HTML document are supported as expected. 91 | 92 | When using `srcdoc` property, an extension is considered internally hosted. A file being pointed by the `srcdoc` property will be loaded and uploaded as a string to Contentful. All local dependencies have to be manually inlined into the file. The command line tool does not take care of link resolving and inlining of referenced local resources. The maximal size of a file used with the `srcdoc` property is 200kB. Use [HTML minifier with `minifyJS` option](https://www.npmjs.com/package/html-minifier) and use CDN sources for libraries that your extension is depending on. 93 | 94 | If a relative value of `srcdoc` property is used, the path is resolved from a directory in which the descriptor file is placed or a working directory when using the `--srcdoc` command line option. 95 | 96 | Use the `src` property when you want to be as flexible as possible with your development and deployment process. Use the `srcdoc` property if you don't want to host anything on your own and can accept the drawbacks (need for a non-standard build, filesize limitation). Note that using `srcdoc` is [not supported][caniuse-srcdoc] on Internet Explorer and Microsof Edge. 97 | 98 | [caniuse-srcdoc]: https://caniuse.com/#feat=iframe-srcdoc 99 | 100 | #### Specifying extension properties 101 | 102 | Subcommands that create of modify extensions (`create` and `update`) accept the properties for the extension in two forms: command line options or a JSON file. 103 | 104 | ##### Command line options 105 | 106 | For every property in the extension there's a corresponding long option with the same name. So for example, there's a `name` property and so a `--name` option too. 107 | 108 | ``` 109 | contentful-extension create --space-id 123 --name foo --src foo.com/extension 110 | ``` 111 | Note that camelcased property names like `fieldTypes` are hyphenated (`--field-types`). 112 | 113 | ##### Descriptor files 114 | 115 | Descriptor files are JSON files that contain the values that will be sent to the API to create the extension. By default the CLI will look in the current working directory for a descriptor file called `extension.json`. Another file can be used witht the `--descriptor` option. 116 | 117 | A descriptor file can contain: 118 | 119 | - All the extension properties (`name`, `src`, ...). Please note that the `srcdoc` property has to be a path to a file containing the extension HTML document. 120 | - An `id` property. Including the `id` in the descriptor file means that you won't have to use the `--id` option when creating or updating an extension. 121 | 122 | All the properties included in a descriptor file can be overriden by its counterpart command line options. This means that, for example, a `--name bar` option will take precedence over the `name` property in the descriptor file. Following is an example were the usage of descriptor files is explained: 123 | 124 | Assuming that there's an `extension.json` file in the directory where the CLI is run and that's its contents are: 125 | 126 | ```json 127 | { 128 | "name": "foo", 129 | "src": "foo.com/extension", 130 | "id": "foo-extension" 131 | } 132 | ``` 133 | 134 | The following command 135 | 136 | ``` 137 | contentful-extension create --space-id 123 --name bar 138 | ``` 139 | 140 | Will create the following extension. Note that the `name` is `bar` and that the `id` is `foo-extension`. 141 | 142 | ```json 143 | { 144 | "name": "bar", 145 | "src": "foo.com/extension", 146 | "id": "foo-extension" 147 | "sys": { 148 | "id": "foo-extension" 149 | ... 150 | } 151 | } 152 | ``` 153 | 154 | 155 | ### Authentication 156 | 157 | Extensions are managed via the Contentful Management API (CMA). 158 | You will therefore need to provide a valid access token in the 159 | `CONTENTFUL_MANAGEMENT_ACCESS_TOKEN` environment variable. 160 | 161 | Our documentation describes [how to obtain a token](https://www.contentful.com/developers/docs/references/authentication/#getting-an-oauth-token). 162 | 163 | 164 | ### Version locking 165 | 166 | Contentful API use [optimistic locking](https://www.contentful.com/developers/docs/references/content-management-api/#/introduction/updating-and-version-locking) to ensure that accidental non-idemptotent operations (`update` or `delete`) can't happen. 167 | 168 | This means that the CLI needs to know the current version of the extension when using the `update` and `delete` subcommands. On these case you have to specify the version of the extension using the `--version` option. 169 | 170 | If you don't want to use the `--version` option on every update or deletion, the alternative is to use `--force`. When the `--force` option is present the CLI will automatically use the latest version of the extension. Be aware that using `--force` option might lead to accidental overwrites if multiple people are working on the same extension. 171 | 172 | ### Programmatic usage 173 | 174 | You can also use CLI's methods with a programmatic interface (for example in your build process). A client can be created simply by requiring `contentful-extension-cli` npm package: 175 | 176 | ```js 177 | const cli = require('contentful-extension-cli'); 178 | 179 | const client = cli.createClient({ 180 | accessToken: process.env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN, 181 | spaceId: 'xxxyyyzzz', 182 | host: 'https://api.contentful.com' // optional, default value shown 183 | }); 184 | 185 | // getting an array of all extensions in the space 186 | client.getAll().then(function (extensions) {}); 187 | 188 | // getting a single extension 189 | client.get(extensionIs).then(function (extension) {}); 190 | 191 | // save method takes an object of extension properties described above 192 | client.save({ 193 | id: 'test-id', 194 | name: 'test', 195 | src: 'https://extension.example' 196 | }).then(function (savedExtension) {}); 197 | 198 | // the only difference is that srcdoc is a HTML document string 199 | // instead of a path (so it can be fed with custom build data) 200 | client.save({ 201 | id: 'test-id', 202 | name: 'test', 203 | srcdoc: '

test...' 204 | }).then(function (savedExtension) {}); 205 | 206 | // if extension was saved, a result of the get method call will contain 207 | // version that has to be supplied to the consecutive save call 208 | client.save({ 209 | id: 'test-id', 210 | name: 'test', 211 | src: 'https://extension.example', 212 | version: 123 213 | }).then(function (savedExtension) {}); 214 | 215 | // delete method also requires a version number 216 | client.delete(extensionIs, currentExtensionVersion).then(function () {}); 217 | ``` 218 | -------------------------------------------------------------------------------- /test/integration/descriptor-file-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var temp = require('temp'); 4 | var _ = require('lodash'); 5 | var Bluebird = require('bluebird'); 6 | var fs = Bluebird.promisifyAll(require('fs')); 7 | var path = require('path'); 8 | 9 | var command = require('./helpers/command'); 10 | var chai = require('../helper'); 11 | var expect = chai.expect; 12 | var assert = chai.assert; 13 | 14 | var server = require('./http-server'); 15 | 16 | 17 | function example (options, test) { 18 | Object.keys(options).forEach(function (key) { 19 | let commands = options[key]; 20 | 21 | if (!_.isArray(commands)) { 22 | commands = [commands]; 23 | } 24 | 25 | test(key, commands); 26 | }); 27 | } 28 | 29 | function runCommands (commands, execOptions) { 30 | return function () { 31 | return Bluebird.reduce(commands, function (acc, c) { 32 | return command(c, execOptions); 33 | }, []); // give some non 'undefined' initial value 34 | }; 35 | } 36 | 37 | describe('Descriptor file', function () { 38 | this.timeout(6000); 39 | 40 | beforeEach(function () { 41 | server.start(); 42 | }); 43 | 44 | afterEach(function () { 45 | server.stop(); 46 | }); 47 | 48 | let execOptions; 49 | 50 | beforeEach(function () { 51 | let env = _.clone(process.env); 52 | 53 | env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN = 'lol-token'; 54 | 55 | execOptions = {env: env}; 56 | }); 57 | 58 | describe('when a descriptor file is given', function () { 59 | let customDescriptor; 60 | let customDescriptorPath = path.resolve(process.cwd(), 'descriptor.json'); 61 | 62 | beforeEach(function () { 63 | customDescriptor = { 64 | id: '123', 65 | src: 'foo.com', 66 | name: 'foo', 67 | fieldTypes: ['Symbol', 'Assets'], 68 | sidebar: true 69 | }; 70 | 71 | return fs.writeFileAsync(customDescriptorPath, JSON.stringify(customDescriptor)); 72 | }); 73 | 74 | afterEach(function () { 75 | return fs.unlinkAsync(customDescriptorPath); 76 | }); 77 | 78 | example({ 79 | create: [ 80 | `create --space-id 123 --descriptor ${customDescriptorPath} --host http://localhost:3000`, 81 | 'read --space-id 123 --id 123 --host http://localhost:3000' 82 | ], 83 | update: [ 84 | 'create --space-id 123 --id 123 --name lol --src foo.com --field-types Symbol --host http://localhost:3000', 85 | `update --space-id 123 --descriptor ${customDescriptorPath} --force --host http://localhost:3000`, 86 | 'read --space-id 123 --id 123 --host http://localhost:3000' 87 | ] 88 | }, 89 | function (commandName, commands) { 90 | it(`${commandName}s an extension`, function () { 91 | return runCommands(commands, execOptions)() 92 | .then(function (stdout) { 93 | let payload = JSON.parse(stdout); 94 | 95 | expect(payload.extension.name).to.eql(customDescriptor.name); 96 | expect(payload.extension.src).to.eql(customDescriptor.src); 97 | expect(payload.extension.fieldTypes).to.eql([ 98 | {type: 'Symbol'}, 99 | {type: 'Array', items: {type: 'Link', linkType: 'Asset'}} 100 | ]); 101 | expect(payload.extension.sidebar).to.be.true(); 102 | expect(payload.sys.id).to.eql(customDescriptor.id); 103 | expect(payload.sys.space.sys.id).to.eql('123'); 104 | }); 105 | }); 106 | }); 107 | }); 108 | 109 | describe('when the descriptor file does not exist', function () { 110 | let customDescriptorPath = path.resolve(process.cwd(), 'descriptor.json'); 111 | 112 | example( 113 | { 114 | create: `create --space-id 123 --descriptor ${customDescriptorPath}`, 115 | update: `update --space-id 123 --descriptor ${customDescriptorPath}` 116 | }, 117 | function (commandName, commands) { 118 | it(`${commandName}s returns an error`, function () { 119 | return runCommands(commands, execOptions)() 120 | .then(assert.fail) 121 | .catch(function (error) { 122 | let cause = `ENOENT: no such file or directory, stat '${customDescriptorPath}'`; 123 | let msg = `Failed to ${commandName} the extension: ${cause}`; 124 | 125 | expect(error.error.code).to.eq(1); 126 | expect(error.stderr).to.include(msg); 127 | }); 128 | }); 129 | } 130 | ); 131 | }); 132 | 133 | describe('when the cli can not open the file', function () { 134 | let customDescriptorPath = path.resolve(process.cwd(), 'descriptor.json'); 135 | 136 | beforeEach(function () { 137 | return fs.writeFileAsync(customDescriptorPath, JSON.stringify({})) 138 | .then(function () { 139 | return fs.chmodAsync(customDescriptorPath, '300'); 140 | }); 141 | }); 142 | 143 | afterEach(function () { 144 | return fs.unlinkAsync(customDescriptorPath); 145 | }); 146 | 147 | example( 148 | { 149 | create: `create --space-id 123 --descriptor ${customDescriptorPath}`, 150 | update: `update --space-id 123 --descriptor ${customDescriptorPath}` 151 | }, 152 | function (commandName, commands) { 153 | it(`${commandName} returns an error`, function () { 154 | return runCommands(commands, execOptions)() 155 | .then(assert.fail) 156 | .catch(function (error) { 157 | let cause = `EACCES: permission denied, open \'.+\/descriptor\.json\'`; 158 | let msg = new RegExp(`Failed to ${commandName} the extension: ${cause}`); 159 | 160 | expect(error.error.code).to.eq(1); 161 | expect(error.stderr).to.match(msg); 162 | }); 163 | }); 164 | } 165 | ); 166 | }); 167 | 168 | describe('when there is an "extension.json" file present', function () { 169 | let file, descriptor; 170 | 171 | beforeEach(function () { 172 | descriptor = { 173 | id: '456', 174 | src: 'lol.com', 175 | name: 'foo', 176 | fieldTypes: ['Symbol', 'Assets'], 177 | sidebar: true 178 | }; 179 | 180 | file = path.resolve(process.cwd(), 'extension.json'); 181 | return fs.writeFileAsync(file, JSON.stringify(descriptor)); 182 | }); 183 | 184 | afterEach(function () { 185 | return fs.unlinkAsync(file); 186 | }); 187 | 188 | example( 189 | { 190 | create: [ 191 | 'create --space-id 123 --host http://localhost:3000', 192 | 'read --space-id 123 --id 456 --host http://localhost:3000' 193 | ], 194 | update: [ 195 | 'create --space-id 123 --src foo.com --host http://localhost:3000', 196 | 'update --space-id 123 --force --host http://localhost:3000', 197 | 'read --space-id 123 --id 456 --host http://localhost:3000' 198 | ] 199 | }, 200 | function (commandName, commands) { 201 | it(`${commandName}s the extension using the values in descriptor file`, function () { 202 | return runCommands(commands, execOptions)() 203 | .then(function (stdout) { 204 | let payload = JSON.parse(stdout); 205 | 206 | expect(payload.extension.name).to.eql(descriptor.name); 207 | expect(payload.extension.src).to.eql(descriptor.src); 208 | expect(payload.sys.id).to.eql(descriptor.id); 209 | expect(payload.extension.fieldTypes).to.eql([ 210 | {type: 'Symbol'}, 211 | {type: 'Array', items: {type: 'Link', linkType: 'Asset'}} 212 | ]); 213 | }); 214 | }); 215 | } 216 | ); 217 | 218 | describe('when the descriptor file has the srcdoc property set', function () { 219 | let srdoc, bundle; 220 | 221 | beforeEach(function () { 222 | srdoc = temp.path(); 223 | bundle = 'the-bundle-contents'; 224 | 225 | return fs.writeFileAsync(srdoc, bundle); 226 | }); 227 | 228 | afterEach(function () { 229 | return fs.unlinkAsync(srdoc); 230 | }); 231 | 232 | example( 233 | { 234 | create: [ 235 | 'create --space-id 123 --host http://localhost:3000', 236 | 'read --space-id 123 --id 456 --host http://localhost:3000' 237 | ], 238 | update: [ 239 | 'create --space-id 123 --id 456 --src foo.com --host http://localhost:3000', 240 | 'update --space-id 123 --force --host http://localhost:3000', 241 | 'read --space-id 123 --id 456 --host http://localhost:3000' 242 | ] 243 | }, 244 | function (commandName, commands) { 245 | it(`${commandName}s the extension using the values in the descriptor file`, function () { 246 | delete descriptor.src; 247 | descriptor.srcdoc = srdoc; 248 | 249 | return fs.writeFileAsync(file, JSON.stringify(descriptor)) 250 | .then(runCommands(commands, execOptions)) 251 | .then(function (stdout) { 252 | let payload = JSON.parse(stdout); 253 | 254 | expect(payload.extension.srcdoc).to.eql(bundle); 255 | expect(payload.sys.id).to.eql(descriptor.id); 256 | }); 257 | }); 258 | } 259 | ); 260 | 261 | example( 262 | { 263 | create: [ 264 | 'create --space-id 123 --src foo.com --host http://localhost:3000', 265 | 'read --space-id 123 --id 456 --host http://localhost:3000' 266 | ], 267 | update: [ 268 | 'create --space-id 123 --src wow.com --host http://localhost:3000', 269 | 'update --space-id 123 --src foo.com --force --host http://localhost:3000', 270 | 'read --space-id 123 --id 456 --host http://localhost:3000' 271 | ] 272 | }, 273 | function (commandName, commands) { 274 | it(`${commandName} --src excludes the srdoc property in the descriptor`, function () { 275 | delete descriptor.src; 276 | descriptor.srcdoc = srdoc; 277 | 278 | return fs.writeFileAsync(file, JSON.stringify(descriptor)) 279 | .then(runCommands(commands, execOptions)) 280 | .then(function (stdout) { 281 | let payload = JSON.parse(stdout); 282 | 283 | expect(payload.extension).to.not.have.ownProperty('srcdoc'); 284 | expect(payload.extension.src).to.eql('foo.com'); 285 | expect(payload.sys.id).to.eql(descriptor.id); 286 | }); 287 | }); 288 | } 289 | ); 290 | }); 291 | 292 | example( 293 | { 294 | create: [ 295 | 'create --space-id 123 --id 456 --src foo.com --host http://localhost:3000', 296 | 'read --space-id 123 --id 456 --host http://localhost:3000' 297 | ], 298 | update: [ 299 | 'create --space-id 123 --id 456 --src wow.com --host http://localhost:3000', 300 | 'update --space-id 123 --id 456 --src foo.com --force --host http://localhost:3000', 301 | 'read --space-id 123 --id 456 --host http://localhost:3000' 302 | ] 303 | }, 304 | function (commandName, commands) { 305 | it(`${commandName} --src option overwrites src property in the descriptor`, function () { 306 | return runCommands(commands, execOptions)() 307 | .then(function (stdout) { 308 | let payload = JSON.parse(stdout); 309 | 310 | expect(payload.extension.src).to.eql('foo.com'); 311 | expect(payload.sys.id).to.eql(descriptor.id); 312 | }); 313 | }); 314 | } 315 | ); 316 | 317 | example( 318 | { 319 | create: [ 320 | 'create --space-id 123 --id 456 --name doge --host http://localhost:3000', 321 | 'read --space-id 123 --id 456 --host http://localhost:3000' 322 | ], 323 | update: [ 324 | 'create --space-id 123 --id 456 --host http://localhost:3000', 325 | 'update --space-id 123 --id 456 --name doge --force --host http://localhost:3000', 326 | 'read --space-id 123 --id 456 --host http://localhost:3000' 327 | ] 328 | }, 329 | function (commandName, commands) { 330 | it(`${commandName} --name option overwrites name property in the descriptor`, function () { 331 | return runCommands(commands, execOptions)() 332 | .then(function (stdout) { 333 | let payload = JSON.parse(stdout); 334 | 335 | expect(payload.extension.name).to.eql('doge'); 336 | expect(payload.sys.id).to.eql(descriptor.id); 337 | }); 338 | }); 339 | } 340 | ); 341 | 342 | example( 343 | { 344 | create: [ 345 | 'create --space-id 123 --field-types Number Date --host http://localhost:3000', 346 | 'read --space-id 123 --id 456 --host http://localhost:3000' 347 | ], 348 | update: [ 349 | 'create --space-id 123 --id 456 --host http://localhost:3000', 350 | 'update --space-id 123 --id 456 --field-types Number Date --force --host http://localhost:3000', 351 | 'read --space-id 123 --id 456 --host http://localhost:3000' 352 | ] 353 | }, 354 | function (commandName, commands) { 355 | it(`${commandName} --field-types option overwrites fieldTypes property in the descriptor`, function () { 356 | return runCommands(commands, execOptions)() 357 | .then(function (stdout) { 358 | let payload = JSON.parse(stdout); 359 | 360 | expect(payload.extension.fieldTypes).to.eql([ 361 | {type: 'Number'}, 362 | {type: 'Date'} 363 | ]); 364 | expect(payload.sys.id).to.eql(descriptor.id); 365 | }); 366 | }); 367 | } 368 | ); 369 | 370 | describe('when the --srcdoc option is used', function () { 371 | let srcdoc, bundle, f, b; 372 | 373 | f = temp.path(); 374 | 375 | beforeEach(function () { 376 | srcdoc = temp.path(); 377 | bundle = 'the-bundle-contents'; 378 | b = 'another-bundle'; 379 | 380 | return Bluebird.all([ 381 | fs.writeFileAsync(srcdoc, bundle), 382 | fs.writeFileAsync(f, b) 383 | ]); 384 | }); 385 | 386 | afterEach(function () { 387 | return Bluebird.all([ 388 | fs.unlinkAsync(srcdoc), 389 | fs.unlinkAsync(f) 390 | ]); 391 | }); 392 | 393 | example( 394 | { 395 | create: [ 396 | `create --space-id 123 --srcdoc ${f} --host http://localhost:3000`, 397 | 'read --space-id 123 --id 456 --host http://localhost:3000' 398 | ], 399 | update: [ 400 | 'create --space-id 123 --id 456 --host http://localhost:3000', 401 | `update --space-id 123 --srcdoc ${f} --force --host http://localhost:3000`, 402 | 'read --space-id 123 --id 456 --host http://localhost:3000' 403 | ] 404 | }, 405 | function (commandName, commands) { 406 | it(`${commandName} --srcdoc option overwrites srdoc property in the descriptor`, function () { 407 | delete descriptor.src; 408 | descriptor.srcdoc = srcdoc; 409 | 410 | return fs.writeFileAsync(file, JSON.stringify(descriptor)) 411 | .then(runCommands(commands, execOptions)) 412 | .then(function (stdout) { 413 | let payload = JSON.parse(stdout); 414 | 415 | expect(payload.extension.srcdoc).to.eql(b); 416 | expect(payload.sys.id).to.eql(descriptor.id); 417 | }); 418 | }); 419 | } 420 | ); 421 | 422 | example( 423 | { 424 | create: [ 425 | `create --space-id 123 --id 456 --srcdoc ${f} --host http://localhost:3000`, 426 | 'read --space-id 123 --id 456 --host http://localhost:3000' 427 | ], 428 | update: [ 429 | 'create --space-id 123 --id 456 --host http://localhost:3000', 430 | `update --space-id 123 --srcdoc ${f} --force --host http://localhost:3000`, 431 | 'read --space-id 123 --id 456 --host http://localhost:3000' 432 | ] 433 | }, 434 | function (commandName, commands) { 435 | it(`${commandName} --srcdoc excludes the src property in the descriptor`, function () { 436 | return fs.writeFileAsync(file, JSON.stringify(descriptor)) 437 | .then(runCommands(commands, execOptions)) 438 | .then(function (stdout) { 439 | let payload = JSON.parse(stdout); 440 | 441 | expect(payload.extension).to.not.have.ownProperty('src'); 442 | expect(payload.extension.srcdoc).to.eql(b); 443 | expect(payload.sys.id).to.eql(descriptor.id); 444 | }); 445 | }); 446 | } 447 | ); 448 | }); 449 | 450 | example( 451 | { 452 | create: [ 453 | 'create --space-id 123 --id 88 --host http://localhost:3000', 454 | 'read --space-id 123 --id 88 --host http://localhost:3000' 455 | ], 456 | update: [ 457 | // TODO: use a different file when updating (or modify the 458 | // existing) one as now we are using the same descriptor file 459 | 'create --space-id 123 --id 88 --host http://localhost:3000', 460 | 'update --space-id 123 --id 88 --force --host http://localhost:3000', 461 | 'read --space-id 123 --id 88 --host http://localhost:3000' 462 | ] 463 | }, 464 | function (commandName, commands) { 465 | it(`${commandName} --id option overwrites id property in the descriptor`, function () { 466 | return runCommands(commands, execOptions)() 467 | .then(function (stdout) { 468 | let payload = JSON.parse(stdout); 469 | 470 | expect(payload.extension.src).to.eql(descriptor.src); 471 | expect(payload.sys.id).to.eql('88'); 472 | }); 473 | }); 474 | } 475 | ); 476 | 477 | example( 478 | { 479 | create: [ 480 | 'create --space-id 123 --id 88 --no-sidebar --host http://localhost:3000', 481 | 'read --space-id 123 --id 88 --host http://localhost:3000' 482 | ], 483 | update: [ 484 | 'create --space-id 123 --id 88 --name foo --host http://localhost:3000', 485 | 'update --space-id 123 --id 88 --no-sidebar --force --host http://localhost:3000', 486 | 'read --space-id 123 --id 88 --host http://localhost:3000' 487 | ] 488 | }, 489 | function (commandName, commands) { 490 | it(`${commandName} --sidebar option overwrites sidebar property in the descriptor`, function () { 491 | return runCommands(commands, execOptions)() 492 | .then(function (stdout) { 493 | let payload = JSON.parse(stdout); 494 | 495 | expect(payload.extension.sidebar).to.be.false(); 496 | }); 497 | }); 498 | } 499 | ); 500 | 501 | example( 502 | { 503 | create: 'create --space-id 123 --host http://localhost:3000', 504 | update: 'update --space-id 123 --host http://localhost:3000' 505 | 506 | }, 507 | function (commandName, commands) { 508 | it(`${commandName} errors when the descriptor file is not valid JSON`, function () { 509 | return fs.writeFileAsync(file, 'not-valid-json') 510 | .then(runCommands(commands, execOptions)) 511 | .then(assert.fail) 512 | .catch(function (error) { 513 | let cause = 'In file extension\.json: Unexpected token o'; 514 | let regexp = new RegExp(`Failed to ${commandName} the extension: ${cause}`); 515 | 516 | expect(error.error.code).to.eq(1); 517 | expect(error.stderr).to.match(regexp); 518 | }); 519 | }); 520 | } 521 | ); 522 | 523 | example( 524 | { 525 | create: 'create --space-id 123 --host http://localhost:3000', 526 | update: 'update --space-id 123 --host http://localhost:3000' 527 | }, 528 | function (commandName, commands) { 529 | it(`${commandName} errors when there are missing properties on the file (id)`, function () { 530 | descriptor = {src: 'foo.com'}; 531 | 532 | return fs.writeFileAsync(file, JSON.stringify(descriptor)) 533 | .then(runCommands(commands, execOptions)) 534 | .then(assert.fail) 535 | .catch(function (error) { 536 | let regexp = new RegExp(`Failed to ${commandName} the extension: Missing extension ID in descriptor file`); 537 | 538 | expect(error.error.code).to.eq(1); 539 | expect(error.stderr).to.match(regexp); 540 | }); 541 | }); 542 | } 543 | ); 544 | 545 | example( 546 | { 547 | create: 'create --space-id 123 --host http://localshot:3000', 548 | update: 'update --space-id 123 --host http://localshot:3000' 549 | }, 550 | function (commandName, commands) { 551 | it('errors when there are missing properties on the file (src or srcdoc)', function () { 552 | descriptor = {id: 123}; 553 | 554 | return fs.writeFileAsync(file, JSON.stringify(descriptor)) 555 | .then(runCommands(commands, execOptions)) 556 | .then(assert.fail) 557 | .catch(function (error) { 558 | let msg = new RegExp(`Failed to ${commandName} the extension: Missing "src" or "srcdoc" property in descriptor file`); 559 | 560 | expect(error.error.code).to.eq(1); 561 | expect(error.stderr).to.match(msg); 562 | }); 563 | }); 564 | } 565 | ); 566 | 567 | describe('when the --descripor option is used', function () { 568 | describe('when the file exists', function () { 569 | let customDescriptor; 570 | let customDescriptorPath = path.resolve(process.cwd(), 'descriptor.json'); 571 | 572 | beforeEach(function () { 573 | customDescriptor = { 574 | id: 'desc-123', 575 | src: 'desc-foo.com', 576 | name: 'desc-foo', 577 | fieldTypes: ['Asset', 'Text'], 578 | sidebar: true 579 | }; 580 | 581 | return fs.writeFileAsync(customDescriptorPath, JSON.stringify(customDescriptor)); 582 | }); 583 | 584 | afterEach(function () { 585 | return fs.unlinkAsync(customDescriptorPath); 586 | }); 587 | 588 | example({ 589 | create: [ 590 | `create --space-id 123 --descriptor ${customDescriptorPath} --host http://localhost:3000`, 591 | 'read --space-id 123 --id desc-123 --host http://localhost:3000' 592 | ], 593 | update: [ 594 | 'create --space-id 123 --id desc-123 --name lol --src foo.com --host http://localhost:3000', 595 | `update --space-id 123 --descriptor ${customDescriptorPath} --force --host http://localhost:3000`, 596 | 'read --space-id 123 --id desc-123 --host http://localhost:3000' 597 | ] 598 | }, 599 | function (commandName, commands) { 600 | it(`${commandName}s an extension`, function () { 601 | return runCommands(commands, execOptions)() 602 | .then(function (stdout) { 603 | let payload = JSON.parse(stdout); 604 | 605 | expect(payload.extension.name).to.eql(customDescriptor.name); 606 | expect(payload.extension.src).to.eql(customDescriptor.src); 607 | expect(payload.extension.fieldTypes).to.eql([ 608 | {type: 'Link', linkType: 'Asset'}, 609 | {type: 'Text'} 610 | ]); 611 | expect(payload.extension.sidebar).to.be.true(); 612 | expect(payload.sys.id).to.eql(customDescriptor.id); 613 | expect(payload.sys.space.sys.id).to.eql('123'); 614 | 615 | expect(payload.extension.name).not.to.eql(descriptor.name); 616 | expect(payload.extension.src).not.to.eql(descriptor.src); 617 | expect(payload.extension.fieldTypes).not.to.eql(descriptor.fieldTypes); 618 | expect(payload.sys.id).not.to.eql(descriptor.id); 619 | }); 620 | }); 621 | }); 622 | }); 623 | 624 | describe('when file does not exist', function () { 625 | let customDescriptorPath = path.resolve(process.cwd(), 'missing-descriptor.json'); 626 | 627 | example( 628 | { 629 | create: `create --space-id 123 --descriptor ${customDescriptorPath}`, 630 | update: `update --space-id 123 --descriptor ${customDescriptorPath}` 631 | }, 632 | function (commandName, commands) { 633 | it(`${commandName}s returns an error`, function () { 634 | return runCommands(commands, execOptions)() 635 | .then(assert.fail) 636 | .catch(function (error) { 637 | let cause = `ENOENT: no such file or directory, stat '${customDescriptorPath}'`; 638 | let msg = new RegExp(`Failed to ${commandName} the extension: ${cause}`); 639 | 640 | expect(error.error.code).to.eq(1); 641 | expect(error.stderr).to.match(msg); 642 | }); 643 | }); 644 | } 645 | ); 646 | }); 647 | }); 648 | }); 649 | }); 650 | -------------------------------------------------------------------------------- /test/integration/commands-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var temp = require('temp'); 4 | var _ = require('lodash'); 5 | var Bluebird = require('bluebird'); 6 | var fs = Bluebird.promisifyAll(require('fs')); 7 | 8 | var commandsFlags = require('../../lib/bin-helpers/flags'); 9 | var command = require('./helpers/command'); 10 | var chai = require('../helper'); 11 | var expect = chai.expect; 12 | var assert = chai.assert; 13 | 14 | var server = require('./http-server'); 15 | 16 | function testHelpOutput (flags, output) { 17 | flags.forEach(function (flag) { 18 | let description = commandsFlags.options[flag].description; 19 | let regexp = new RegExp(`--${flag}\\s+${description}`); 20 | 21 | expect(output).to.match(regexp); 22 | }); 23 | } 24 | 25 | describe('Commands', function () { 26 | this.timeout(6000); 27 | 28 | beforeEach(function () { 29 | server.start(); 30 | }); 31 | 32 | afterEach(function () { 33 | server.stop(); 34 | }); 35 | 36 | let execOptions; 37 | 38 | beforeEach(function () { 39 | let env = _.clone(process.env); 40 | 41 | env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN = 'lol-token'; 42 | 43 | execOptions = {env: env}; 44 | }); 45 | 46 | ['create', 'update', 'delete', 'read', 'list'].forEach(function (subcommand) { 47 | describe('when the token is not defined on the environment', function () { 48 | it(`${subcommand} fails`, function () { 49 | delete execOptions.env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN; 50 | let msg = 'CONTENTFUL_MANAGEMENT_ACCESS_TOKEN is undefined or empty'; 51 | let command = `${subcommand} --space-id 123 --id 456`; 52 | 53 | return expectErrorAndMessage(command, execOptions, msg); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('Create', function () { 59 | var flags = [ 60 | 'space-id', 'id', 'src', 'srcdoc', 'name', 'host', 'sidebar', 61 | 'field-types', 'descriptor' 62 | ]; 63 | 64 | it('reads the host config from the environment', function () { 65 | execOptions.env.CONTENTFUL_MANAGEMENT_HOST = 'http://localhost:3000'; 66 | 67 | return command('create --space-id 123 --field-types Symbol --id 456 --name foo --src foo.com', execOptions) 68 | .then(function () { 69 | return command('read --space-id 123 --id 456', execOptions); 70 | }) 71 | .then(function (stdout) { 72 | let payload = JSON.parse(stdout); 73 | 74 | expect(payload.sys.id).to.eql('456'); 75 | expect(payload.extension.name).to.eql('foo'); 76 | expect(payload.extension.src).to.eql('foo.com'); 77 | }); 78 | }); 79 | 80 | it('--host option has precedence over the CONTENTFUL_MANAGEMENT_HOST option', function () { 81 | // no API listening on localhost:9999 82 | execOptions.env.CONTENTFUL_MANAGEMENT_HOST = 'http://localhost:9999'; 83 | 84 | return command('create --space-id 123 --id 456 --name foo --field-types Symbol --src foo.com --host http://localhost:3000', execOptions) 85 | .then(function () { 86 | return command('read --space-id 123 --id 456 --host http://localhost:3000', execOptions); 87 | }) 88 | .then(function (stdout) { 89 | let payload = JSON.parse(stdout); 90 | 91 | expect(payload.sys.id).to.eql('456'); 92 | expect(payload.extension.name).to.eql('foo'); 93 | expect(payload.extension.src).to.eql('foo.com'); 94 | }); 95 | }); 96 | 97 | it('shows the help when the --help flag is present', function () { 98 | // Use the --space-id flag because otherwise the help would be 99 | // shown because it's a required flag 100 | 101 | return command('create --space-id 123 --help', execOptions) 102 | .then(function (stdout) { 103 | testHelpOutput(flags, stdout); 104 | }); 105 | }); 106 | 107 | it('shows all the available options when no one is provided', function () { 108 | return command('create', execOptions) 109 | .then(assert.fail) 110 | .catch(function (error) { 111 | expect(error.error.code).to.eql(1); 112 | testHelpOutput(flags, error.stderr); 113 | }); 114 | }); 115 | 116 | it('fails if the --space-id option is not provided', function () { 117 | return command('create --src foo.com --host http://localhost:3000', execOptions) 118 | .then(assert.fail) 119 | .catch(function (error) { 120 | expect(error.error.code).to.eq(1); 121 | expect(error.stderr).to.match(/Missing required argument: space-id/); 122 | }); 123 | }); 124 | 125 | it('fails if no --src or --srcdoc options are provided', function () { 126 | return command('create --space-id 123 --host http://localhost:3000', execOptions) 127 | .then(assert.fail) 128 | .catch(function (error) { 129 | expect(error.error.code).to.eq(1); 130 | expect(error.stderr).to.match(/you're missing the following parameters: name, fieldTypes, src or srcdoc/); 131 | }); 132 | }); 133 | 134 | it('creates an extension', function () { 135 | // TODO add test that works with host without protocol 136 | return command('create --space-id 123 --field-types Symbol --src lol.com --name lol --host http://localhost:3000 --id 456', execOptions) 137 | .then(function () { 138 | let readCmd = 'read --space-id 123 --host http://localhost:3000 --id 456'; 139 | 140 | return command(readCmd, execOptions); 141 | }) 142 | .then(function (stdout) { 143 | let payload = JSON.parse(stdout); 144 | 145 | expect(payload.extension.src).to.eql('lol.com'); 146 | expect(payload.extension.name).to.eql('lol'); 147 | }); 148 | }); 149 | 150 | it('creates an extension with fieldTypes', function () { 151 | let cmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol Text --host http://localhost:3000 --id 456'; 152 | let readCmd = 'read --space-id 123 --host http://localhost:3000 --id 456'; 153 | 154 | return command(cmd, execOptions) 155 | .then(function () { 156 | return command(readCmd, execOptions); 157 | }) 158 | .then(function (stdout) { 159 | let payload = JSON.parse(stdout); 160 | 161 | expect(payload.extension.fieldTypes).to.eql([ 162 | {type: 'Symbol'}, 163 | {type: 'Text'} 164 | ]); 165 | }); 166 | }); 167 | 168 | it('creates an extension with the sidebar property set to true', function () { 169 | let cmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --sidebar --host http://localhost:3000 --id 456'; 170 | let readCmd = 'read --space-id 123 --host http://localhost:3000 --id 456'; 171 | 172 | return command(cmd, execOptions) 173 | .then(function () { 174 | return command(readCmd, execOptions); 175 | }) 176 | .then(function (stdout) { 177 | let payload = JSON.parse(stdout); 178 | 179 | expect(payload.extension.sidebar).to.be.true(); 180 | }); 181 | }); 182 | 183 | it('creates an extension with the sidebar property set to false', function () { 184 | let cmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --no-sidebar --host http://localhost:3000 --id 456'; 185 | let readCmd = 'read --space-id 123 --id 456 --host http://localhost:3000'; 186 | 187 | return command(cmd, execOptions) 188 | .then(function () { 189 | return command(readCmd, execOptions); 190 | }) 191 | .then(function (stdout) { 192 | let payload = JSON.parse(stdout); 193 | 194 | expect(payload.extension.sidebar).to.be.false(); 195 | }); 196 | }); 197 | 198 | it('creates an extension with the sidebar property set to undefined if no sidebar option', function () { 199 | let cmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --host http://localhost:3000 --id 456'; 200 | let readCmd = 'read --space-id 123 --id 456 --host http://localhost:3000'; 201 | 202 | return command(cmd, execOptions) 203 | .then(function () { 204 | return command(readCmd, execOptions); 205 | }) 206 | .then(function (stdout) { 207 | let payload = JSON.parse(stdout); 208 | 209 | expect(payload.extension.sidebar).to.be.undefined(); 210 | }); 211 | }); 212 | 213 | it('creates an extension with a custom id', function () { 214 | let cmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456 --host http://localhost:3000'; 215 | let readCmd = 'read --space-id 123 --id 456 --host http://localhost:3000'; 216 | 217 | return command(cmd, execOptions) 218 | .then(function () { 219 | return command(readCmd, execOptions); 220 | }) 221 | .then(function (stdout) { 222 | let payload = JSON.parse(stdout); 223 | 224 | expect(payload.extension.src).to.eql('lol.com'); 225 | expect(payload.sys.id).to.eql('456'); 226 | }); 227 | }); 228 | 229 | it('reports the error when the API request fails', function () { 230 | let cmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id fail --host http://localhost:3000'; 231 | let msg = serverErrorMsg('put', 'create', 'ServerError'); 232 | 233 | return expectErrorAndMessage(cmd, execOptions, msg); 234 | }); 235 | 236 | it('reports the error when supplied name is too long', function () { 237 | let cmd = 'create --space-id 123 --name imagine-there-is-300-chars --src lol.com --field-types Symbol --id too-long-name --host http://localhost:3000'; 238 | let msg = httpError('put', 'create', 'ValidationFailed', 'Provide a valid extension name (1-255 characters).'); 239 | 240 | return expectErrorAndMessage(cmd, execOptions, msg); 241 | }); 242 | 243 | it('reports the error when invalid field type is provided', function () { 244 | let cmd = 'create --space-id 123 --name lol --src lol.com --field-types Lol --host http://localhost:3000'; 245 | let msg = httpError('post', 'create', 'ValidationFailed', 'The "fieldTypes" extension property expects: Symbol,Yolo'); 246 | 247 | return expectErrorAndMessage(cmd, execOptions, msg); 248 | }); 249 | 250 | it('reports an unknown validation error', function () { 251 | let cmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id so-invalid --host http://localhost:3000'; 252 | let msg = httpError('put', 'create', 'ValidationFailed', 'An unknown validation error occurred.'); 253 | 254 | return expectErrorAndMessage(cmd, execOptions, msg); 255 | }); 256 | 257 | describe('when the --srcdoc option is used', function () { 258 | let file; 259 | 260 | beforeEach(function () { 261 | file = temp.path(); 262 | return fs.writeFileAsync(file, 'the-bundle-contents'); 263 | }); 264 | 265 | afterEach(function () { 266 | return fs.unlinkAsync(file); 267 | }); 268 | 269 | it('reports the error when the API request fails', function () { 270 | let cmd = `create --space-id 123 --name lol --srcdoc ${file} --field-types Symbol --id fail --host http://localhost:3000`; 271 | let msg = serverErrorMsg('put', 'create', 'ServerError'); 272 | 273 | return expectErrorAndMessage(cmd, execOptions, msg); 274 | }); 275 | 276 | it('reports the error when the file does not exist', function () { 277 | let cmd = 'create --space-id 123 --name lol --field-types Symbol --srcdoc some-unexisting-file --host http://localhost:3000'; 278 | let msg = 'Cannot read the file defined as the "srcdoc" property (some-unexisting-file)'; 279 | 280 | return expectErrorAndMessage(cmd, execOptions, msg); 281 | }); 282 | 283 | it('reports the error when the file is too big', function () { 284 | let cmd = `create --space-id 123 --name lol --field-types Symbol --srcdoc ${file} --host http://localhost:3000 --id too-big`; 285 | let msg = 'The "srcdoc" extension property must have at most 7777 characters.'; 286 | 287 | return expectErrorAndMessage(cmd, execOptions, msg); 288 | }); 289 | 290 | it('creates an extension from a file', function () { 291 | let cmd = `create --space-id 123 --name lol --srcdoc ${file} --field-types Symbol --host http://localhost:3000 --id 456`; 292 | let readCmd = 'read --space-id 123 --host http://localhost:3000 --id 456'; 293 | 294 | return command(cmd, execOptions) 295 | .then(function () { 296 | return command(readCmd, execOptions); 297 | }) 298 | .then(function (stdout) { 299 | let payload = JSON.parse(stdout); 300 | 301 | expect(payload.extension.srcdoc).to.eql('the-bundle-contents'); 302 | }); 303 | }); 304 | }); 305 | 306 | it('gives the create success message', function () { 307 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456 --host http://localhost:3000'; 308 | 309 | return command(createCmd, execOptions) 310 | .then(function (stdout) { 311 | expect(stdout).to.include('Successfully created extension, id: 456 name: lol'); 312 | }); 313 | }); 314 | }); 315 | 316 | describe('Read', function () { 317 | var flags = [ 'space-id', 'id', 'host', 'all' ]; 318 | 319 | it('reads the host config from the environment', function () { 320 | execOptions.env.CONTENTFUL_MANAGEMENT_HOST = 'http://localhost:3000'; 321 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456'; 322 | let readCmd = 'read --space-id 123 --id 456'; 323 | 324 | return command(createCmd, execOptions) 325 | .then(function () { 326 | return command(readCmd, execOptions); 327 | }) 328 | .then(function (stdout) { 329 | let payload = JSON.parse(stdout); 330 | 331 | expect(payload.extension.src).to.eql('lol.com'); 332 | expect(payload.sys.id).to.eql('456'); 333 | }); 334 | }); 335 | 336 | it('--host option has precedence over the CONTENTFUL_MANAGEMENT_HOST option', function () { 337 | // no API listening on localhost:9999 338 | execOptions.env.CONTENTFUL_MANAGEMENT_HOST = 'http://localhost:9999'; 339 | 340 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456 --host http://localhost:3000'; 341 | let readCmd = 'read --space-id 123 --id 456 --host http://localhost:3000'; 342 | 343 | return command(createCmd, execOptions) 344 | .then(function () { 345 | return command(readCmd, execOptions); 346 | }) 347 | .then(function (stdout) { 348 | let payload = JSON.parse(stdout); 349 | 350 | expect(payload.extension.src).to.eql('lol.com'); 351 | expect(payload.sys.id).to.eql('456'); 352 | }); 353 | }); 354 | 355 | it('shows the help when the --help flag is present', function () { 356 | // Use the --space-id flag because otherwise the help would be 357 | // shown because it's a required flag 358 | 359 | return command('read --space-id 123 --id 456 --help', execOptions) 360 | .then(function (stdout) { 361 | testHelpOutput(flags, stdout); 362 | }); 363 | }); 364 | 365 | it('shows all the available options when no one is provided', function () { 366 | return command('read', execOptions) 367 | .then(assert.fail) 368 | .catch(function (error) { 369 | expect(error.error.code).to.eql(1); 370 | testHelpOutput(flags, error.stderr); 371 | }); 372 | }); 373 | 374 | it('fails if the --space-id option is not provided', function () { 375 | return command('read --id 123 --host http://localhost:3000', execOptions) 376 | .then(assert.fail) 377 | .catch(function (error) { 378 | expect(error.error.code).to.eq(1); 379 | expect(error.stderr).to.match(/Missing required argument: space-id/); 380 | }); 381 | }); 382 | 383 | it('fails if no --id or --all options are provided', function () { 384 | return command('read --space-id 123 --src foo.com --host http://localhost:3000', execOptions) 385 | .then(assert.fail) 386 | .catch(function (error) { 387 | expect(error.error.code).to.eq(1); 388 | expect(error.stderr).to.match(/missing one of --id or --all options/); 389 | }); 390 | }); 391 | 392 | it('reports when the extension can not be found', function () { 393 | let cmd = 'read --space-id 123 --id not-found --host http://localhost:3000'; 394 | let msg = notFoundMsg('get', 'read'); 395 | 396 | return expectErrorAndMessage(cmd, execOptions, msg); 397 | }); 398 | 399 | it('reports the error when the API request fails', function () { 400 | let cmd = 'read --space-id 123 --id fail --host http://localhost:3000'; 401 | let msg = serverErrorMsg('get', 'read', 'ServerError'); 402 | 403 | return expectErrorAndMessage(cmd, execOptions, msg); 404 | }); 405 | 406 | it('reads an extension', function () { 407 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456 --host http://localhost:3000'; 408 | let readCmd = 'read --space-id 123 --id 456 --host http://localhost:3000'; 409 | 410 | return command(createCmd, execOptions) 411 | .then(function () { 412 | return command(readCmd, execOptions); 413 | }) 414 | .then(function (stdout) { 415 | let payload = JSON.parse(stdout); 416 | 417 | expect(payload.extension.src).to.eql('lol.com'); 418 | expect(payload.sys.id).to.eql('456'); 419 | }); 420 | }); 421 | 422 | it('reads all extension', function () { 423 | let createCmd1 = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456 --host http://localhost:3000'; 424 | let createCmd2 = 'create --space-id 123 --name foo --src foo.com --field-types Symbol --id 789 --host http://localhost:3000'; 425 | let readCmd = 'read --space-id 123 --all --host http://localhost:3000'; 426 | 427 | return Bluebird.all([ 428 | command(createCmd1, execOptions), 429 | command(createCmd2, execOptions) 430 | ]) 431 | .then(function () { 432 | return command(readCmd, execOptions); 433 | }) 434 | .then(function (stdout) { 435 | let payloads = JSON.parse(stdout); 436 | let lolExtension = _.find(payloads.items, {sys: {id: '456'}}); 437 | let fooExtension = _.find(payloads.items, {sys: {id: '789'}}); 438 | 439 | expect(payloads.total).to.eq(2); 440 | expect(lolExtension.extension.name).to.eql('lol'); 441 | expect(lolExtension.extension.src).to.eql('lol.com'); 442 | expect(fooExtension.extension.name).to.eql('foo'); 443 | expect(fooExtension.extension.src).to.eql('foo.com'); 444 | }); 445 | }); 446 | 447 | it('reports the error when the API request fails (reading all extensions)', function () { 448 | let cmd = 'read --space-id fail --all --host http://localhost:3000'; 449 | let msg = serverErrorMsg('get', 'read', 'ServerError'); 450 | 451 | return expectErrorAndMessage(cmd, execOptions, msg); 452 | }); 453 | }); 454 | 455 | describe('Update', function () { 456 | let flags = [ 457 | 'space-id', 'id', 'src', 'srcdoc', 'name', 'host', 'sidebar', 'field-types', 'descriptor', 458 | 'version', 'force' 459 | ]; 460 | 461 | it('reads the host config from the environment', function () { 462 | execOptions.env.CONTENTFUL_MANAGEMENT_HOST = 'http://localhost:3000'; 463 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456'; 464 | let updateCmd = 'update --space-id 123 --name lol --id 456 --version 1 --src foo.com --field-types Symbol'; 465 | let readCmd = 'read --space-id 123 --id 456'; 466 | 467 | return command(createCmd, execOptions) 468 | .then(function () { 469 | return command(updateCmd, execOptions); 470 | }) 471 | .then(function () { 472 | return command(readCmd, execOptions); 473 | }) 474 | .then(function (stdout) { 475 | let payload = JSON.parse(stdout); 476 | 477 | expect(payload.extension.src).to.eql('foo.com'); 478 | }); 479 | }); 480 | 481 | it('--host option has precedence over the CONTENTFUL_MANAGEMENT_HOST opion', function () { 482 | // no API listening on localhost:9999 483 | execOptions.env.CONTENTFUL_MANAGEMENT_HOST = 'http://localhost:9999'; 484 | 485 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456 --host http://localhost:3000'; 486 | let updateCmd = 'update --space-id 123 --name foo --src foo.com --field-types Symbol --id 456 --host http://localhost:3000 --force'; 487 | let readCmd = 'read --space-id 123 --id 456 --host http://localhost:3000'; 488 | 489 | return command(createCmd, execOptions) 490 | .then(function () { 491 | return command(updateCmd, execOptions); 492 | }) 493 | .then(function () { 494 | return command(readCmd, execOptions); 495 | }) 496 | .then(function (stdout) { 497 | let payload = JSON.parse(stdout); 498 | 499 | expect(payload.extension.src).to.eql('foo.com'); 500 | expect(payload.sys.id).to.eql('456'); 501 | }); 502 | }); 503 | 504 | it('shows the help when the --help flag is present', function () { 505 | // Use the --space-id flag because otherwise the help would be 506 | // shown because it's a required flag 507 | 508 | return command('update --space-id 123 --help', execOptions) 509 | .then(function (stdout) { 510 | testHelpOutput(flags, stdout); 511 | }); 512 | }); 513 | 514 | it('shows all the available options when no one is provided', function () { 515 | return command('update', execOptions) 516 | .then(assert.fail) 517 | .catch(function (error) { 518 | expect(error.error.code).to.eql(1); 519 | testHelpOutput(flags, error.stderr); 520 | }); 521 | }); 522 | 523 | it('fails if the --space-id option is not provided', function () { 524 | return command('update --id 123 --name lol --field-types Symbol --src foo.com --host http://localhost:3000', execOptions) 525 | .then(assert.fail) 526 | .catch(function (error) { 527 | expect(error.error.code).to.eq(1); 528 | expect(error.stderr).to.match(/Missing required argument: space-id/); 529 | }); 530 | }); 531 | 532 | it('fails if no --name option is provided', function () { 533 | let cmd = 'update --space-id 123 --id 456 --src foo.com --field-types Symbol --host http://localhost:3000'; 534 | let msg = `you're missing the following parameters: name`; 535 | 536 | return expectErrorAndMessage(cmd, execOptions, msg); 537 | }); 538 | 539 | it('fails if no --id option is provided', function () { 540 | let cmd = 'update --space-id 123 --name lol --src foo.com --field-types Symbol --host http://localhost:3000'; 541 | let msg = `you're missing the following parameters: id`; 542 | 543 | return expectErrorAndMessage(cmd, execOptions, msg); 544 | }); 545 | 546 | it('fails if no --srcdoc or --src options are provided', function () { 547 | let cmd = 'update --space-id 123 --name lol --field-types Symbol --id 123 --force --host http://localhost:3000'; 548 | let msg = `you're missing the following parameters: src or srcdoc`; 549 | 550 | return expectErrorAndMessage(cmd, execOptions, msg); 551 | }); 552 | 553 | it('fails if no --field-types option is provided', function () { 554 | let cmd = 'update --space-id 123 --name lol --src lol.com --id 123 --host http://localhost:3000'; 555 | let msg = `you're missing the following parameters: fieldTypes`; 556 | 557 | return expectErrorAndMessage(cmd, execOptions, msg); 558 | }); 559 | 560 | it('reports the error when the API request fails (without version, reading current)', function () { 561 | let cmd = 'update --space-id 123 --name lol --src lol.com --field-types Symbol --id fail --force --host http://localhost:3000'; 562 | let msg = serverErrorMsg('get', 'update', 'ServerError'); 563 | 564 | return expectErrorAndMessage(cmd, execOptions, msg); 565 | }); 566 | 567 | it('reports the error when the API request fails (without version)', function () { 568 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id fail-update --host http://localhost:3000'; 569 | let updateCmd = 'update --space-id 123 --name lol --id fail-update --src foo.com --field-types Symbol --force --host http://localhost:3000'; 570 | let msg = serverErrorMsg('put', 'update', 'ServerError'); 571 | 572 | return command(createCmd, execOptions) 573 | .then(function () { 574 | return expectErrorAndMessage(updateCmd, execOptions, msg); 575 | }); 576 | }); 577 | 578 | it('reports the error when the API request fails (with version)', function () { 579 | let cmd = 'update --space-id 123 --name lol --src lol.com --version 1 --field-types Symbol --id fail --host http://localhost:3000'; 580 | let msg = serverErrorMsg('put', 'update', 'ServerError'); 581 | 582 | return expectErrorAndMessage(cmd, execOptions, msg); 583 | }); 584 | 585 | it('updates an extension passing the version', function () { 586 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456 --host http://localhost:3000'; 587 | let updateCmd = 'update --space-id 123 --name foo --id 456 --version 1 --src foo.com --field-types Symbol --host http://localhost:3000'; 588 | let readCmd = 'read --space-id 123 --id 456 --host http://localhost:3000'; 589 | 590 | return command(createCmd, execOptions) 591 | .then(function () { 592 | return command(updateCmd, execOptions); 593 | }) 594 | .then(function () { 595 | return command(readCmd, execOptions); 596 | }) 597 | .then(function (stdout) { 598 | let payload = JSON.parse(stdout); 599 | 600 | expect(payload.extension.name).to.eql('foo'); 601 | expect(payload.extension.src).to.eql('foo.com'); 602 | }); 603 | }); 604 | 605 | it('fails to update the extension if no version is given and force option not present', function () { 606 | let cmd = 'update --space-id 123 --id 456 --name lol --field-types Symbol --src foo.com --host http://localhost:3000'; 607 | let msg = 'to update without version use the --force flag'; 608 | 609 | return expectErrorAndMessage(cmd, execOptions, msg); 610 | }); 611 | 612 | it('updates an extension without explicitely giving it version', function () { 613 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456 --host http://localhost:3000'; 614 | let updateCmd = 'update --space-id 123 --name foo --id 456 --src foo.com --field-types Symbol --force --host http://localhost:3000'; 615 | let readCmd = 'read --space-id 123 --id 456 --host http://localhost:3000'; 616 | 617 | return command(createCmd, execOptions) 618 | .then(function () { 619 | return command(updateCmd, execOptions); 620 | }) 621 | .then(function () { 622 | return command(readCmd, execOptions); 623 | }) 624 | .then(function (stdout) { 625 | let payload = JSON.parse(stdout); 626 | 627 | expect(payload.extension.name).to.eql('foo'); 628 | expect(payload.extension.src).to.eql('foo.com'); 629 | }); 630 | }); 631 | 632 | it('returns an error if neither descriptor or options are present', function () { 633 | let cmd = 'update --space-id 123 --name lol --id 456'; 634 | let msg = `you're missing the following parameters: fieldTypes, src or srcdoc`; 635 | 636 | return expectErrorAndMessage(cmd, execOptions, msg); 637 | }); 638 | 639 | it('updates the name of an extension', function () { 640 | let createCmd = 'create --space-id 123 --name lol --src l.com --field-types Symbol --id 456 --host http://localhost:3000'; 641 | let updateCmd = 'update --space-id 123 --name doge --src l.com --id 456 --field-types Symbol --force --host http://localhost:3000'; 642 | let readCmd = 'read --space-id 123 --id 456 --host http://localhost:3000'; 643 | 644 | return command(createCmd, execOptions) 645 | .then(function () { 646 | return command(updateCmd, execOptions); 647 | }) 648 | .then(function () { 649 | return command(readCmd, execOptions); 650 | }) 651 | .then(function (stdout) { 652 | let payload = JSON.parse(stdout); 653 | 654 | expect(payload.extension.name).to.eql('doge'); 655 | }); 656 | }); 657 | 658 | it('updates the fieldTypes of an extension', function () { 659 | let createCmd = 'create --space-id 123 --name lol --src l.com --id 456 --field-types Symbol --name foo --host http://localhost:3000'; 660 | let updateCmd = 'update --space-id 123 --name lol --src l.com --id 456 --field-types Text Symbol Assets --force --host http://localhost:3000'; 661 | let readCmd = 'read --space-id 123 --id 456 --host http://localhost:3000'; 662 | 663 | return command(createCmd, execOptions) 664 | .then(function () { 665 | return command(updateCmd, execOptions); 666 | }) 667 | .then(function () { 668 | return command(readCmd, execOptions); 669 | }) 670 | .then(function (stdout) { 671 | let payload = JSON.parse(stdout); 672 | 673 | expect(payload.extension.fieldTypes).to.eql([ 674 | {type: 'Text'}, 675 | {type: 'Symbol'}, 676 | {type: 'Array', items: {type: 'Link', linkType: 'Asset'}} 677 | ]); 678 | }); 679 | }); 680 | 681 | it('updates the sibebar property to true', function () { 682 | let createCmd = 'create --space-id 123 --name lol --src l.com --field-types Symbol --id 456 --name foo --no-sidebar --host http://localhost:3000'; 683 | let updateCmd = 'update --space-id 123 --name lol --src l.com --field-types Symbol --id 456 --sidebar --force --host http://localhost:3000'; 684 | let readCmd = 'read --space-id 123 --id 456 --host http://localhost:3000'; 685 | 686 | return command(createCmd, execOptions) 687 | .then(function () { 688 | return command(updateCmd, execOptions); 689 | }) 690 | .then(function () { 691 | return command(readCmd, execOptions); 692 | }) 693 | .then(function (stdout) { 694 | let payload = JSON.parse(stdout); 695 | 696 | expect(payload.extension.sidebar).to.be.true(); 697 | }); 698 | }); 699 | 700 | it('updates the sidebar property to false', function () { 701 | let createCmd = 'create --space-id 123 --name lol --src l.com --field-types Symbol --id 456 --name foo --sidebar --host http://localhost:3000'; 702 | let updateCmd = 'update --space-id 123 --name lol --src l.com --field-types Symbol --id 456 --no-sidebar --force --host http://localhost:3000'; 703 | let readCmd = 'read --space-id 123 --id 456 --host http://localhost:3000'; 704 | 705 | return command(createCmd, execOptions) 706 | .then(function () { 707 | return command(updateCmd, execOptions); 708 | }) 709 | .then(function () { 710 | return command(readCmd, execOptions); 711 | }) 712 | .then(function (stdout) { 713 | let payload = JSON.parse(stdout); 714 | 715 | expect(payload.extension.sidebar).to.be.false(); 716 | }); 717 | }); 718 | 719 | it('removes the sidebar property (when ommited)', function () { 720 | let createCmd = 'create --space-id 123 --name lol --src l.com --field-types Symbol --id 456 --name foo --sidebar --host http://localhost:3000'; 721 | let updateCmd = 'update --space-id 123 --name lol --src l.com --field-types Symbol --id 456 --name foo --force --host http://localhost:3000'; 722 | let readCmd = 'read --space-id 123 --id 456 --host http://localhost:3000'; 723 | 724 | return command(createCmd, execOptions) 725 | .then(function () { 726 | return command(updateCmd, execOptions); 727 | }) 728 | .then(function () { 729 | return command(readCmd, execOptions); 730 | }) 731 | .then(function (stdout) { 732 | let payload = JSON.parse(stdout); 733 | 734 | expect(payload.extension).to.not.have.ownProperty('sidebar'); 735 | }); 736 | }); 737 | 738 | describe('when the --srcdoc option is used', function () { 739 | let file; 740 | 741 | beforeEach(function () { 742 | file = temp.path(); 743 | return fs.writeFileAsync(file, 'the-bundle-contents'); 744 | }); 745 | 746 | afterEach(function () { 747 | return fs.unlinkAsync(file); 748 | }); 749 | 750 | it('reports the error when the API request fails (without version)', function () { 751 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id fail-update --host http://localhost:3000'; 752 | let updateCmd = `update --space-id 123 --name lol --field-types Symbol --id fail-update --srcdoc ${file} --force --host http://localhost:3000`; 753 | let msg = serverErrorMsg('put', 'update', 'ServerError'); 754 | 755 | return command(createCmd, execOptions) 756 | .then(function () { 757 | return expectErrorAndMessage(updateCmd, execOptions, msg); 758 | }); 759 | }); 760 | 761 | it('reports the error when the API request fails (without version, reading current)', function () { 762 | let updateCmd = `update --space-id 123 --name lol --field-types Symbol --id fail --srcdoc ${file} --force --host http://localhost:3000`; 763 | let msg = serverErrorMsg('get', 'update', 'ServerError'); 764 | 765 | return expectErrorAndMessage(updateCmd, execOptions, msg); 766 | }); 767 | 768 | it('reports the error when the API request fails (with version)', function () { 769 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id fail-update --host http://localhost:3000'; 770 | let updateCmd = `update --space-id 123 --name lol --src lol.com --version 1 --field-types Symbol --id fail-update --srcdoc ${file} --force --host http://localhost:3000`; 771 | let msg = serverErrorMsg('put', 'update', 'ServerError'); 772 | 773 | return command(createCmd, execOptions) 774 | .then(function () { 775 | return expectErrorAndMessage(updateCmd, execOptions, msg); 776 | }); 777 | }); 778 | 779 | it('reports the error when the file does not exist', function () { 780 | let cmd = 'update --space-id 123 --name lol --field-types Symbol --id 456 --srcdoc some-unexisting-file --force --host http://localhost:3000'; 781 | let msg = 'Cannot read the file defined as the "srcdoc" property (some-unexisting-file)'; 782 | 783 | return expectErrorAndMessage(cmd, execOptions, msg); 784 | }); 785 | 786 | it('updates an extension from a file without explicitely giving its version', function () { 787 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456 --host http://localhost:3000'; 788 | let updateCmd = `update --space-id 123 --name lol --field-types Symbol --id 456 --srcdoc ${file} --force --host http://localhost:3000`; 789 | let readCmd = 'read --space-id 123 --id 456 --host http://localhost:3000'; 790 | 791 | return command(createCmd, execOptions) 792 | .then(function () { 793 | return command(updateCmd, execOptions); 794 | }) 795 | .then(function () { 796 | return command(readCmd, execOptions); 797 | }) 798 | .then(function (stdout) { 799 | let payload = JSON.parse(stdout); 800 | 801 | expect(payload.extension.srcdoc).to.eql('the-bundle-contents'); 802 | }); 803 | }); 804 | }); 805 | 806 | it('gives the update success message', function () { 807 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456 --host http://localhost:3000'; 808 | let updateCmd = 'update --space-id 123 --name foo --src foo.com --field-types Symbol --id 456 --host http://localhost:3000 --force'; 809 | 810 | return command(createCmd, execOptions) 811 | .then(function (stdout) { 812 | return command(updateCmd, execOptions); 813 | }) 814 | .then(function (stdout) { 815 | expect(stdout).to.include('Successfully updated extension, id: 456 name: foo'); 816 | }); 817 | }); 818 | }); 819 | 820 | describe('Delete', function () { 821 | let flags = ['space-id', 'id', 'version', 'force', 'host']; 822 | 823 | it('reads the host config from the environment', function () { 824 | execOptions.env.CONTENTFUL_MANAGEMENT_HOST = 'http://localhost:3000'; 825 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456'; 826 | let deleteCmd = 'delete --space-id 123 --id 456 --version 1'; 827 | let readCmd = 'read --space-id 123 --all'; 828 | 829 | return command(createCmd, execOptions) 830 | .then(function () { 831 | return command(deleteCmd, execOptions); 832 | }) 833 | .then(function () { 834 | return command(readCmd, execOptions); 835 | }) 836 | .then(function (stdout) { 837 | let payload = JSON.parse(stdout); 838 | 839 | expect(payload.total).to.eql(0); 840 | expect(payload.items).to.be.empty(); 841 | }); 842 | }); 843 | 844 | it('--host option has precedence over the CONTENTFUL_MANAGEMENT_HOST option', function () { 845 | // no API listening on localhost:9999 846 | execOptions.env.CONTENTFUL_MANAGEMENT_HOST = 'http://localhost:9999'; 847 | 848 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456 --host http://localhost:3000'; 849 | let deleteCmd = 'delete --space-id 123 --id 456 --version 1 --host http://localhost:3000'; 850 | let readCmd = 'read --space-id 123 --all --host http://localhost:3000'; 851 | 852 | return command(createCmd, execOptions) 853 | .then(function () { 854 | return command(deleteCmd, execOptions); 855 | }) 856 | .then(function () { 857 | return command(readCmd, execOptions); 858 | }) 859 | .then(function (stdout) { 860 | let payload = JSON.parse(stdout); 861 | 862 | expect(payload.total).to.eql(0); 863 | expect(payload.items).to.be.empty(); 864 | }); 865 | }); 866 | 867 | it('shows the help when the --help flag is present', function () { 868 | // Use the --space-id and --id flags because otherwise the help would be 869 | // shown because they are required flags 870 | 871 | return command('delete --space-id 123 --id 456 --help', execOptions) 872 | .then(function (stdout) { 873 | testHelpOutput(flags, stdout); 874 | }); 875 | }); 876 | 877 | it('shows all the available options when no one is provided', function () { 878 | return command('delete', execOptions) 879 | .then(assert.fail) 880 | .catch(function (error) { 881 | expect(error.error.code).to.eql(1); 882 | testHelpOutput(flags, error.stderr); 883 | }); 884 | }); 885 | 886 | it('fails if the --space-id option is not provided', function () { 887 | return command('update --id 123 --src foo.com --host http://localhost:3000', execOptions) 888 | .then(assert.fail) 889 | .catch(function (error) { 890 | expect(error.error.code).to.eq(1); 891 | expect(error.stderr).to.match(/Missing required argument: space-id/); 892 | }); 893 | }); 894 | 895 | it('fails if no --id option is provided', function () { 896 | return command('delete --space-id 123 --src foo.com --host http://localhost:3000', execOptions) 897 | .then(assert.fail) 898 | .catch(function (error) { 899 | expect(error.error.code).to.eq(1); 900 | expect(error.stderr).to.match(/Missing required argument: id/); 901 | }); 902 | }); 903 | 904 | it('fails to delete the extension if no version is given and force option not present', function () { 905 | return command('delete --space-id 123 --src foo.com --id 456 --host http://localshot', execOptions) 906 | .then(assert.fail) 907 | .catch(function (error) { 908 | expect(error.error.code).to.eq(1); 909 | expect(error.stderr).to.match(/to delete without version use the --force flag/); 910 | }); 911 | }); 912 | 913 | it('reports the error when the API request fails (without version, reading current)', function () { 914 | let cmd = 'delete --space-id 123 --id fail --force --host http://localhost:3000'; 915 | let msg = serverErrorMsg('get', 'delete', 'ServerError'); 916 | 917 | return expectErrorAndMessage(cmd, execOptions, msg); 918 | }); 919 | 920 | it('reports the error when the API request fails (without version, deleting)', function () { 921 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id fail-delete --host http://localhost:3000'; 922 | let deleteCmd = 'delete --space-id 123 --id fail-delete --force --host http://localhost:3000'; 923 | let msg = serverErrorMsg('delete', 'delete', 'ServerError'); 924 | 925 | return command(createCmd, execOptions) 926 | .then(function () { 927 | return expectErrorAndMessage(deleteCmd, execOptions, msg); 928 | }); 929 | }); 930 | 931 | it('reports the error when the API request fails (without version, not found)', function () { 932 | let deleteCmd = 'delete --space-id 123 --force --id not-found --host http://localhost:3000'; 933 | let msg = notFoundMsg('get', 'delete'); 934 | 935 | return expectErrorAndMessage(deleteCmd, execOptions, msg); 936 | }); 937 | 938 | it('reports the error when the API request fails (with version, not found)', function () { 939 | let deleteCmd = 'delete --space-id 123 --version 1 --id not-found --host http://localhost:3000'; 940 | let msg = notFoundMsg('delete', 'delete'); 941 | 942 | return expectErrorAndMessage(deleteCmd, execOptions, msg); 943 | }); 944 | 945 | it('reports the error when the API request fails (with version)', function () { 946 | let deleteCmd = 'delete --space-id 123 --version 1 --id fail-delete --host http://localhost:3000'; 947 | let msg = serverErrorMsg('delete', 'delete', 'ServerError'); 948 | 949 | return expectErrorAndMessage(deleteCmd, execOptions, msg); 950 | }); 951 | 952 | it('deletes an extension', function () { 953 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456 --host http://localhost:3000'; 954 | let deleteCmd = 'delete --space-id 123 --id 456 --version 1 --host http://localhost:3000'; 955 | let readCmd = 'read --space-id 123 --all --host http://localhost:3000'; 956 | 957 | return command(createCmd, execOptions) 958 | .then(function () { 959 | return command(deleteCmd, execOptions); 960 | }) 961 | .then(function () { 962 | return command(readCmd, execOptions); 963 | }) 964 | .then(function (stdout) { 965 | let payload = JSON.parse(stdout); 966 | 967 | expect(payload.total).to.eql(0); 968 | expect(payload.items).to.be.empty(); 969 | }); 970 | }); 971 | 972 | it('deletes an extension without explicitely giving its version', function () { 973 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456 --host http://localhost:3000'; 974 | let deleteCmd = 'delete --space-id 123 --id 456 --force --host http://localhost:3000'; 975 | let readCmd = 'read --space-id 123 --all --host http://localhost:3000'; 976 | 977 | return command(createCmd, execOptions) 978 | .then(function () { 979 | return command(deleteCmd, execOptions); 980 | }) 981 | .then(function () { 982 | return command(readCmd, execOptions); 983 | }) 984 | .then(function (stdout) { 985 | let payload = JSON.parse(stdout); 986 | 987 | expect(payload.total).to.eql(0); 988 | expect(payload.items).to.be.empty(); 989 | }); 990 | }); 991 | 992 | it('gives the delete success message', function () { 993 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456 --host http://localhost:3000'; 994 | let deleteCmd = 'delete --space-id 123 --id 456 --force --host http://localhost:3000'; 995 | 996 | return command(createCmd, execOptions) 997 | .then(function () { 998 | return command(deleteCmd, execOptions); 999 | }) 1000 | .then(function (stdout) { 1001 | expect(stdout).to.include('Successfully deleted extension'); 1002 | }); 1003 | }); 1004 | }); 1005 | 1006 | describe('List', function () { 1007 | it('gives a message when no extensions are created yet', function () { 1008 | let listCmd = `list --space-id 123 --host http://localhost:3000`; 1009 | 1010 | return command(listCmd, execOptions) 1011 | .then(function (stdout) { 1012 | expect(stdout).to.include('No extensions for this space created yet.'); 1013 | }); 1014 | }); 1015 | 1016 | it('lists created extentions', function () { 1017 | let createCmd = 'create --space-id 123 --name lol --src lol.com --field-types Symbol --id 456 --host http://localhost:3000'; 1018 | let listCmd = `list --space-id 123 --host http://localhost:3000`; 1019 | 1020 | return command(createCmd, execOptions) 1021 | .then(function () { 1022 | return command(listCmd, execOptions); 1023 | }) 1024 | .then(function (stdout) { 1025 | expect(stdout).to.include('lol'); 1026 | expect(stdout).to.include('456'); 1027 | }) 1028 | .catch(function (err) { 1029 | console.log(err); 1030 | }); 1031 | }); 1032 | }); 1033 | }); 1034 | 1035 | function expectErrorAndMessage (commandString, execOptions, errorMessage) { 1036 | return command(commandString, execOptions) 1037 | .then(assert.fail) 1038 | .catch(function (error) { 1039 | expect(error.error.code).to.eq(1); 1040 | expect(error.stderr).to.have.string(errorMessage); 1041 | }); 1042 | } 1043 | 1044 | function notFoundMsg (method, op) { 1045 | let reasons = ['Check used CMA access token / space ID combination.']; 1046 | 1047 | if (method !== 'post') { 1048 | reasons.push('Check the extension ID.'); 1049 | } 1050 | 1051 | return httpError(method, op, 'NotFound', reasons.join('\n')); 1052 | } 1053 | 1054 | function serverErrorMsg (method, op, errorCode) { 1055 | let details = 'Server failed to fulfill the request.'; 1056 | 1057 | return httpError(method, op, errorCode, details); 1058 | } 1059 | 1060 | function httpError (method, op, errorCode, details) { 1061 | let link = 'See https://www.contentful.com/developers/docs/references/errors for more information.\n'; 1062 | 1063 | return `${method.toUpperCase()} (${op}) request failed because of ${errorCode} error.\n${details}\n${link}`; 1064 | } 1065 | --------------------------------------------------------------------------------