diff --git a/.travis.yml b/.travis.yml index ef251cb..29daca7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,13 @@ language: node_js node_js: + - "0.12" - "4.4" - "5.12" - "6.2" sudo: false + +script: + - npm run test + - cat coverage/lcov.info | node_modules/.bin/coveralls || echo "Coveralls upload failed" diff --git a/README.md b/README.md index f9cd681..9b2ffcc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,15 @@ +[![Travis Status][trav_img]][trav_site] +[![Coverage Status][cov_img]][cov_site] +[![NPM Package][npm_img]][npm_site] + # Publishr A tool for harmonious publishing of git and npm packages. + + +[trav_img]: https://img.shields.io/travis/FormidableLabs/publishr.svg +[trav_site]: https://travis-ci.org/FormidableLabs/publishr +[cov_img]: https://img.shields.io/coveralls/FormidableLabs/publishr.svg +[cov_site]: https://coveralls.io/r/FormidableLabs/publishr +[npm_img]: https://img.shields.io/npm/v/publishr.svg +[npm_site]: https://www.npmjs.org/package/publishr diff --git a/package.json b/package.json index dc15bff..0146bfa 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,12 @@ "scripts": { "build": "rimraf lib && babel src/ -d lib/", "check-coverage": "babel-istanbul check-coverage", - "cover": "NODE_PATH=./src babel-node node_modules/.bin/babel-istanbul cover node_modules/.bin/_mocha -- --require test/main.js --recursive test", + "cover": "rimraf coverage && NODE_PATH=src babel-node node_modules/.bin/babel-istanbul cover node_modules/.bin/_mocha -- --require test/main.js --recursive test", "lint": "eslint src test", "test": "npm run lint && npm run cover && npm run check-coverage", "postpublish": "node lib/cli.js postpublish", "postversion": "npm run build && node lib/cli.js postversion", - "unit": "NODE_PATH=./src babel-node node_modules/.bin/_mocha --require test/main.js --recursive test" + "unit": "NODE_PATH=src babel-node node_modules/.bin/_mocha --require test/main.js --recursive test" }, "keywords": [ "git", @@ -34,10 +34,12 @@ "babel-eslint": "^6.0.4", "babel-istanbul": "^0.8.0", "chai": "^3.5.0", + "coveralls": "^2.11.9", "eslint": "^1.0.0", "eslint-config-defaults": "^9.0.0", "eslint-plugin-filenames": "^0.2.0", "mocha": "^2.5.3", + "mock-fs": "^3.9.0", "proxyquire": "^1.7.9", "sinon": "^1.17.4", "sinon-chai": "^2.8.0" diff --git a/src/file-utils.js b/src/file-utils.js index 12e6cf0..0143c5d 100644 --- a/src/file-utils.js +++ b/src/file-utils.js @@ -34,12 +34,16 @@ const fileUtils = { readPackage() { return new Promise((resolve, reject) => { - fs.readFile("package.json", "utf8", (err, contents) => { - if (err) { - return reject(err); + fs.readFile("package.json", "utf8", (readErr, contents) => { + if (readErr) { + return reject(readErr); } - return resolve(JSON.parse(contents)); + try { + return resolve(JSON.parse(contents)); + } catch (parseErr) { + return reject(parseErr); + } }); }); }, @@ -92,7 +96,15 @@ const fileUtils = { writePackage(json) { return new Promise((resolve, reject) => { - fs.writeFile("package.json", JSON.stringify(json, null, 2), "utf8", (err) => { + let contents; + + try { + contents = JSON.stringify(json, null, 2); + } catch (err) { + return reject(err); + } + + fs.writeFile("package.json", contents, "utf8", (err) => { if (err) { return reject(err); } diff --git a/test/spec/file-handler.spec.js b/test/spec/file-handler.spec.js index 76056a6..84851d8 100644 --- a/test/spec/file-handler.spec.js +++ b/test/spec/file-handler.spec.js @@ -6,10 +6,20 @@ import sinon from "sinon"; describe("fileHandler", () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + describe("fixFiles", () => { it("should checkout files", () => { - sinon.stub(fileUtils, "checkoutFile"); - sinon.stub(fileUtils, "removeFile"); + sandbox.stub(fileUtils, "checkoutFile"); + sandbox.stub(fileUtils, "removeFile"); fileHandler.fixFiles({ _publishr: [{ @@ -18,16 +28,14 @@ describe("fileHandler", () => { }] }); expect(fileUtils.removeFile).to.have.callCount(0); - expect(fileUtils.checkoutFile).to.have.callCount(1); - expect(fileUtils.checkoutFile).to.have.been.calledWith("checkout.js"); - - fileUtils.checkoutFile.restore(); - fileUtils.removeFile.restore(); + expect(fileUtils.checkoutFile) + .to.have.callCount(1).and + .to.have.been.calledWith("checkout.js"); }); it("should remove files", () => { - sinon.stub(fileUtils, "checkoutFile"); - sinon.stub(fileUtils, "removeFile"); + sandbox.stub(fileUtils, "checkoutFile"); + sandbox.stub(fileUtils, "removeFile"); fileHandler.fixFiles({ _publishr: [{ @@ -36,16 +44,14 @@ describe("fileHandler", () => { }] }); expect(fileUtils.checkoutFile).to.have.callCount(0); - expect(fileUtils.removeFile).to.have.callCount(1); - expect(fileUtils.removeFile).to.have.been.calledWith("remove.js"); - - fileUtils.checkoutFile.restore(); - fileUtils.removeFile.restore(); + expect(fileUtils.removeFile) + .to.have.callCount(1).and + .to.have.been.calledWith("remove.js"); }); it("should handle multiple files", () => { - sinon.stub(fileUtils, "checkoutFile"); - sinon.stub(fileUtils, "removeFile"); + sandbox.stub(fileUtils, "checkoutFile"); + sandbox.stub(fileUtils, "removeFile"); fileHandler.fixFiles({ _publishr: [{ @@ -62,15 +68,14 @@ describe("fileHandler", () => { path: "remove2.js" }] }); - expect(fileUtils.checkoutFile).to.have.callCount(2); - expect(fileUtils.removeFile).to.have.callCount(2); - expect(fileUtils.checkoutFile).to.have.been.calledWith("checkout1.js"); - expect(fileUtils.checkoutFile).to.have.been.calledWith("checkout2.js"); - expect(fileUtils.removeFile).to.have.been.calledWith("remove1.js"); - expect(fileUtils.removeFile).to.have.been.calledWith("remove2.js"); - - fileUtils.checkoutFile.restore(); - fileUtils.removeFile.restore(); + expect(fileUtils.checkoutFile) + .to.have.callCount(2).and + .to.have.been.calledWith("checkout1.js").and + .to.have.been.calledWith("checkout2.js"); + expect(fileUtils.removeFile) + .to.have.callCount(2).and + .to.have.been.calledWith("remove1.js").and + .to.have.been.calledWith("remove2.js"); }); }); @@ -78,10 +83,10 @@ describe("fileHandler", () => { it("should overwrite files", () => { const handler = (files) => Promise.resolve(files); - sinon.stub(fileUtils, "statFiles", handler); - sinon.stub(fileUtils, "readFiles", handler); - sinon.stub(fileUtils, "writeFiles", handler); - sinon.stub(fileHandler, "overwritePackage", handler); + sandbox.stub(fileUtils, "statFiles", handler); + sandbox.stub(fileUtils, "readFiles", handler); + sandbox.stub(fileUtils, "writeFiles", handler); + sandbox.stub(fileHandler, "overwritePackage", handler); return fileHandler.overwriteFiles({ publishr: { @@ -91,17 +96,18 @@ describe("fileHandler", () => { } } }).then((json) => { - expect(fileUtils.statFiles).to.have.callCount(1); + expect(fileUtils.statFiles) + .to.have.callCount(1).and + .to.have.been.calledWith([{ + newPath: "first.js", + oldPath: "first.js.publishr" + }, { + newPath: "second.js", + oldPath: "second.js.publishr" + }]); expect(fileUtils.readFiles).to.have.callCount(1); expect(fileUtils.writeFiles).to.have.callCount(1); expect(fileHandler.overwritePackage).to.have.callCount(1); - expect(fileUtils.statFiles).to.have.been.calledWith([{ - newPath: "first.js", - oldPath: "first.js.publishr" - }, { - newPath: "second.js", - oldPath: "second.js.publishr" - }]); expect(json).to.deep.equal({ publishr: { files: { @@ -110,22 +116,13 @@ describe("fileHandler", () => { } } }); - - fileUtils.statFiles.restore(); - fileUtils.readFiles.restore(); - fileUtils.writeFiles.restore(); - fileHandler.overwritePackage.restore(); }); }); it("should reject on an error", () => { const mockErr = new Error("Something bad happend!"); - sinon.stub(fileUtils, "statFiles", () => { - return new Promise((resolve, reject) => { - reject(mockErr); - }); - }); + sandbox.stub(fileUtils, "statFiles", () => Promise.reject(mockErr)); return fileHandler.overwriteFiles({ publishr: { @@ -135,8 +132,6 @@ describe("fileHandler", () => { } }).catch((err) => { expect(err).to.equal(mockErr); - - fileUtils.statFiles.restore(); }); }); }); @@ -156,28 +151,27 @@ describe("fileHandler", () => { path: ".npmignore" }]; - sinon.stub(fileUtils, "writePackage", () => Promise.resolve()); - sinon.stub(packageUtils, "updateDependencies"); - sinon.stub(packageUtils, "updateMeta"); + sandbox.stub(fileUtils, "writePackage", () => Promise.resolve()); + sandbox.stub(packageUtils, "updateDependencies"); + sandbox.stub(packageUtils, "updateMeta"); return fileHandler.overwritePackage(packageJSON, files).then(() => { - expect(packageUtils.updateDependencies).to.have.callCount(1); - expect(packageUtils.updateMeta).to.have.callCount(1); - expect(fileUtils.writePackage).to.have.callCount(1); - expect(packageUtils.updateDependencies).to.have.been.calledWith(packageJSON); - expect(packageUtils.updateMeta).to.have.been.calledWith(packageJSON, files); - expect(fileUtils.writePackage).to.have.been.calledWith({ - dependencies: { - babel: "1.0.0" - }, - devDependencies: { - eslint: "1.0.0" - } - }); - - fileUtils.writePackage.restore(); - packageUtils.updateDependencies.restore(); - packageUtils.updateMeta.restore(); + expect(packageUtils.updateDependencies) + .to.have.callCount(1).and + .to.have.been.calledWith(packageJSON); + expect(packageUtils.updateMeta) + .to.have.callCount(1).and + .to.have.been.calledWith(packageJSON, files); + expect(fileUtils.writePackage) + .to.have.callCount(1).and + .to.have.been.calledWith({ + dependencies: { + babel: "1.0.0" + }, + devDependencies: { + eslint: "1.0.0" + } + }); }); }); }); diff --git a/test/spec/file-utils.spec.js b/test/spec/file-utils.spec.js index e320187..132cd8a 100644 --- a/test/spec/file-utils.spec.js +++ b/test/spec/file-utils.spec.js @@ -1,30 +1,39 @@ /* eslint-disable max-params, max-nested-callbacks */ import childProcess from "child_process"; +import {Promise} from "es6-promise"; import fileUtils from "file-utils"; -import fs from "fs"; +import mockfs from "mock-fs"; import sinon from "sinon"; +import testHelpers from "../test-helpers"; describe("fileUtils", () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + mockfs.restore(); + sandbox.restore(); + }); + describe("checkoutFile", () => { it("should exec git checkout on file", () => { - sinon.stub(childProcess, "exec", (filePath, cb) => cb(null, "mock stdout")); + sandbox.stub(childProcess, "exec", (filePath, cb) => cb(null, "mock stdout")); return fileUtils.checkoutFile("checkout.js").then((stdout) => { expect(stdout).to.equal("mock stdout"); - - childProcess.exec.restore(); }); }); it("should reject on an error", () => { - sinon.stub(childProcess, "exec", (filePath, cb) => cb("mock error")); + sandbox.stub(childProcess, "exec", (filePath, cb) => cb("mock error")); return fileUtils.checkoutFile("checkout.js").catch((err) => { expect(err).to.equal("mock error"); - - childProcess.exec.restore(); }); }); }); @@ -37,48 +46,43 @@ describe("fileUtils", () => { oldPath: "file-2.js" }]; - sinon.stub(fs, "readFile", (filePath, opts, cb) => cb(null, "mock contents")); + mockfs({ + "file-1.js": "mock contents file 1", + "file-2.js": "mock contents file 2" + }); return fileUtils.readFiles(files).then((result) => { expect(result).to.deep.equal([{ - contents: "mock contents", + contents: "mock contents file 1", oldPath: "file-1.js" }, { - contents: "mock contents", + contents: "mock contents file 2", oldPath: "file-2.js" }]); - - fs.readFile.restore(); }); }); it("should reject on read error", () => { const files = [{ oldPath: "file-1.js" - }, { - oldPath: "file-2.js" }]; - sinon.stub(fs, "readFile", (filePath, opts, cb) => cb("mock error")); + mockfs({}); return fileUtils.readFiles(files).catch((err) => { - expect(err).to.equal("mock error"); - - fs.readFile.restore(); + expect(err).to.have.property("code", "ENOENT"); }); }); }); describe("readPackage", () => { it("should read the package.json file", () => { - sinon.stub(fs, "readFile", (filePath, opts, cb) => { - expect(filePath).to.equal("package.json"); - expect(opts).to.equal("utf8"); - cb(null, JSON.stringify({ + mockfs({ + "package.json": JSON.stringify({ dependencies: { lodash: "1.0.0" } - })); + }, null, 2) }); return fileUtils.readPackage().then((contents) => { @@ -87,100 +91,90 @@ describe("fileUtils", () => { lodash: "1.0.0" } }); + }); + }); - fs.readFile.restore(); + it("should reject on a JSON parse error", () => { + mockfs({ + "package.json": "bad" + }); + + return fileUtils.readPackage().catch((err) => { + expect(err).to.be.an.instanceOf(SyntaxError); }); }); it("should reject on read error", () => { - sinon.stub(fs, "readFile", (filePath, opts, cb) => cb("mock error")); + mockfs({}); return fileUtils.readPackage().catch((err) => { - expect(err).to.equal("mock error"); - - fs.readFile.restore(); + expect(err).to.have.property("code", "ENOENT"); }); }); }); describe("removeFile", () => { it("should not throw without a remove file error", () => { - sinon.stub(fs, "unlink", (filePath, cb) => cb()); - - return fileUtils.removeFile("remove.js").then((result) => { - expect(result).to.equal(undefined); - - fs.unlink.restore(); + mockfs({ + "remove.js": "mock contents" }); + + return fileUtils + .removeFile("remove.js") + .then(() => testHelpers.fileExists("remove.js")) + .then((fileExists) => { + expect(fileExists).to.equal(false); + }); }); it("should reject on an error", () => { - sinon.stub(fs, "unlink", (filePath, cb) => cb("mock error")); + mockfs({}); return fileUtils.removeFile("remove.js").catch((err) => { - expect(err).to.equal("mock error"); - - fs.unlink.restore(); + expect(err).to.have.property("code", "ENOENT"); }); }); }); describe("statFiles", () => { - it("should mark files created", () => { + it("should mark files created or not created", () => { const files = [{ - newPath: "new-file-1.js" + newPath: "new-file.js" }, { - newPath: "new-file-2.js" + newPath: "existing-file.js" }]; - sinon.stub(fs, "stat", (filePath, cb) => cb({code: "ENOENT"})); - - return fileUtils.statFiles(files).then((result) => { - expect(result).to.deep.equal([{ - newPath: "new-file-1.js", - created: true - }, { - newPath: "new-file-2.js", - created: true - }]); - - fs.stat.restore(); + mockfs({ + "existing-file.js": "mock contents" }); - }); - - it("should mark files not created", () => { - const files = [{ - newPath: "existing-file-1.js" - }, { - newPath: "existing-file-2.js" - }]; - - sinon.stub(fs, "stat", (filePath, cb) => cb()); return fileUtils.statFiles(files).then((result) => { expect(result).to.deep.equal([{ - newPath: "existing-file-1.js", - created: false + newPath: "new-file.js", + created: true }, { - newPath: "existing-file-2.js", + newPath: "existing-file.js", created: false }]); - - fs.stat.restore(); }); }); it("should reject on other stat errors", () => { const files = [{ - newPath: "existing-file-1.js" + newPath: "private/existing-file.js" }]; - sinon.stub(fs, "stat", (filePath, cb) => cb({code: "EIO"})); + mockfs({ + "private": mockfs.directory({ + items: { + "existing-file.js": "mock contents" + }, + mode: "0000" + }) + }); return fileUtils.statFiles(files).catch((err) => { - expect(err).to.deep.equal({code: "EIO"}); - - fs.stat.restore(); + expect(err).to.have.property("code", "EACCES"); }); }); }); @@ -188,79 +182,99 @@ describe("fileUtils", () => { describe("writeFiles", () => { it("should write and mark files", () => { const files = [{ - contents: "mock contents", + contents: "new contents 1", newPath: "file-1.js" }, { - contents: "mock contents", + contents: "new contents 2", newPath: "file-2.js" }]; - sinon.stub(fs, "writeFile", (filePath, contents, opts, cb) => cb()); + mockfs({ + "file-1.js": "old contents 1" + }); - return fileUtils.writeFiles(files).then((result) => { - expect(result).to.deep.equal([{ - contents: "mock contents", + return fileUtils + .writeFiles(files) + .then((result) => Promise.all([ + Promise.resolve(result), + testHelpers.readFile(files[0].newPath), + testHelpers.readFile(files[1].newPath) + ])) + .then((results) => { + expect(results[0]).to.deep.equal([{ + contents: "new contents 1", newPath: "file-1.js", - "written": true + written: true }, { - contents: "mock contents", + contents: "new contents 2", newPath: "file-2.js", - "written": true + written: true }]); - fs.writeFile.restore(); + expect(results[1]).to.equal("new contents 1"); + expect(results[2]).to.equal("new contents 2"); }); }); it("should reject on write error", () => { const files = [{ - contents: "mock contents", + contents: "new mock contents", newPath: "file-1.js" - }, { - contents: "mock contents", - newPath: "file-2.js" }]; - sinon.stub(fs, "writeFile", (filePath, contents, opts, cb) => cb("mock error")); + mockfs({ + "file-1.js": mockfs.file({ + mode: "0000" + }) + }); return fileUtils.writeFiles(files).catch((err) => { - expect(err).to.equal("mock error"); - - fs.writeFile.restore(); + expect(err).to.have.property("code", "EACCES"); }); }); }); describe("writePackage", () => { it("should write the package.json file", () => { - sinon.stub(fs, "writeFile", (filePath, contents, opts, cb) => { - expect(filePath).to.equal("package.json"); - expect(contents).to.equal(JSON.stringify({ + mockfs({ + "package.json": JSON.stringify({ + old: "contents" + }) + }); + + return fileUtils + .writePackage({ + dependencies: { + lodash: "1.0.0" + } + }) + .then(() => testHelpers.readFile("package.json")) + .then((result) => expect(result).to.equal(JSON.stringify({ dependencies: { lodash: "1.0.0" } - }, null, 2)); - expect(opts).to.equal("utf8"); + }, null, 2))); + }); - cb(); - }); + it("should reject on a stringify error", () => { + const packageJSON = {}; - return fileUtils.writePackage({ - dependencies: { - lodash: "1.0.0" - } - }).then(() => { - fs.writeFile.restore(); + packageJSON.packageJSON = packageJSON; // Create a circular structure + + return fileUtils.writePackage(packageJSON).catch((err) => { + expect(err).to.be.an.instanceOf(TypeError); }); }); - it("should reject on read error", () => { - sinon.stub(fs, "writeFile", (filePath, contents, opts, cb) => cb("mock error")); + it("should reject on write error", () => { + mockfs({ + "package.json": mockfs.file({ + mode: "0000" + }) + }); return fileUtils.writePackage().catch((err) => { - expect(err).to.equal("mock error"); - - fs.writeFile.restore(); + expect(err).to.have.property("code", "EACCES"); }); }); }); diff --git a/test/spec/postpublish.spec.js b/test/spec/postpublish.spec.js index 4045b21..ec7e1db 100644 --- a/test/spec/postpublish.spec.js +++ b/test/spec/postpublish.spec.js @@ -6,6 +6,16 @@ import sinon from "sinon"; describe("postpublish", () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + it("should fix files", () => { const contents = { _publishr: [{ @@ -14,21 +24,19 @@ describe("postpublish", () => { }] }; - sinon.stub(fileUtils, "readPackage", () => Promise.resolve(contents)); - sinon.stub(fileHandler, "fixFiles"); + sandbox.stub(fileUtils, "readPackage", () => Promise.resolve(contents)); + sandbox.stub(fileHandler, "fixFiles"); return postpublish().then(() => { expect(fileUtils.readPackage).to.have.callCount(1); - expect(fileHandler.fixFiles).to.have.callCount(1); - expect(fileHandler.fixFiles).to.have.been.calledWith({ - _publishr: [{ - created: true, - path: "file.js" - }] - }); - - fileHandler.fixFiles.restore(); - fileUtils.readPackage.restore(); + expect(fileHandler.fixFiles) + .to.have.callCount(1).and + .to.have.been.calledWith({ + _publishr: [{ + created: true, + path: "file.js" + }] + }); }); }); }); diff --git a/test/spec/postversion.spec.js b/test/spec/postversion.spec.js index 5f96a37..2e55703 100644 --- a/test/spec/postversion.spec.js +++ b/test/spec/postversion.spec.js @@ -6,6 +6,12 @@ import sinon from "sinon"; describe("postversion", () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + it("should overwrite files", () => { const contents = { publishr: { @@ -14,20 +20,18 @@ describe("postversion", () => { }; const files = [{newPath: "file.js"}]; - sinon.stub(fileHandler, "overwriteFiles", () => Promise.resolve(files)); - sinon.stub(fileUtils, "readPackage", () => Promise.resolve(contents)); + sandbox.stub(fileHandler, "overwriteFiles", () => Promise.resolve(files)); + sandbox.stub(fileUtils, "readPackage", () => Promise.resolve(contents)); return postversion().then(() => { expect(fileUtils.readPackage).to.have.callCount(1); - expect(fileHandler.overwriteFiles).to.have.callCount(1); - expect(fileHandler.overwriteFiles).to.have.been.calledWith({ - publishr: { - dependencies: ["^babel"] - } - }); - - fileHandler.overwriteFiles.restore(); - fileUtils.readPackage.restore(); + expect(fileHandler.overwriteFiles) + .to.have.callCount(1).and + .to.have.been.calledWith({ + publishr: { + dependencies: ["^babel"] + } + }); }); }); }); diff --git a/test/test-helpers.js b/test/test-helpers.js new file mode 100644 index 0000000..30954bd --- /dev/null +++ b/test/test-helpers.js @@ -0,0 +1,33 @@ +import {Promise} from "es6-promise"; +import fs from "fs"; + + +const testHelpers = { + fileExists(filePath) { + return new Promise((resolve, reject) => { + fs.stat(filePath, (err) => { + if (err && err.code === "ENOENT") { + resolve(false); + } else if (!err) { + resolve(true); + } else { + reject(err); + } + }); + }); + }, + + readFile(filePath) { + return new Promise((resolve, reject) => { + fs.readFile(filePath, "utf8", (err, contents) => { + if (err) { + return reject(err); + } + + return resolve(contents); + }); + }); + } +}; + +export default testHelpers;