chore(@embark/pipeline): Update pipeline dependencies and add tests

Update `embark-pipeline` dependencies.

Add unit tests for `embark-pipeline`.

Update Embark testing framework to include assertion testsing for API calls and event actions.
This commit is contained in:
emizzle 2020-02-06 17:33:55 +11:00 committed by Iuri Matias
parent d14e93ceb1
commit a7693c0e53
8 changed files with 541 additions and 26 deletions

View File

@ -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"
}
]
}
}
}

View File

@ -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) => {

View File

@ -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);
});
},

View File

@ -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));
});
});
});
});
});

View File

@ -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);
});
});
});
});

View File

@ -10,12 +10,6 @@
"src/**/*"
],
"references": [
{
"path": "../../core/core"
},
{
"path": "../../core/i18n"
},
{
"path": "../../core/utils"
}

View File

@ -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 };
}
}

View File

@ -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;