diff --git a/packages/stack/pipeline/package.json b/packages/stack/pipeline/package.json index f6cbcbf9e..a3e89f12b 100644 --- a/packages/stack/pipeline/package.json +++ b/packages/stack/pipeline/package.json @@ -38,7 +38,8 @@ "lint": "eslint src/", "qa": "npm-run-all lint _typecheck _build", "reset": "npx rimraf dist embark-*.tgz package", - "solo": "embark-solo" + "solo": "embark-solo", + "test": "jest" }, "eslintConfig": { "extends": "../../../.eslintrc.json" @@ -46,17 +47,14 @@ "dependencies": { "@babel/runtime-corejs3": "7.7.4", "async": "2.6.1", - "colors": "1.3.2", "core-js": "3.4.3", - "embark-core": "^5.2.0-nightly.2", - "embark-i18n": "^5.1.1", - "embark-utils": "^5.2.0-nightly.1", - "find-up": "2.1.0", - "fs-extra": "8.1.0" + "embark-utils": "^5.2.0-nightly.1" }, "devDependencies": { + "babel-jest": "24.9.0", "embark-solo": "^5.1.1", "eslint": "5.7.0", + "jest": "24.9.0", "npm-run-all": "4.1.5", "rimraf": "3.0.0" }, @@ -64,5 +62,20 @@ "node": ">=10.17.0", "npm": ">=6.11.3", "yarn": ">=1.19.1" + }, + "jest": { + "collectCoverage": true, + "testEnvironment": "node", + "testMatch": [ + "**/test/**/*.js" + ], + "transform": { + "\\.(js|ts)$": [ + "babel-jest", + { + "rootMode": "upward" + } + ] + } } } diff --git a/packages/stack/pipeline/src/api.js b/packages/stack/pipeline/src/api.js index e6f7a34a6..795bf04c8 100644 --- a/packages/stack/pipeline/src/api.js +++ b/packages/stack/pipeline/src/api.js @@ -4,13 +4,13 @@ import { dappPath, fileTreeSort } from 'embark-utils'; class API { constructor(embark, _options) { + this.embark = embark; this.plugins = embark.config.plugins; this.fs = embark.fs; } registerAPIs() { - let plugin = this.plugins.createPlugin('deployment', {}); - plugin.registerAPICall( + this.embark.registerAPICall( 'get', '/embark-api/file', (req, res) => { @@ -26,7 +26,7 @@ class API { } ); - plugin.registerAPICall( + this.embark.registerAPICall( 'post', '/embark-api/folders', (req, res) => { @@ -42,7 +42,7 @@ class API { } ); - plugin.registerAPICall( + this.embark.registerAPICall( 'post', '/embark-api/files', (req, res) => { @@ -58,7 +58,7 @@ class API { } ); - plugin.registerAPICall( + this.embark.registerAPICall( 'delete', '/embark-api/file', (req, res) => { @@ -72,7 +72,7 @@ class API { } ); - plugin.registerAPICall( + this.embark.registerAPICall( 'get', '/embark-api/files', (req, res) => { diff --git a/packages/stack/pipeline/src/index.js b/packages/stack/pipeline/src/index.js index e6c9b6b41..10755a687 100644 --- a/packages/stack/pipeline/src/index.js +++ b/packages/stack/pipeline/src/index.js @@ -24,7 +24,7 @@ class Pipeline { generateAll(cb) { async.waterfall([ (next) => { - this.plugins.runActionsForEvent("pipeline:generateAll:before", (err) => { + this.plugins.runActionsForEvent("pipeline:generateAll:before", {}, (err) => { next(err); }); }, diff --git a/packages/stack/pipeline/test/api.spec.js b/packages/stack/pipeline/test/api.spec.js new file mode 100644 index 000000000..93578f55a --- /dev/null +++ b/packages/stack/pipeline/test/api.spec.js @@ -0,0 +1,254 @@ +/* global describe, beforeEach, afterEach, test */ +import assert from 'assert'; +import sinon from 'sinon'; +import { fakeEmbark } from 'embark-testing'; +import API from '../src/api'; +import path from 'path'; + +// Due to our `DAPP_PATH` dependency in `embark-utils` `dappPath()`, we need to +// ensure that this environment variable is defined. +const DAPP_PATH = 'something'; +process.env.DAPP_PATH = DAPP_PATH; + +describe('stack/pipeline/api', () => { + + const { embark } = fakeEmbark(); + + let pipelineApi; + + beforeEach(() => { + pipelineApi = new API(embark); + }); + + afterEach(() => { + embark.teardown(); + sinon.restore(); + }); + + describe('constructor', () => { + test('it should assign the correct properties', () => { + assert.strictEqual(pipelineApi.plugins, embark.plugins); + assert.strictEqual(pipelineApi.fs, embark.fs); + }); + }); + + describe('methods', () => { + describe('apiGuardBadFile', () => { + let pathToCheck; + const pathToFile = "/path/to/file"; + const error = { message: 'Path is invalid' }; + beforeEach(() => { + pathToCheck = path.join(DAPP_PATH, pathToFile); + }); + test('it should throw when file doesn\'t exist and is expected to exist', () => { + const options = { ensureExists: true }; + const existsSync = sinon.fake.returns(false); + sinon.replace(pipelineApi.fs, 'existsSync', existsSync); + assert.throws(() => pipelineApi.apiGuardBadFile(pathToCheck, options), error); + }); + test('it should not throw when file exists and is expected to exist', () => { + const options = { ensureExists: true }; + const existsSync = sinon.fake.returns(true); + sinon.replace(pipelineApi.fs, 'existsSync', existsSync); + assert.doesNotThrow(() => pipelineApi.apiGuardBadFile(pathToCheck, options)); + }); + test('it should throw when file is not in the dappPath', () => { + assert.throws(() => pipelineApi.apiGuardBadFile(pathToFile), error); + }); + test('it should not throw when file is in the dappPath', () => { + assert.doesNotThrow(() => pipelineApi.apiGuardBadFile(pathToCheck)); + }); + }); + + describe('registerAPIs', () => { + describe('GET /embark-api/file', () => { + let req, readFileSync; + const method = "GET"; + const endpoint = "/embark-api/file"; + const filepath = path.join(DAPP_PATH, "/path/to/file"); + beforeEach(() => { + req = { path: filepath }; + readFileSync = sinon.stub().returns("content"); + pipelineApi.fs = { readFileSync }; + + pipelineApi.registerAPIs(); + }); + + test(`it should register ${method} ${endpoint}`, () => { + pipelineApi.plugins.assert.apiCallRegistered(method, endpoint); + }); + test('it should throw error when guarding bad files', () => { + const error = "testing error"; + pipelineApi.apiGuardBadFile = sinon.stub().throws(new Error(error)); + const resp = pipelineApi.plugins.mock.apiCall(method, endpoint, req); + assert(resp.send.calledWith({ error })); + }); + test('it should return a file', () => { + pipelineApi.apiGuardBadFile = sinon.stub().returns(); + const resp = pipelineApi.plugins.mock.apiCall(method, endpoint, req); + assert(readFileSync.calledWith(filepath, 'utf8')); + assert(resp.send.calledWith({ name: "file", content: "content", path: filepath })); + }); + }); + + describe('POST /embark-api/folders', () => { + let req, mkdirpSync; + const method = "POST"; + const endpoint = "/embark-api/folders"; + const filepath = path.join(DAPP_PATH, "/path/to/folder"); + beforeEach(() => { + req = { path: filepath }; + mkdirpSync = sinon.stub(); + pipelineApi.fs = { mkdirpSync }; + + pipelineApi.registerAPIs(); + }); + + test(`it should register ${method} ${endpoint}`, () => { + pipelineApi.plugins.assert.apiCallRegistered(method, endpoint); + }); + test('it should throw error when guarding bad files', () => { + const error = "testing error"; + pipelineApi.apiGuardBadFile = sinon.stub().throws(new Error(error)); + const resp = pipelineApi.plugins.mock.apiCall(method, endpoint, req); + assert(resp.send.calledWith({ error })); + }); + test('it should create a folder', () => { + pipelineApi.apiGuardBadFile = sinon.stub().returns(); + const resp = pipelineApi.plugins.mock.apiCall(method, endpoint, req); + assert(mkdirpSync.calledWith(filepath)); + assert(resp.send.calledWith({ name: "folder", path: filepath })); + }); + }); + + describe('POST /embark-api/files', () => { + let req, writeFileSync; + const method = "POST"; + const endpoint = "/embark-api/files"; + const filepath = path.join(DAPP_PATH, "/path/to/file"); + beforeEach(() => { + req = { path: filepath, content: "content" }; + writeFileSync = sinon.stub(); + pipelineApi.fs = { writeFileSync }; + + pipelineApi.registerAPIs(); + }); + + test(`it should register ${method} ${endpoint}`, () => { + pipelineApi.plugins.assert.apiCallRegistered(method, endpoint); + }); + test('it should throw error when guarding bad files', () => { + const error = "testing error"; + pipelineApi.apiGuardBadFile = sinon.stub().throws(new Error(error)); + const resp = pipelineApi.plugins.mock.apiCall(method, endpoint, req); + assert(resp.send.calledWith({ error })); + }); + test('it should write a file to the filesystem', () => { + pipelineApi.apiGuardBadFile = sinon.stub().returns(); + const resp = pipelineApi.plugins.mock.apiCall(method, endpoint, req); + assert(writeFileSync.calledWith(req.path, req.content, { encoding: 'utf8' })); + assert(resp.send.calledWith({ name: "file", ...req })); + }); + }); + + describe('DELETE /embark-api/file', () => { + let req, removeSync; + const method = "DELETE"; + const endpoint = "/embark-api/file"; + const filepath = path.join(DAPP_PATH, "/path/to/file"); + beforeEach(() => { + req = { path: filepath, content: "content" }; + removeSync = sinon.stub(); + pipelineApi.fs = { removeSync }; + + pipelineApi.registerAPIs(); + }); + + test(`it should register ${method} ${endpoint}`, () => { + pipelineApi.plugins.assert.apiCallRegistered(method, endpoint); + }); + test('it should throw error when guarding bad files', () => { + const error = "testing error"; + pipelineApi.apiGuardBadFile = sinon.stub().throws(new Error(error)); + const resp = pipelineApi.plugins.mock.apiCall(method, endpoint, req); + assert(resp.send.calledWith({ error })); + }); + test('it should delete a file from the filesystem', () => { + pipelineApi.apiGuardBadFile = sinon.stub().returns(); + const resp = pipelineApi.plugins.mock.apiCall(method, endpoint, req); + assert(removeSync.calledWith(req.path)); + assert(resp.send.called); + }); + }); + + describe('GET /embark-api/files', () => { + let req, readdirSync, statSync; + const method = "GET"; + const endpoint = "/embark-api/files"; + const file = "file"; + const fileHidden = ".file"; + const folder = "folder"; + const child = "child"; + beforeEach(() => { + req = {}; + readdirSync = sinon.stub(); + readdirSync.withArgs(DAPP_PATH).returns([ + file, + fileHidden, + folder + ]); + readdirSync.withArgs(path.join(DAPP_PATH, folder)).returns([child]); + + statSync = sinon.stub(); + statSync.returns({ isDirectory: () => false }); + statSync.withArgs(path.join(DAPP_PATH, folder)).returns({ isDirectory: () => true }); + + pipelineApi.fs = { readdirSync, statSync }; + + pipelineApi.registerAPIs(); + }); + + test(`it should register ${method} ${endpoint}`, () => { + pipelineApi.plugins.assert.apiCallRegistered(method, endpoint); + }); + test('it should return a tree of file objects for the dapp', () => { + const resp = pipelineApi.plugins.mock.apiCall(method, endpoint, req); + const expectedValue = [ + { + isRoot: true, + name: 'folder', + dirname: 'something', + path: path.join(DAPP_PATH, folder), + isHidden: false, + children: [ + { + name: 'child', + isRoot: false, + path: path.join(DAPP_PATH, folder, child), + dirname: path.join(DAPP_PATH, folder), + isHidden: false + } + ] + }, + { + name: '.file', + isRoot: true, + path: path.join(DAPP_PATH, fileHidden), + dirname: 'something', + isHidden: true + }, + { + name: 'file', + isRoot: true, + path: path.join(DAPP_PATH, file), + dirname: 'something', + isHidden: false + } + ]; + assert(resp.send.calledWith(expectedValue)); + }); + }); + }); + + }); +}); diff --git a/packages/stack/pipeline/test/pipeline.spec.js b/packages/stack/pipeline/test/pipeline.spec.js new file mode 100644 index 000000000..c72ce6954 --- /dev/null +++ b/packages/stack/pipeline/test/pipeline.spec.js @@ -0,0 +1,188 @@ +/* global describe, beforeEach, afterEach, test */ +import assert from 'assert'; +import sinon from 'sinon'; +import { fakeEmbark } from 'embark-testing'; +import Pipeline from '../src/'; +import path from 'path'; + +// Due to our `DAPP_PATH` dependency in `embark-utils` `dappPath()`, we need to +// ensure that this environment variable is defined. +const DAPP_PATH = 'something'; +process.env.DAPP_PATH = DAPP_PATH; + +describe('stack/pipeline', () => { + + const { embark } = fakeEmbark(); + + let pipeline; + + beforeEach(() => { + pipeline = new Pipeline(embark); + }); + + afterEach(() => { + embark.teardown(); + sinon.restore(); + }); + + describe('constructor', () => { + test('it should assign the correct properties', () => { + assert.strictEqual(pipeline.events, embark.events); + assert.strictEqual(pipeline.plugins, embark.plugins); + assert.strictEqual(pipeline.fs, embark.fs); + }); + test('it should register command handler for pipeline:generateAll', () => { + pipeline.events.assert.commandHandlerRegistered("pipeline:generateAll"); + }); + + test('it should register command handler for pipeline:register', () => { + pipeline.events.assert.commandHandlerRegistered("pipeline:register"); + }); + }); + + describe('methods', () => { + describe('generateAll', () => { + let action, file1, file2, eachCb; + beforeEach(() => { + action = sinon.spy(pipeline.plugins, "runActionsForEvent"); + file1 = {format: "json"}; + file2 = {format: "other"}; + pipeline.files = {file1, file2}; + eachCb = sinon.fake.yields(null, null); + sinon.replace(pipeline, 'writeFile', eachCb); + sinon.replace(pipeline, 'writeJSONFile', eachCb); + }); + test('it should run before action', () => { + pipeline.generateAll(() => {}); + assert(action.calledWith("pipeline:generateAll:before", {})); + }); + test('it should write JSON files', () => { + pipeline.generateAll(() => {}); + assert(pipeline.writeJSONFile.calledWith(file1)); + }); + test('it should write other files', () => { + pipeline.generateAll(() => {}); + assert(pipeline.writeFile.calledWith(file2)); + }); + test('it should write no files when none registered', () => { + pipeline.files = {}; + pipeline.generateAll(() => {}); + assert(pipeline.writeJSONFile.notCalled); + assert(pipeline.writeFile.notCalled); + }); + test('it should run after action', () => { + pipeline.generateAll(() => {}); + assert(action.calledWith("pipeline:generateAll:after", {})); + }); + test('it should call callback', () => { + const cb = sinon.spy(); + pipeline.generateAll(cb); + assert(cb.called); + }); + }); + + describe('writeJSONFile', () => { + let file1, nextCb; + beforeEach(() => { + file1 = {format: "json", path: "path/to/json", content: "file 1 content", file: "nameJson"}; + nextCb = sinon.fake.yields(null, null); + sinon.replace(pipeline.fs, 'mkdirp', nextCb); + sinon.replace(pipeline.fs, 'writeJson', nextCb); + }); + test('it should make directory', () => { + const dir = path.join(DAPP_PATH, ...file1.path); + pipeline.writeJSONFile(file1, () => {}); + assert(pipeline.fs.mkdirp.calledWith(dir)); + assert(nextCb.called); + }); + test('it should write JSON files', () => { + const filename = path.join(DAPP_PATH, ...file1.path, file1.file); + pipeline.writeJSONFile(file1, () => {}); + assert(pipeline.fs.writeJson.calledWith(filename, file1.content, {spaces: 2})); + assert(nextCb.called); + }); + test('it should call callback', () => { + const cb = sinon.spy(); + pipeline.writeJSONFile(file1, cb); + assert(cb.called); + }); + test('it should bubble error to callback', () => { + const cb = sinon.spy(); + const error = "error"; + sinon.restore(); + nextCb = sinon.fake.yields(error); + sinon.replace(pipeline.fs, 'mkdirp', nextCb); + pipeline.writeJSONFile(file1, cb); + assert(cb.calledWith(error)); + }); + }); + + describe('writeFile', () => { + let file1, nextCb; + beforeEach(() => { + file1 = {format: "json", path: "path/to/json", content: "file 1 content", file: "nameJson"}; + nextCb = sinon.fake.yields(null, null); + sinon.replace(pipeline.fs, 'mkdirp', nextCb); + sinon.replace(pipeline.fs, 'writeFile', nextCb); + }); + test('it should make directory', () => { + const dir = path.join(DAPP_PATH, ...file1.path); + pipeline.writeFile(file1, () => {}); + assert(pipeline.fs.mkdirp.calledWith(dir)); + assert(nextCb.called); + }); + test('it should write JSON files', () => { + const filename = path.join(DAPP_PATH, ...file1.path, file1.file); + pipeline.writeFile(file1, () => {}); + assert(pipeline.fs.writeFile.calledWith(filename, file1.content)); + assert(nextCb.called); + }); + test('it should call callback', () => { + const cb = sinon.spy(); + pipeline.writeFile(file1, cb); + assert(cb.called); + }); + test('it should bubble error to callback', () => { + const cb = sinon.spy(); + const error = "error"; + sinon.restore(); + nextCb = sinon.fake.yields(error); + sinon.replace(pipeline.fs, 'mkdirp', nextCb); + pipeline.writeFile(file1, cb); + assert(cb.calledWith(error)); + }); + }); + }); + + describe('implementation', () => { + describe('register file', () => { + test('it should register a file', async () => { + const params = { path: "path/to", file: "file" }; + await pipeline.events.request2("pipeline:register", params); + pipeline.events.assert.commandHandlerCalledWith("pipeline:register", params); + + const filepath = path.join(DAPP_PATH, ...params.path, params.file); + assert.equal(pipeline.files[filepath], params, "File not added to pipeline files array"); + }); + }); + describe('generate files', () => { + test('it should run actions for "pipeline:generateAll:before"', async () => { + const action = (params, cb) => { + cb(); + }; + pipeline.plugins.registerActionForEvent('pipeline:generateAll:before', action); + await pipeline.events.request2("pipeline:generateAll"); + pipeline.plugins.assert.actionForEventCalled('pipeline:generateAll:before', action); + }); + test('it should run actions for "pipeline:generateAll:after"', async () => { + const action = (params, cb) => { + cb(); + }; + pipeline.plugins.registerActionForEvent('pipeline:generateAll:after', action); + await pipeline.events.request2("pipeline:generateAll"); + pipeline.plugins.assert.actionForEventCalled('pipeline:generateAll:after', action); + }); + }); + }); +}); + diff --git a/packages/stack/pipeline/tsconfig.json b/packages/stack/pipeline/tsconfig.json index fecd8c4e1..dac886458 100644 --- a/packages/stack/pipeline/tsconfig.json +++ b/packages/stack/pipeline/tsconfig.json @@ -10,12 +10,6 @@ "src/**/*" ], "references": [ - { - "path": "../../core/core" - }, - { - "path": "../../core/i18n" - }, { "path": "../../core/utils" } diff --git a/packages/utils/testing/src/lib/embark.js b/packages/utils/testing/src/lib/embark.js index 7b45accb9..347242d97 100644 --- a/packages/utils/testing/src/lib/embark.js +++ b/packages/utils/testing/src/lib/embark.js @@ -6,6 +6,7 @@ class Embark { this.events = events; this.plugins = plugins; this.config = config || {}; + this.config.plugins = plugins; this.assert = new EmbarkAssert(this); this.fs = fs; @@ -17,6 +18,10 @@ class Embark { }; } + registerAPICall(method, endpoint, callback) { + this.plugins.plugin.registerAPICall(method, endpoint, callback); + } + registerActionForEvent(name, cb) { this.plugins.registerActionForEvent(name, cb); } @@ -26,12 +31,12 @@ class Embark { } teardown() { - this.config = {}; + this.config = { plugins: this.plugins }; this.plugins.teardown(); } setConfig(config) { - this.config = config; + this.config = { ...config, plugins: this.plugins }; } } diff --git a/packages/utils/testing/src/lib/plugin.js b/packages/utils/testing/src/lib/plugin.js index 3fe250945..a20bc0876 100644 --- a/packages/utils/testing/src/lib/plugin.js +++ b/packages/utils/testing/src/lib/plugin.js @@ -1,7 +1,12 @@ +const assert = require('assert'); +const sinon = require('sinon'); + class Plugins { constructor() { this.plugin = new Plugin(); this.plugins = []; + this.assert = new PluginsAssert(this); + this.mock = new PluginsMock(this); } createPlugin(name) { @@ -17,7 +22,7 @@ class Plugins { runActionsForEvent(name, options, callback) { const listeners = this.plugin.getListeners(name); if (listeners) { - listeners.forEach(fn => fn(options, callback)); + listeners.forEach(fn => fn.spy(options, callback)); } else { callback(null, options); } @@ -52,6 +57,7 @@ class Plugins { class Plugin { constructor() { this.listeners = {}; + this.apiCalls = {}; this.pluginTypes = []; this.console = []; this.compilers = []; @@ -65,7 +71,7 @@ class Plugin { if (!this.listeners[name]) { this.listeners[name] = []; } - this.listeners[name].push(action); + this.listeners[name].push({ raw: action, spy: sinon.spy(action) }); } has(pluginType) { @@ -82,8 +88,9 @@ class Plugin { this.addPluginType('compilers'); } - registerAPICall(_method, _endpoint, _callback) { - + registerAPICall(method, endpoint, callback) { + const index = (method + endpoint).toLowerCase(); + this.apiCalls[index] = callback; } registerConsoleCommand(options) { @@ -96,5 +103,59 @@ class Plugin { } } +class PluginsAssert { + constructor(plugins) { + this.plugins = plugins; + } + actionForEventRegistered(name, action) { + assert(this.plugins.plugin.listeners[name] && this.plugins.plugin.listeners[name].some(registered => registered.raw === action), `action for ${name} wanted, but not registered`); + } + actionForEventCalled(name, action) { + this.actionForEventRegistered(name, action); + const registered = this.plugins.plugin.listeners[name].find(registered => registered.raw === action); + sinon.assert.called(registered.spy); + } + + actionForEventCalledWith(name, action, ...args) { + this.actionForEventRegistered(name, action); + const registered = this.plugins.plugin.listeners[name].find(registered => registered.raw === action); + sinon.assert.calledWith(registered.spy, ...args); + } + + apiCallRegistered(method, endpoint) { + const index = (method + endpoint).toLowerCase(); + assert(this.plugins.plugin.apiCalls[index], `API call for '${method} ${endpoint}' wanted, but not registered`); + } +} + +class PluginsMock { + constructor(plugins) { + this.plugins = plugins; + } + + apiCall(method, endpoint, params) { + const index = (method + endpoint).toLowerCase(); + const apiFn = this.plugins.plugin.apiCalls[index]; + assert(apiFn, `API call for '${method} ${endpoint}' wanted, but not registered`); + + let req; + if (["GET", "DELETE"].includes(method.toUpperCase())) { + req = { + query: params + }; + } else { + req = { + body: params + }; + } + const resp = { + send: sinon.spy(), + status: sinon.spy() + }; + apiFn(req, resp); + return resp; + } +} + module.exports = Plugins;