From 1a9a10c82bfc01af4bf30d2ca42dd18d51d92684 Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Thu, 14 Dec 2017 22:07:53 +0100 Subject: [PATCH] Moved zip folder, read csv and package-json to lib/fs refs #9178, refs https://github.com/TryGhost/Ghost/commit/849e97640ff507ebbe3464ccf7ae0049593cdb6f - i've reconsidered, these modules belong to lib - prettify package-json module --- core/server/api/schedules.js | 2 +- core/server/api/subscribers.js | 4 +- core/server/lib/fs/index.js | 11 + .../fs/package-json/filter.js} | 0 .../packages => lib/fs/package-json}/index.js | 16 +- .../fs/package-json/parse.js} | 2 +- .../fs/package-json/read.js} | 4 +- core/server/{utils => lib/fs}/read-csv.js | 10 +- .../themes/utils.js => lib/fs/zip-folder.js} | 2 +- core/server/services/apps/permissions.js | 4 +- core/server/services/themes/Storage.js | 4 +- core/server/services/themes/loader.js | 6 +- core/server/services/themes/to-json.js | 6 +- core/server/utils/index.js | 2 - .../integration/api/api_subscribers_spec.js | 11 +- .../unit/lib/fs/package-json/filter_spec.js | 122 +++++ .../unit/lib/fs/package-json/parse_spec.js | 120 +++++ .../unit/lib/fs/package-json/read_spec.js | 259 +++++++++ .../unit/{utils => lib/fs}/read-csv_spec.js | 12 +- .../fs/zip-folder_spec.js} | 6 +- core/test/unit/utils/packages_spec.js | 499 ------------------ 21 files changed, 559 insertions(+), 543 deletions(-) create mode 100644 core/server/lib/fs/index.js rename core/server/{utils/packages/filter-packages.js => lib/fs/package-json/filter.js} (100%) rename core/server/{utils/packages => lib/fs/package-json}/index.js (73%) rename core/server/{utils/packages/parse-package-json.js => lib/fs/package-json/parse.js} (97%) rename core/server/{utils/packages/read-packages.js => lib/fs/package-json/read.js} (96%) rename core/server/{utils => lib/fs}/read-csv.js (95%) rename core/server/{services/themes/utils.js => lib/fs/zip-folder.js} (89%) delete mode 100644 core/server/utils/index.js create mode 100644 core/test/unit/lib/fs/package-json/filter_spec.js create mode 100644 core/test/unit/lib/fs/package-json/parse_spec.js create mode 100644 core/test/unit/lib/fs/package-json/read_spec.js rename core/test/unit/{utils => lib/fs}/read-csv_spec.js (87%) rename core/test/unit/{services/themes/utils_spec.js => lib/fs/zip-folder_spec.js} (88%) delete mode 100644 core/test/unit/utils/packages_spec.js diff --git a/core/server/api/schedules.js b/core/server/api/schedules.js index 6b86dc27b6..391d61e5da 100644 --- a/core/server/api/schedules.js +++ b/core/server/api/schedules.js @@ -42,7 +42,7 @@ exports.publishPost = function publishPost(object, options) { cleanOptions.transacting = transacting; cleanOptions.forUpdate = true; - // CASE: extend allowed options, see api/utils.js + // CASE: extend allowed options, see api/zip-folder.js cleanOptions.opts = ['forUpdate', 'transacting']; return postsAPI.read(cleanOptions) diff --git a/core/server/api/subscribers.js b/core/server/api/subscribers.js index 500cbae383..db89ca3b3a 100644 --- a/core/server/api/subscribers.js +++ b/core/server/api/subscribers.js @@ -4,7 +4,7 @@ var Promise = require('bluebird'), _ = require('lodash'), fs = require('fs-extra'), pipeline = require('../lib/promise/pipeline'), - globalUtils = require('../utils'), + fsLib = require('../lib/fs'), localUtils = require('./utils'), models = require('../models'), common = require('../lib/common'), @@ -304,7 +304,7 @@ subscribers = { invalid = 0, duplicates = 0; - return globalUtils.readCSV({ + return fsLib.readCSV({ path: filePath, columnsToExtract: [{name: 'email', lookup: /email/i}] }).then(function (result) { diff --git a/core/server/lib/fs/index.js b/core/server/lib/fs/index.js new file mode 100644 index 0000000000..4856ea2d28 --- /dev/null +++ b/core/server/lib/fs/index.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = { + get readCSV() { + return require('./read-csv'); + }, + + get zipFolder() { + return require('./zip-folder'); + } +}; diff --git a/core/server/utils/packages/filter-packages.js b/core/server/lib/fs/package-json/filter.js similarity index 100% rename from core/server/utils/packages/filter-packages.js rename to core/server/lib/fs/package-json/filter.js diff --git a/core/server/utils/packages/index.js b/core/server/lib/fs/package-json/index.js similarity index 73% rename from core/server/utils/packages/index.js rename to core/server/lib/fs/package-json/index.js index e29194d9d0..354949ce23 100644 --- a/core/server/utils/packages/index.js +++ b/core/server/lib/fs/package-json/index.js @@ -10,8 +10,18 @@ * These utils facilitate loading, reading, managing etc, packages from the file system. */ +'use strict'; + module.exports = { - read: require('./read-packages'), - parsePackageJSON: require('./parse-package-json'), - filterPackages: require('./filter-packages') + get read() { + return require('./read'); + }, + + get parse() { + return require('./parse'); + }, + + get filter() { + return require('./filter'); + } }; diff --git a/core/server/utils/packages/parse-package-json.js b/core/server/lib/fs/package-json/parse.js similarity index 97% rename from core/server/utils/packages/parse-package-json.js rename to core/server/lib/fs/package-json/parse.js index a4f0d5ffce..eb76276f21 100644 --- a/core/server/utils/packages/parse-package-json.js +++ b/core/server/lib/fs/package-json/parse.js @@ -4,7 +4,7 @@ var Promise = require('bluebird'), fs = require('fs-extra'), - common = require('../../lib/common'); + common = require('../../common'); /** * Parse package.json and validate it has diff --git a/core/server/utils/packages/read-packages.js b/core/server/lib/fs/package-json/read.js similarity index 96% rename from core/server/utils/packages/read-packages.js rename to core/server/lib/fs/package-json/read.js index a189cfa6fe..6a10fab547 100644 --- a/core/server/utils/packages/read-packages.js +++ b/core/server/lib/fs/package-json/read.js @@ -5,8 +5,8 @@ var Promise = require('bluebird'), _ = require('lodash'), join = require('path').join, fs = require('fs-extra'), - parsePackageJson = require('./parse-package-json'), - common = require('../../lib/common'), + parsePackageJson = require('./parse'), + common = require('../../common'), notAPackageRegex = /^\.|_messages|README.md|node_modules|bower_components/i, packageJSONPath = 'package.json', diff --git a/core/server/utils/read-csv.js b/core/server/lib/fs/read-csv.js similarity index 95% rename from core/server/utils/read-csv.js rename to core/server/lib/fs/read-csv.js index 1ef4998bf4..ea1fe6e70e 100644 --- a/core/server/utils/read-csv.js +++ b/core/server/lib/fs/read-csv.js @@ -1,9 +1,11 @@ -var Promise = require('bluebird'), +'use strict'; + +const Promise = require('bluebird'), csvParser = require('csv-parser'), _ = require('lodash'), fs = require('fs-extra'); -function readCSV(options) { +module.exports = function readCSV(options) { var columnsToExtract = options.columnsToExtract || [], results = [], rows = []; @@ -54,6 +56,4 @@ function readCSV(options) { resolve(results); }); }); -} - -module.exports = readCSV; +}; diff --git a/core/server/services/themes/utils.js b/core/server/lib/fs/zip-folder.js similarity index 89% rename from core/server/services/themes/utils.js rename to core/server/lib/fs/zip-folder.js index 356ba2df98..7185a9e7bd 100644 --- a/core/server/services/themes/utils.js +++ b/core/server/lib/fs/zip-folder.js @@ -2,7 +2,7 @@ const fs = require('fs-extra'); -exports.zipFolder = function zipFolder(folderToZip, destination, callback) { +module.exports = function zipFolder(folderToZip, destination, callback) { var archiver = require('archiver'), output = fs.createWriteStream(destination), archive = archiver.create('zip', {}); diff --git a/core/server/services/apps/permissions.js b/core/server/services/apps/permissions.js index 7f710fac96..83221ed23a 100644 --- a/core/server/services/apps/permissions.js +++ b/core/server/services/apps/permissions.js @@ -1,7 +1,7 @@ var fs = require('fs-extra'), Promise = require('bluebird'), path = require('path'), - parsePackageJson = require('../../utils/packages').parsePackageJSON; + packageJSON = require('../../lib/fs/package-json'); function AppPermissions(appPath) { this.appPath = appPath; @@ -45,7 +45,7 @@ AppPermissions.prototype.checkPackageContentsExists = function () { // Get the contents of the package.json in the appPath root AppPermissions.prototype.getPackageContents = function () { - return parsePackageJson(this.packagePath); + return packageJSON.parse(this.packagePath); }; // Default permissions for an App. diff --git a/core/server/services/themes/Storage.js b/core/server/services/themes/Storage.js index b63bd02817..7dcb900f3c 100644 --- a/core/server/services/themes/Storage.js +++ b/core/server/services/themes/Storage.js @@ -6,7 +6,7 @@ var fs = require('fs-extra'), Promise = require('bluebird'), config = require('../../config'), security = require('../../lib/security'), - localUtils = require('./utils'), + fsLib = require('../../lib/fs'), LocalFileStorage = require('../../adapters/storage/LocalFileStorage'); /** @@ -37,7 +37,7 @@ class ThemeStorage extends LocalFileStorage { fs.ensureDir(zipBasePath) .then(function () { - return Promise.promisify(localUtils.zipFolder)(themePath, zipPath); + return Promise.promisify(fsLib.zipFolder)(themePath, zipPath); }) .then(function (length) { res.set({ diff --git a/core/server/services/themes/loader.js b/core/server/services/themes/loader.js index b50bfdc14c..b10e22eeb3 100644 --- a/core/server/services/themes/loader.js +++ b/core/server/services/themes/loader.js @@ -1,12 +1,12 @@ var debug = require('ghost-ignition').debug('themes:loader'), config = require('../../config'), + packageJSON = require('../../lib/fs/package-json'), themeList = require('./list'), - read = require('../../utils/packages').read, loadAllThemes, loadOneTheme; loadAllThemes = function loadAllThemes() { - return read + return packageJSON.read .all(config.getContentPath('themes')) .then(function updateThemeList(themes) { debug('loading themes', Object.keys(themes)); @@ -16,7 +16,7 @@ loadAllThemes = function loadAllThemes() { }; loadOneTheme = function loadOneTheme(themeName) { - return read + return packageJSON.read .one(config.getContentPath('themes'), themeName) .then(function (readThemes) { debug('loaded one theme', themeName); diff --git a/core/server/services/themes/to-json.js b/core/server/services/themes/to-json.js index 1c6d890226..a45201a901 100644 --- a/core/server/services/themes/to-json.js +++ b/core/server/services/themes/to-json.js @@ -1,7 +1,7 @@ var _ = require('lodash'), themeList = require('./list'), active = require('./active'), - packages = require('../../utils/packages'), + packageJSON = require('../../lib/fs/package-json'), settingsCache = require('../settings/cache'); /** @@ -21,13 +21,13 @@ module.exports = function toJSON(name, checkedTheme) { if (!name) { toFilter = themeList.getAll(); - themeResult = packages.filterPackages(toFilter, settingsCache.get('active_theme')); + themeResult = packageJSON.filter(toFilter, settingsCache.get('active_theme')); } else { toFilter = { [name]: themeList.get(name) }; - themeResult = packages.filterPackages(toFilter, settingsCache.get('active_theme')); + themeResult = packageJSON.filter(toFilter, settingsCache.get('active_theme')); if (checkedTheme && checkedTheme.results.warning.length > 0) { themeResult[0].warnings = _.cloneDeep(checkedTheme.results.warning); diff --git a/core/server/utils/index.js b/core/server/utils/index.js deleted file mode 100644 index 6272f67e2c..0000000000 --- a/core/server/utils/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// here for mocking, will be moved soon -module.exports.readCSV = require('./read-csv'); diff --git a/core/test/integration/api/api_subscribers_spec.js b/core/test/integration/api/api_subscribers_spec.js index 65123c879d..5f50b89fae 100644 --- a/core/test/integration/api/api_subscribers_spec.js +++ b/core/test/integration/api/api_subscribers_spec.js @@ -7,10 +7,8 @@ var should = require('should'), _ = require('lodash'), context = testUtils.context, common = require('../../../server/lib/common'), - globalUtils = require('../../../server/utils'), - apiUtils = require('../../../server/api/utils'), + fsLib = require('../../../server/lib/fs'), SubscribersAPI = require('../../../server/api/subscribers'), - sandbox = sinon.sandbox.create(); describe('Subscribers API', function () { @@ -275,14 +273,11 @@ describe('Subscribers API', function () { }); describe('Read CSV', function () { - var scope = {}, - stub; + var scope = {}; beforeEach(function () { sandbox.stub(fs, 'unlink').resolves(); - sandbox.stub(apiUtils, 'checkFileExists').returns(true); - stub = sandbox.stub(apiUtils, 'checkFileIsValid').returns(true); - sandbox.stub(globalUtils, 'readCSV').callsFake(function () { + sandbox.stub(fsLib, 'readCSV').value(function () { if (scope.csvError) { return Promise.reject(new Error('csv')); } diff --git a/core/test/unit/lib/fs/package-json/filter_spec.js b/core/test/unit/lib/fs/package-json/filter_spec.js new file mode 100644 index 0000000000..20ef8769ba --- /dev/null +++ b/core/test/unit/lib/fs/package-json/filter_spec.js @@ -0,0 +1,122 @@ +var should = require('should'), // jshint ignore:line + packageJSON = require('../../../../../server/lib/fs/package-json'); + +describe('lib/fs/package-json', function () { + // @TODO: introduce some non-theme package examples + var casper = { + name: 'casper', + path: '~/content/themes/casper', + 'package.json': { + name: 'casper', + description: 'The default personal blogging theme for Ghost. Beautiful, minimal and responsive.', + demo: 'https://demo.ghost.io', + version: '1.3.5', + engines: {}, + license: 'MIT', + screenshots: {}, + author: {}, + gpm: {}, + keywords: {}, + repository: {}, + bugs: 'https://github.com/TryGhost/Casper/issues', + contributors: 'https://github.com/TryGhost/Casper/graphs/contributors' + } + }, + simplePackage = { + name: 'simple', + path: '~/content/themes/simple', + 'package.json': { + name: 'simple', + version: '0.1.0' + } + }, + missingPackageJson = { + name: 'missing', + path: '~/content/themes/missing', + 'package.json': null + }; + + it('should filter packages correctly', function () { + var result = packageJSON.filter({casper: casper}), + package1; + + result.should.be.an.Array().with.lengthOf(1); + package1 = result[0]; + + package1.should.be.an.Object().with.properties('name', 'package', 'active'); + Object.keys(package1).should.be.an.Array().with.lengthOf(3); + package1.name.should.eql('casper'); + package1.package.should.be.an.Object().with.properties('name', 'version'); + package1.active.should.be.false(); + }); + + it('should filter packages and handle a single active package string', function () { + var result = packageJSON.filter({casper: casper, simple: simplePackage}, 'casper'), + package1, package2; + + result.should.be.an.Array().with.lengthOf(2); + package1 = result[0]; + package2 = result[1]; + + package1.should.be.an.Object().with.properties('name', 'package', 'active'); + Object.keys(package1).should.be.an.Array().with.lengthOf(3); + package1.name.should.eql('casper'); + package1.package.should.be.an.Object().with.properties('name', 'version'); + package1.active.should.be.true(); + + package2.should.be.an.Object().with.properties('name', 'package', 'active'); + Object.keys(package2).should.be.an.Array().with.lengthOf(3); + package2.name.should.eql('simple'); + package2.package.should.be.an.Object().with.properties('name', 'version'); + package2.active.should.be.false(); + }); + + it('should filter packages and handle an array of active packages', function () { + var result = packageJSON.filter({casper: casper, simple: simplePackage}, ['casper', 'simple']), + package1, package2; + + result.should.be.an.Array().with.lengthOf(2); + package1 = result[0]; + package2 = result[1]; + + package1.should.be.an.Object().with.properties('name', 'package', 'active'); + Object.keys(package1).should.be.an.Array().with.lengthOf(3); + package1.name.should.eql('casper'); + package1.package.should.be.an.Object().with.properties('name', 'version'); + package1.active.should.be.true(); + + package2.should.be.an.Object().with.properties('name', 'package', 'active'); + Object.keys(package2).should.be.an.Array().with.lengthOf(3); + package2.name.should.eql('simple'); + package2.package.should.be.an.Object().with.properties('name', 'version'); + package2.active.should.be.true(); + }); + + it('handles packages with no package.json even though this makes us sad', function () { + var result = packageJSON.filter({casper: casper, missing: missingPackageJson}, ['casper']), + package1, package2; + + result.should.be.an.Array().with.lengthOf(2); + package1 = result[0]; + package2 = result[1]; + + package1.should.be.an.Object().with.properties('name', 'package', 'active'); + Object.keys(package1).should.be.an.Array().with.lengthOf(3); + package1.name.should.eql('casper'); + package1.package.should.be.an.Object().with.properties('name', 'version'); + package1.active.should.be.true(); + + package2.should.be.an.Object().with.properties('name', 'package', 'active'); + Object.keys(package2).should.be.an.Array().with.lengthOf(3); + package2.name.should.eql('missing'); + package2.package.should.be.false(); + package2.active.should.be.false(); + }); + + it('filters out things which are not packages', function () { + var result = packageJSON.filter({ + '.git': {}, '.anything': {}, 'README.md': {}, _messages: {} + }); + result.should.be.an.Array().with.lengthOf(0); + }); +}); diff --git a/core/test/unit/lib/fs/package-json/parse_spec.js b/core/test/unit/lib/fs/package-json/parse_spec.js new file mode 100644 index 0000000000..b119987065 --- /dev/null +++ b/core/test/unit/lib/fs/package-json/parse_spec.js @@ -0,0 +1,120 @@ +var should = require('should'), // jshint ignore:line + tmp = require('tmp'), + fs = require('fs-extra'), + packageJSON = require('../../../../../server/lib/fs/package-json'); + +describe('lib/fs/package-json: parse', function () { + it('should parse valid package.json', function (done) { + var pkgJson, tmpFile; + + tmpFile = tmp.fileSync(); + pkgJson = JSON.stringify({ + name: 'test', + version: '0.0.0' + }); + + fs.writeSync(tmpFile.fd, pkgJson); + + packageJSON.parse(tmpFile.name) + .then(function (pkg) { + pkg.should.eql({ + name: 'test', + version: '0.0.0' + }); + + done(); + }) + .catch(done) + .finally(tmpFile.removeCallback); + }); + + it('should fail when name is missing', function (done) { + var pkgJson, tmpFile; + + tmpFile = tmp.fileSync(); + pkgJson = JSON.stringify({ + version: '0.0.0' + }); + + fs.writeSync(tmpFile.fd, pkgJson); + + packageJSON.parse(tmpFile.name) + .then(function () { + done(new Error('packageJSON.parse succeeded, but should\'ve failed')); + }) + .catch(function (err) { + err.message.should.equal('"name" or "version" is missing from theme package.json file.'); + err.context.should.equal(tmpFile.name); + err.help.should.equal('This will be required in future. Please see http://docs.ghost.org/themes/'); + + done(); + }) + .catch(done) + .finally(tmpFile.removeCallback); + }); + + it('should fail when version is missing', function (done) { + var pkgJson, tmpFile; + + tmpFile = tmp.fileSync(); + pkgJson = JSON.stringify({ + name: 'test' + }); + + fs.writeSync(tmpFile.fd, pkgJson); + + packageJSON.parse(tmpFile.name) + .then(function () { + done(new Error('packageJSON.parse succeeded, but should\'ve failed')); + }) + .catch(function (err) { + err.message.should.equal('"name" or "version" is missing from theme package.json file.'); + err.context.should.equal(tmpFile.name); + err.help.should.equal('This will be required in future. Please see http://docs.ghost.org/themes/'); + + done(); + }) + .catch(done) + .finally(tmpFile.removeCallback); + }); + + it('should fail when JSON is invalid', function (done) { + var pkgJson, tmpFile; + + tmpFile = tmp.fileSync(); + pkgJson = '{name:"test"}'; + + fs.writeSync(tmpFile.fd, pkgJson); + + packageJSON.parse(tmpFile.name) + .then(function () { + done(new Error('packageJSON.parse succeeded, but should\'ve failed')); + }) + .catch(function (err) { + err.message.should.equal('Theme package.json file is malformed'); + err.context.should.equal(tmpFile.name); + err.help.should.equal('This will be required in future. Please see http://docs.ghost.org/themes/'); + + done(); + }) + .catch(done) + .finally(tmpFile.removeCallback); + }); + + it('should fail when file is missing', function (done) { + var tmpFile = tmp.fileSync(); + + tmpFile.removeCallback(); + packageJSON.parse(tmpFile.name) + .then(function () { + done(new Error('packageJSON.parse succeeded, but should\'ve failed')); + }) + .catch(function (err) { + err.message.should.equal('Could not read package.json file'); + err.context.should.equal(tmpFile.name); + + done(); + }) + .catch(done); + }); +}); diff --git a/core/test/unit/lib/fs/package-json/read_spec.js b/core/test/unit/lib/fs/package-json/read_spec.js new file mode 100644 index 0000000000..64691cd0e5 --- /dev/null +++ b/core/test/unit/lib/fs/package-json/read_spec.js @@ -0,0 +1,259 @@ +var should = require('should'), // jshint ignore:line + tmp = require('tmp'), + join = require('path').join, + fs = require('fs-extra'), + packageJSON = require('../../../../../server/lib/fs/package-json'); + +describe('lib/fs/package-json: read', function () { + describe('all', function () { + it('should read directory and ignore unneeded items', function (done) { + var packagePath = tmp.dirSync({unsafeCleanup: true}); + + // create example theme + fs.mkdirSync(join(packagePath.name, 'casper')); + fs.writeFileSync(join(packagePath.name, 'casper', 'index.hbs')); + + // create some trash + fs.mkdirSync(join(packagePath.name, 'node_modules')); + fs.mkdirSync(join(packagePath.name, 'bower_components')); + fs.mkdirSync(join(packagePath.name, '.git')); + fs.writeFileSync(join(packagePath.name, '.DS_Store')); + + packageJSON.read.all(packagePath.name) + .then(function (pkgs) { + pkgs.should.eql({ + casper: { + name: 'casper', + path: join(packagePath.name, 'casper'), + 'package.json': null + } + }); + + done(); + }) + .catch(done) + .finally(packagePath.removeCallback); + }); + + it('should read directory and parse package.json files', function (done) { + var packagePath, pkgJson; + + packagePath = tmp.dirSync({unsafeCleanup: true}); + pkgJson = JSON.stringify({ + name: 'test', + version: '0.0.0' + }); + + // create example theme + fs.mkdirSync(join(packagePath.name, 'testtheme')); + fs.writeFileSync(join(packagePath.name, 'testtheme', 'package.json'), pkgJson); + fs.writeFileSync(join(packagePath.name, 'testtheme', 'index.hbs')); + + packageJSON.read.all(packagePath.name) + .then(function (pkgs) { + pkgs.should.eql({ + testtheme: { + name: 'testtheme', + path: join(packagePath.name, 'testtheme'), + 'package.json': { + name: 'test', + version: '0.0.0' + } + } + }); + + done(); + }) + .catch(done) + .finally(packagePath.removeCallback); + }); + + it('should read directory and ignore invalid package.json files', function (done) { + var packagePath, pkgJson; + + packagePath = tmp.dirSync({unsafeCleanup: true}); + pkgJson = JSON.stringify({ + name: 'test' + }); + + // create example theme + fs.mkdirSync(join(packagePath.name, 'testtheme')); + fs.writeFileSync(join(packagePath.name, 'testtheme', 'package.json'), pkgJson); + fs.writeFileSync(join(packagePath.name, 'testtheme', 'index.hbs')); + + packageJSON.read.all(packagePath.name) + .then(function (pkgs) { + pkgs.should.eql({ + testtheme: { + name: 'testtheme', + path: join(packagePath.name, 'testtheme'), + 'package.json': null + } + }); + + done(); + }) + .catch(done) + .finally(packagePath.removeCallback); + }); + }); + + describe('one', function () { + it('should read directory and ignore unneeded items', function (done) { + var packagePath = tmp.dirSync({unsafeCleanup: true}); + + // create example theme + fs.mkdirSync(join(packagePath.name, 'casper')); + fs.writeFileSync(join(packagePath.name, 'casper', 'index.hbs')); + + // create some trash + fs.mkdirSync(join(packagePath.name, 'node_modules')); + fs.mkdirSync(join(packagePath.name, 'bower_components')); + fs.mkdirSync(join(packagePath.name, '.git')); + fs.writeFileSync(join(packagePath.name, '.DS_Store')); + + packageJSON.read.one(packagePath.name, 'casper') + .then(function (pkgs) { + pkgs.should.eql({ + casper: { + name: 'casper', + path: join(packagePath.name, 'casper'), + 'package.json': null + } + }); + + done(); + }) + .catch(done) + .finally(packagePath.removeCallback); + }); + + it('should read directory and parse package.json files', function (done) { + var packagePath, pkgJson; + + packagePath = tmp.dirSync({unsafeCleanup: true}); + pkgJson = JSON.stringify({ + name: 'test', + version: '0.0.0' + }); + + // create example theme + fs.mkdirSync(join(packagePath.name, 'testtheme')); + fs.writeFileSync(join(packagePath.name, 'testtheme', 'package.json'), pkgJson); + fs.writeFileSync(join(packagePath.name, 'testtheme', 'index.hbs')); + + packageJSON.read.one(packagePath.name, 'testtheme') + .then(function (pkgs) { + pkgs.should.eql({ + testtheme: { + name: 'testtheme', + path: join(packagePath.name, 'testtheme'), + 'package.json': { + name: 'test', + version: '0.0.0' + } + } + }); + + done(); + }) + .catch(done) + .finally(packagePath.removeCallback); + }); + + it('should read directory and ignore invalid package.json files', function (done) { + var packagePath, pkgJson; + + packagePath = tmp.dirSync({unsafeCleanup: true}); + pkgJson = JSON.stringify({ + name: 'test' + }); + + // create example theme + fs.mkdirSync(join(packagePath.name, 'testtheme')); + fs.writeFileSync(join(packagePath.name, 'testtheme', 'package.json'), pkgJson); + fs.writeFileSync(join(packagePath.name, 'testtheme', 'index.hbs')); + + packageJSON.read.one(packagePath.name, 'testtheme') + .then(function (pkgs) { + pkgs.should.eql({ + testtheme: { + name: 'testtheme', + path: join(packagePath.name, 'testtheme'), + 'package.json': null + } + }); + + done(); + }) + .catch(done) + .finally(packagePath.removeCallback); + }); + + it('should read directory and include only single requested package', function (done) { + var packagePath = tmp.dirSync({unsafeCleanup: true}); + + // create trash + fs.writeFileSync(join(packagePath.name, 'casper.zip')); + fs.writeFileSync(join(packagePath.name, '.DS_Store')); + + // create actual theme + fs.mkdirSync(join(packagePath.name, 'casper')); + fs.mkdirSync(join(packagePath.name, 'casper', 'partials')); + fs.writeFileSync(join(packagePath.name, 'casper', 'index.hbs')); + fs.writeFileSync(join(packagePath.name, 'casper', 'partials', 'navigation.hbs')); + fs.mkdirSync(join(packagePath.name, 'not-casper')); + fs.writeFileSync(join(packagePath.name, 'not-casper', 'index.hbs')); + + packageJSON.read.one(packagePath.name, 'casper') + .then(function (pkgs) { + pkgs.should.eql({ + casper: { + name: 'casper', + path: join(packagePath.name, 'casper'), + 'package.json': null + } + }); + + done(); + }) + .catch(done) + .finally(packagePath.removeCallback); + }); + + it('should return an error if package cannot be found', function (done) { + var packagePath = tmp.dirSync({unsafeCleanup: true}); + + // create trash + fs.writeFileSync(join(packagePath.name, 'casper.zip')); + fs.writeFileSync(join(packagePath.name, '.DS_Store')); + + packageJSON.read.one(packagePath.name, 'casper') + .then(function () { + done('Should have thrown an error'); + }) + .catch(function (err) { + err.message.should.eql('Package not found'); + done(); + }) + .finally(packagePath.removeCallback); + }); + + it('should return empty object if package is not a directory', function (done) { + var packagePath = tmp.dirSync({unsafeCleanup: true}); + + // create trash + fs.writeFileSync(join(packagePath.name, 'casper.zip')); + fs.writeFileSync(join(packagePath.name, '.DS_Store')); + + packageJSON.read.one(packagePath.name, 'casper.zip') + .then(function (pkg) { + pkg.should.eql({}); + + done(); + }) + .catch(done) + .finally(packagePath.removeCallback); + }); + }); +}); diff --git a/core/test/unit/utils/read-csv_spec.js b/core/test/unit/lib/fs/read-csv_spec.js similarity index 87% rename from core/test/unit/utils/read-csv_spec.js rename to core/test/unit/lib/fs/read-csv_spec.js index 3dc10046e8..9773145a99 100644 --- a/core/test/unit/utils/read-csv_spec.js +++ b/core/test/unit/lib/fs/read-csv_spec.js @@ -1,11 +1,11 @@ var should = require('should'), path = require('path'), - globalUtils = require('../../../server/utils'), - csvPath = path.join(__dirname, '../../utils/fixtures/csv/'); + fsLib = require('../../../../server/lib/fs'), + csvPath = path.join(__dirname, '../../../utils/fixtures/csv/'); -describe('read csv', function () { +describe('lib/fs: read csv', function () { it('read csv: one column', function (done) { - globalUtils.readCSV({ + fsLib.readCSV({ path: csvPath + 'single-column-with-header.csv', columnsToExtract: [{name: 'email', lookup: /email/i}] }).then(function (result) { @@ -19,7 +19,7 @@ describe('read csv', function () { }); it('read csv: two columns, 1 filter', function (done) { - globalUtils.readCSV({ + fsLib.readCSV({ path: csvPath + 'two-columns-with-header.csv', columnsToExtract: [{name: 'email', lookup: /email/i}] }).then(function (result) { @@ -34,7 +34,7 @@ describe('read csv', function () { }); it('read csv: two columns, 2 filters', function (done) { - globalUtils.readCSV({ + fsLib.readCSV({ path: csvPath + 'two-columns-obscure-header.csv', columnsToExtract: [ {name: 'email', lookup: /email/i}, diff --git a/core/test/unit/services/themes/utils_spec.js b/core/test/unit/lib/fs/zip-folder_spec.js similarity index 88% rename from core/test/unit/services/themes/utils_spec.js rename to core/test/unit/lib/fs/zip-folder_spec.js index 5474bb448a..f6a7e4fd5d 100644 --- a/core/test/unit/services/themes/utils_spec.js +++ b/core/test/unit/lib/fs/zip-folder_spec.js @@ -2,9 +2,9 @@ var should = require('should'), // jshint ignore:line path = require('path'), fs = require('fs-extra'), extract = require('extract-zip'), - themeUtils = require('../../../../server/services/themes/utils'); + fsLib = require('../../../../server/lib/fs'); -describe('services/themes: theme utils', function () { +describe('lib/fs: read csv', function () { const symlinkPath = path.join(__dirname, '..', '..', '..', 'utils', 'fixtures', 'themes', 'theme-symlink'), folderToSymlink = path.join(__dirname, '..', '..', '..', 'utils', 'fixtures', 'themes', 'casper'), zipDestination = path.join(__dirname, '..', '..', '..', 'utils', 'fixtures', 'themes', 'theme-symlink.zip'), @@ -25,7 +25,7 @@ describe('services/themes: theme utils', function () { it('ensure symlinks work', function (done) { fs.symlink(folderToSymlink, symlinkPath); - themeUtils.zipFolder(symlinkPath, zipDestination, function (err) { + fsLib.zipFolder(symlinkPath, zipDestination, function (err) { if (err) { return done(err); } diff --git a/core/test/unit/utils/packages_spec.js b/core/test/unit/utils/packages_spec.js deleted file mode 100644 index 6b7bf3f7fb..0000000000 --- a/core/test/unit/utils/packages_spec.js +++ /dev/null @@ -1,499 +0,0 @@ -var should = require('should'), // jshint ignore:line - tmp = require('tmp'), - join = require('path').join, - fs = require('fs-extra'), - - // Things we are testing - packages = require('../../../server/utils/packages'), - parsePackageJson = packages.parsePackageJSON, - filterPackages = packages.filterPackages; - -describe('Package Utils', function () { - describe('Parse Package.json', function () { - it('should parse valid package.json', function (done) { - var pkgJson, tmpFile; - - tmpFile = tmp.fileSync(); - pkgJson = JSON.stringify({ - name: 'test', - version: '0.0.0' - }); - - fs.writeSync(tmpFile.fd, pkgJson); - - parsePackageJson(tmpFile.name) - .then(function (pkg) { - pkg.should.eql({ - name: 'test', - version: '0.0.0' - }); - - done(); - }) - .catch(done) - .finally(tmpFile.removeCallback); - }); - - it('should fail when name is missing', function (done) { - var pkgJson, tmpFile; - - tmpFile = tmp.fileSync(); - pkgJson = JSON.stringify({ - version: '0.0.0' - }); - - fs.writeSync(tmpFile.fd, pkgJson); - - parsePackageJson(tmpFile.name) - .then(function () { - done(new Error('parsePackageJson succeeded, but should\'ve failed')); - }) - .catch(function (err) { - err.message.should.equal('"name" or "version" is missing from theme package.json file.'); - err.context.should.equal(tmpFile.name); - err.help.should.equal('This will be required in future. Please see http://docs.ghost.org/themes/'); - - done(); - }) - .catch(done) - .finally(tmpFile.removeCallback); - }); - - it('should fail when version is missing', function (done) { - var pkgJson, tmpFile; - - tmpFile = tmp.fileSync(); - pkgJson = JSON.stringify({ - name: 'test' - }); - - fs.writeSync(tmpFile.fd, pkgJson); - - parsePackageJson(tmpFile.name) - .then(function () { - done(new Error('parsePackageJson succeeded, but should\'ve failed')); - }) - .catch(function (err) { - err.message.should.equal('"name" or "version" is missing from theme package.json file.'); - err.context.should.equal(tmpFile.name); - err.help.should.equal('This will be required in future. Please see http://docs.ghost.org/themes/'); - - done(); - }) - .catch(done) - .finally(tmpFile.removeCallback); - }); - - it('should fail when JSON is invalid', function (done) { - var pkgJson, tmpFile; - - tmpFile = tmp.fileSync(); - pkgJson = '{name:"test"}'; - - fs.writeSync(tmpFile.fd, pkgJson); - - parsePackageJson(tmpFile.name) - .then(function () { - done(new Error('parsePackageJson succeeded, but should\'ve failed')); - }) - .catch(function (err) { - err.message.should.equal('Theme package.json file is malformed'); - err.context.should.equal(tmpFile.name); - err.help.should.equal('This will be required in future. Please see http://docs.ghost.org/themes/'); - - done(); - }) - .catch(done) - .finally(tmpFile.removeCallback); - }); - - it('should fail when file is missing', function (done) { - var tmpFile = tmp.fileSync(); - - tmpFile.removeCallback(); - parsePackageJson(tmpFile.name) - .then(function () { - done(new Error('parsePackageJson succeeded, but should\'ve failed')); - }) - .catch(function (err) { - err.message.should.equal('Could not read package.json file'); - err.context.should.equal(tmpFile.name); - - done(); - }) - .catch(done); - }); - }); - - describe('Read Packages', function () { - it('should read directory and ignore unneeded items', function (done) { - var packagePath = tmp.dirSync({unsafeCleanup: true}); - - // create example theme - fs.mkdirSync(join(packagePath.name, 'casper')); - fs.writeFileSync(join(packagePath.name, 'casper', 'index.hbs')); - - // create some trash - fs.mkdirSync(join(packagePath.name, 'node_modules')); - fs.mkdirSync(join(packagePath.name, 'bower_components')); - fs.mkdirSync(join(packagePath.name, '.git')); - fs.writeFileSync(join(packagePath.name, '.DS_Store')); - - packages.read.all(packagePath.name) - .then(function (pkgs) { - pkgs.should.eql({ - casper: { - name: 'casper', - path: join(packagePath.name, 'casper'), - 'package.json': null - } - }); - - done(); - }) - .catch(done) - .finally(packagePath.removeCallback); - }); - - it('should read directory and parse package.json files', function (done) { - var packagePath, pkgJson; - - packagePath = tmp.dirSync({unsafeCleanup: true}); - pkgJson = JSON.stringify({ - name: 'test', - version: '0.0.0' - }); - - // create example theme - fs.mkdirSync(join(packagePath.name, 'testtheme')); - fs.writeFileSync(join(packagePath.name, 'testtheme', 'package.json'), pkgJson); - fs.writeFileSync(join(packagePath.name, 'testtheme', 'index.hbs')); - - packages.read.all(packagePath.name) - .then(function (pkgs) { - pkgs.should.eql({ - testtheme: { - name: 'testtheme', - path: join(packagePath.name, 'testtheme'), - 'package.json': { - name: 'test', - version: '0.0.0' - } - } - }); - - done(); - }) - .catch(done) - .finally(packagePath.removeCallback); - }); - - it('should read directory and ignore invalid package.json files', function (done) { - var packagePath, pkgJson; - - packagePath = tmp.dirSync({unsafeCleanup: true}); - pkgJson = JSON.stringify({ - name: 'test' - }); - - // create example theme - fs.mkdirSync(join(packagePath.name, 'testtheme')); - fs.writeFileSync(join(packagePath.name, 'testtheme', 'package.json'), pkgJson); - fs.writeFileSync(join(packagePath.name, 'testtheme', 'index.hbs')); - - packages.read.all(packagePath.name) - .then(function (pkgs) { - pkgs.should.eql({ - testtheme: { - name: 'testtheme', - path: join(packagePath.name, 'testtheme'), - 'package.json': null - } - }); - - done(); - }) - .catch(done) - .finally(packagePath.removeCallback); - }); - }); - - describe('Read Package', function () { - it('should read directory and ignore unneeded items', function (done) { - var packagePath = tmp.dirSync({unsafeCleanup: true}); - - // create example theme - fs.mkdirSync(join(packagePath.name, 'casper')); - fs.writeFileSync(join(packagePath.name, 'casper', 'index.hbs')); - - // create some trash - fs.mkdirSync(join(packagePath.name, 'node_modules')); - fs.mkdirSync(join(packagePath.name, 'bower_components')); - fs.mkdirSync(join(packagePath.name, '.git')); - fs.writeFileSync(join(packagePath.name, '.DS_Store')); - - packages.read.one(packagePath.name, 'casper') - .then(function (pkgs) { - pkgs.should.eql({ - casper: { - name: 'casper', - path: join(packagePath.name, 'casper'), - 'package.json': null - } - }); - - done(); - }) - .catch(done) - .finally(packagePath.removeCallback); - }); - - it('should read directory and parse package.json files', function (done) { - var packagePath, pkgJson; - - packagePath = tmp.dirSync({unsafeCleanup: true}); - pkgJson = JSON.stringify({ - name: 'test', - version: '0.0.0' - }); - - // create example theme - fs.mkdirSync(join(packagePath.name, 'testtheme')); - fs.writeFileSync(join(packagePath.name, 'testtheme', 'package.json'), pkgJson); - fs.writeFileSync(join(packagePath.name, 'testtheme', 'index.hbs')); - - packages.read.one(packagePath.name, 'testtheme') - .then(function (pkgs) { - pkgs.should.eql({ - testtheme: { - name: 'testtheme', - path: join(packagePath.name, 'testtheme'), - 'package.json': { - name: 'test', - version: '0.0.0' - } - } - }); - - done(); - }) - .catch(done) - .finally(packagePath.removeCallback); - }); - - it('should read directory and ignore invalid package.json files', function (done) { - var packagePath, pkgJson; - - packagePath = tmp.dirSync({unsafeCleanup: true}); - pkgJson = JSON.stringify({ - name: 'test' - }); - - // create example theme - fs.mkdirSync(join(packagePath.name, 'testtheme')); - fs.writeFileSync(join(packagePath.name, 'testtheme', 'package.json'), pkgJson); - fs.writeFileSync(join(packagePath.name, 'testtheme', 'index.hbs')); - - packages.read.one(packagePath.name, 'testtheme') - .then(function (pkgs) { - pkgs.should.eql({ - testtheme: { - name: 'testtheme', - path: join(packagePath.name, 'testtheme'), - 'package.json': null - } - }); - - done(); - }) - .catch(done) - .finally(packagePath.removeCallback); - }); - - it('should read directory and include only single requested package', function (done) { - var packagePath = tmp.dirSync({unsafeCleanup: true}); - - // create trash - fs.writeFileSync(join(packagePath.name, 'casper.zip')); - fs.writeFileSync(join(packagePath.name, '.DS_Store')); - - // create actual theme - fs.mkdirSync(join(packagePath.name, 'casper')); - fs.mkdirSync(join(packagePath.name, 'casper', 'partials')); - fs.writeFileSync(join(packagePath.name, 'casper', 'index.hbs')); - fs.writeFileSync(join(packagePath.name, 'casper', 'partials', 'navigation.hbs')); - fs.mkdirSync(join(packagePath.name, 'not-casper')); - fs.writeFileSync(join(packagePath.name, 'not-casper', 'index.hbs')); - - packages.read.one(packagePath.name, 'casper') - .then(function (pkgs) { - pkgs.should.eql({ - casper: { - name: 'casper', - path: join(packagePath.name, 'casper'), - 'package.json': null - } - }); - - done(); - }) - .catch(done) - .finally(packagePath.removeCallback); - }); - - it('should return an error if package cannot be found', function (done) { - var packagePath = tmp.dirSync({unsafeCleanup: true}); - - // create trash - fs.writeFileSync(join(packagePath.name, 'casper.zip')); - fs.writeFileSync(join(packagePath.name, '.DS_Store')); - - packages.read.one(packagePath.name, 'casper') - .then(function () { - done('Should have thrown an error'); - }) - .catch(function (err) { - err.message.should.eql('Package not found'); - done(); - }) - .finally(packagePath.removeCallback); - }); - - it('should return empty object if package is not a directory', function (done) { - var packagePath = tmp.dirSync({unsafeCleanup: true}); - - // create trash - fs.writeFileSync(join(packagePath.name, 'casper.zip')); - fs.writeFileSync(join(packagePath.name, '.DS_Store')); - - packages.read.one(packagePath.name, 'casper.zip') - .then(function (pkg) { - pkg.should.eql({}); - - done(); - }) - .catch(done) - .finally(packagePath.removeCallback); - }); - }); - - describe('Filter Packages', function () { - // @TODO: introduce some non-theme package examples - var casper = { - name: 'casper', - path: '~/content/themes/casper', - 'package.json': { - name: 'casper', - description: 'The default personal blogging theme for Ghost. Beautiful, minimal and responsive.', - demo: 'https://demo.ghost.io', - version: '1.3.5', - engines: {}, - license: 'MIT', - screenshots: {}, - author: {}, - gpm: {}, - keywords: {}, - repository: {}, - bugs: 'https://github.com/TryGhost/Casper/issues', - contributors: 'https://github.com/TryGhost/Casper/graphs/contributors' - } - }, - simplePackage = { - name: 'simple', - path: '~/content/themes/simple', - 'package.json': { - name: 'simple', - version: '0.1.0' - } - }, - missingPackageJson = { - name: 'missing', - path: '~/content/themes/missing', - 'package.json': null - }; - - it('should filter packages correctly', function () { - var result = filterPackages({casper: casper}), - package1; - - result.should.be.an.Array().with.lengthOf(1); - package1 = result[0]; - - package1.should.be.an.Object().with.properties('name', 'package', 'active'); - Object.keys(package1).should.be.an.Array().with.lengthOf(3); - package1.name.should.eql('casper'); - package1.package.should.be.an.Object().with.properties('name', 'version'); - package1.active.should.be.false(); - }); - - it('should filter packages and handle a single active package string', function () { - var result = filterPackages({casper: casper, simple: simplePackage}, 'casper'), - package1, package2; - - result.should.be.an.Array().with.lengthOf(2); - package1 = result[0]; - package2 = result[1]; - - package1.should.be.an.Object().with.properties('name', 'package', 'active'); - Object.keys(package1).should.be.an.Array().with.lengthOf(3); - package1.name.should.eql('casper'); - package1.package.should.be.an.Object().with.properties('name', 'version'); - package1.active.should.be.true(); - - package2.should.be.an.Object().with.properties('name', 'package', 'active'); - Object.keys(package2).should.be.an.Array().with.lengthOf(3); - package2.name.should.eql('simple'); - package2.package.should.be.an.Object().with.properties('name', 'version'); - package2.active.should.be.false(); - }); - - it('should filter packages and handle an array of active packages', function () { - var result = filterPackages({casper: casper, simple: simplePackage}, ['casper', 'simple']), - package1, package2; - - result.should.be.an.Array().with.lengthOf(2); - package1 = result[0]; - package2 = result[1]; - - package1.should.be.an.Object().with.properties('name', 'package', 'active'); - Object.keys(package1).should.be.an.Array().with.lengthOf(3); - package1.name.should.eql('casper'); - package1.package.should.be.an.Object().with.properties('name', 'version'); - package1.active.should.be.true(); - - package2.should.be.an.Object().with.properties('name', 'package', 'active'); - Object.keys(package2).should.be.an.Array().with.lengthOf(3); - package2.name.should.eql('simple'); - package2.package.should.be.an.Object().with.properties('name', 'version'); - package2.active.should.be.true(); - }); - - it('handles packages with no package.json even though this makes us sad', function () { - var result = filterPackages({casper: casper, missing: missingPackageJson}, ['casper']), - package1, package2; - - result.should.be.an.Array().with.lengthOf(2); - package1 = result[0]; - package2 = result[1]; - - package1.should.be.an.Object().with.properties('name', 'package', 'active'); - Object.keys(package1).should.be.an.Array().with.lengthOf(3); - package1.name.should.eql('casper'); - package1.package.should.be.an.Object().with.properties('name', 'version'); - package1.active.should.be.true(); - - package2.should.be.an.Object().with.properties('name', 'package', 'active'); - Object.keys(package2).should.be.an.Array().with.lengthOf(3); - package2.name.should.eql('missing'); - package2.package.should.be.false(); - package2.active.should.be.false(); - }); - - it('filters out things which are not packages', function () { - var result = filterPackages({ - '.git': {}, '.anything': {}, 'README.md': {}, _messages: {} - }); - result.should.be.an.Array().with.lengthOf(0); - }); - }); -});