I have a test file with a single describe that I want to run it twice but each time I want to pass different parameters into it.
My code looks like that:
MochaRunner.js
'use strict';
const fs = require('fs');
const path = require('path');
const Mocha = require('mocha');
const uuidv1 = require('uuid/v1');
class MochaRunner {
/**
* #param {Object} options
*/
constructor(options = {}) {
this._tmps = [];
this._mocha = new Mocha(options);
}
/**
* Run tests
* #param {Array} tests
* #returns {Promise}
*/
run(tests) {
return new Promise((resolve, reject) => {
const Config = require('../config').create();
let counter = 0;
tests.forEach(test => {
const testDir = path.dirname(test.testPath);
const tmpTest = path.join(testDir,`${uuidv1()}.spec.js`);
fs.writeFileSync(tmpTest, fs.readFileSync(test.testPath));
this._tmps.push(tmpTest);
this._mocha.addFile(tmpTest);
Config.setOptions('var ' + counter++);
});
this._mocha.run((err) => {
return err ? reject(err) : resolve();
});
}).catch(() => {
return this.cleanup();
});
}
/**
* Get mocha instance
* #returns {Mocha}
*/
getMocha() {
return this._mocha;
}
/**
* Remove tmp test files
* #returns {Promise}
*/
cleanup() {
this._tmps.forEach(tmpTest => {
fs.unlinkSync(tmpTest);
});
return Promise.resolve();
}
}
module.exports = MochaRunner;
Config.js
class Config {
constructor() {
this.options = {};
}
setOptions(options){
this.options = options;
}
}
module.exports = {
_instance: null,
create: function(){
if (this._instance){
return this._instance;
}
this._instance= new Config();
return this._instance;
}
};
test.e2e.test.js
const chai = require('chai');
const Config = require('./config').create();
const assertionStyles = {
assert: chai.assert,
expect: chai.expect,
should: chai.should,
};
setImmediate(async () => {
describe('describe blah blha', function () {
console.log(Config.options);
it('test case', function () {
assertionStyles.expect(true).to.be.true;
});
});
run();
});
I do this when I want to run the above code:
let tests = ['test.e2e.test.js', 'test.e2e.test.js'];
let mocha = new MochaRunner({});
await mochaRunner.run(tests).then(() => {
mochaRunner.cleanup();
});
In the above example, I want to pass different data when the test will run for second time.
For example, I want the first file read the param from Config with the value:
'var 0'
and the second file read the param with the value:
'var 1'
Any ideas please?
Any help would be greatly appreciated!
Related
I'm attempting to dynamically set properties for an HTTP response using the getL function, but it gives me back a Promise and I cannot set async over the forEach, since that breaks it (see the generateLinksForList function). Is there another way to do this?
/**
* #module domain/helper
*/
const {
complement,
compose,
isNil,
pickBy
} = require('ramda');
const notNull = compose(complement(isNil));
/**
* #function cleanData
* #returns {undefined}
*/
const cleanData = (entity) => pickBy(notNull, entity);
/**
* #name generateRelations
* #returns {undefined}
*/
const generateRelations = () => ([]);
async function getL(repo, entityContext) {
let d = await repo.nextItem(entityContext);
console.log(d)
return d;
}
/**
* #name generateLinksForList
* #returns {undefined}
*/
const generateLinksForList = (entityContext, type, entityName, repo) => {
const relations = require(`./${entityName}/relations.js`)
const namespace = type + 'Relations';
const _relations = relations[namespace];
let host = 'http://localhost:4000';
let relationsList = [];
_relations.forEach((linkRelation) => {
Object.keys(linkRelation).forEach((key) => {
if (linkRelation.hasOwnProperty('next')) {
let relation = {};
let l = getL(repo, entityContext);
console.log('---- getL');
console.log(l);
if (l.length) {
relation.next = linkRelation.next
.replace('{nextId}', l[0].id)
}
relation.next = linkRelation.next
.replace('{fullhost}', host)
.replace('{id}', entityContext.id);
relationsList.push(relation);
}
if (linkRelation.hasOwnProperty('self')) {
let relation = {};
relation.self = linkRelation['self']
.replace('{fullhost}', host)
.replace('{id}', entityContext.id);
relationsList.push(relation);
}
})
});
return relationsList;
};
/**
* #function generateLinksForItem
* #static
* #returns {array}
*/
const generateLinksForItem = (entityContext, type, entityName) => {
const relations = require(`./${entityName}/relations.js`)
const namespace = type + 'Relations';
const _relations = relations[namespace];
let host = 'http://localhost:4000';
let token, eventToken;
let _list = [];
_relations.forEach((relRef) => {
Object.keys(relRef).forEach((keyRef) => {
if (relRef[keyRef].includes('{id}')) {
relRef[keyRef] = relRef[keyRef].replace(/{id}/, entityContext.id);
}
if (relRef[keyRef].includes('{fullhost}')) {
relRef[keyRef] = relRef[keyRef].replace(/{fullhost}/, host);
}
if (relRef[keyRef].includes('{token}')) {
relRef[keyRef] = relRef[keyRef].replace(/{token}/, token);
}
if (relRef[keyRef].includes('{eventToken}')) {
relRef[keyRef] = relRef[keyRef].replace(/{eventToken}/, eventToken);
}
_list.push({
rel: keyRef,
href: relRef[keyRef]
});
});
});
return _list;
};
/**
* #function generateClassList
* #static
* #returns {array}
*/
const generateClassList = (context) => (['organization']);
/**
* #function generateEntities
* #param {object} repo Repostory for generating entities under a resource.
* #returns {array}
*/
const generateEntities = (repo) => {
return repo.getAll()
.then((documentSnapshots) => {
return documentSnapshots.map((doc) => {
if (doc.exists) {
let data = doc.data()
return {
//class: generateClassList(data.id),
//rel: generateRelations(data.id),
properties: data
//links: generateLinksForItem(data.id, 'item', 'user')
};
}
});
});
};
/**
* #function generateActions
* #static
* #returns {array}
* #description
* Ok, so here we're basically scripting with CasperJS. The presumption is
* that there will be a generic client through which we can automate browser
* behaviors and platform capabilities (Cordova). We can think of hypermedia
* controls as projections from rhetorical relations over a domain knowledge
* graph that represents the plurality of connections or links in hypermedia
* ensembles (differentiations of numerical and other kinds of identity).
*/
const generateActions = (_itemForms, entity, entityName) => {
let host = `http://localhost:4000/api/${entityName}`;
_itemForms.forEach(function (itemRef, key) {
Object.keys(itemRef).forEach(function (keyRef) {
if (itemRef[keyRef].includes('{id}')) {
itemRef[keyRef] = itemRef[keyRef].replace(/{id}/, entity.id);
}
if (itemRef[keyRef].includes('{fullhost}')) {
itemRef[keyRef] = itemRef[keyRef].replace(/{fullhost}/, host);
}
if (itemRef.hasOwnProperty('properties')) {
itemRef.properties.forEach((p) => {
Object.keys(p).forEach((k) => {
if (p[k].includes('{status}')) {
p[k] = p[k].replace(/{status}/, 'pending');
}
});
});
}
});
});
return _itemForms;
};
module.exports = {
cleanData,
generateLinksForItem,
generateLinksForList,
generateClassList,
generateActions,
generateEntities
};
// EOF
This is where I'm using the generateLinksForList function, hence why I cannot use async for the containing map:
/**
* #module app/place/get
* #description
* Get all places.
*/
const { itemForms } = require('../../domain/place/transitions');
const { Place } = require('../../domain/place');
const {
generateActions,
generateLinksForList,
} = require('../../domain/helper.js');
module.exports = ({ placeRepository }) => {
const all = () => {
return Promise
.resolve()
.then(() => placeRepository.getAll()
.then((documentSnapshots) => {
return documentSnapshots.map((doc) => {
let properties = doc.data() || {};
let place = Place(properties);
let links = generateLinksForList(place, 'item', 'place', placeRepository);
let actions = generateActions(itemForms, doc, 'places');
let props = Object.assign({}, place, {});
let hypermediaResponse = {
actions: actions,
links: links,
properties: props,
class: ['place'],
entities: []
};
if (doc.exists) {
return hypermediaResponse;
}
});
})
)
.catch(error => {
throw new Error(error);
})
}
return {
all
};
};
// EOF
The repo function:
/**
* #module infrastructure/repositories/place/index
* #description
* Repo/methods for places.
*/
const { toEntity } = require('./transform');
module.exports = ({ model, database }) => {
/**
* #name getAll
* #param {Object} args - Firestore clause (where, etc.);
* #returns {Object}
*/
const _getAll = async (...args) =>
await model
.listDocuments().then((docRefs) => database.firestore.getAll(...docRefs));
/**
* #name getAll
* #returns {Promise}
*/
const getAll = async () =>
await model.listDocuments()
.then((docRefs) => database.firestore.getAll(...docRefs));
/**
* #name prev
* #returns {undefined}
*/
const prev = async (...args) => {
let payload = args[0];
let docRef = model.doc(payload.id);
let snapshot = await docRef.get();
let last = snapshot.docs[snapshot.docs.length - 1];
return last.data();
};
/**
* #name next
* #returns {undefined}
*/
const nextItem = async (...args) => {
let payload = args[0];
const docRef = model.doc(payload.id);
const snapshot = (await docRef.get()).data();
const start = await model
.orderBy('createdAt')
.endBefore(snapshot.createdAt)
.limitToLast(1)
//.startAt(snapshot)
//.offset(1)
//.limit(1)
.get();
let list = [];
start.forEach((d) => {
list.push(d.data());
});
return list;
};
/**
* #name create
* #returns {object}
*/
const create = async (...args) => {
let payload = args[0];
let docRef = await model.doc(payload.id);
await docRef.set(payload);
return docRef.get().then((documentSnapshot) => {
return documentSnapshot.data();
})
};
/**
* #name update
* #returns {object}
*/
const update = async (...args) =>
await model.doc(args.id).update(...args);
/**
* #name findById
* #returns {object}
*/
const findById = async (...args) =>
await model.where('id', '==', args.id).get();
/**
* #name findOne
* #returns {object}
*/
const findOne = async (...args) => await model
.limit(1)
.get();
/**
* #name destroy
* #returns {object}
*/
const destroy = async (...args) =>
await model.doc(args.id).delete();
return {
getAll,
create,
update,
findById,
findOne,
destroy,
nextItem,
prev
};
};
// EOF
.forEach won't wait for an async function. Use a normal loop:
for ( const linkRelation of relations ) {
for ( const key in linkRelation) {
...
const l = await getL(repo, entityContext);
...
}
}
I'm trying to sign a Psbt transaction from bitcoinjs-lib following what I found here:
https://github.com/helperbit/helperbit-wallet/blob/master/app/components/dashboard.wallet/bitcoin.service/ledger.ts
I've checked that the compressed publicKey both from ledger, and the one from bitcoinjsLib returned the same value.
I could sign it with the bitcoinjs-lib ECPair, but when I tries to sign it using ledger, it is always invalid.
Can someone helps me point out where did I made a mistake?
These variables is already mentioned in the code below, but for clarity purpose:
- mnemonics:
abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
- previousTx:
02000000000101869362410c61a69ab9390b2167d08219662196e869626e8b0350f1a8e4075efb0100000017160014ef3fdddccdb6b53e6dd1f5a97299a6ba2e1c11c3ffffffff0240420f000000000017a914f748afee815f78f97672be5a9840056d8ed77f4887df9de6050000000017a9142ff4aa6ffa987335c7bdba58ef4cbfecbe9e49938702473044022061a01bf0fbac4650a9b3d035b3d9282255a5c6040aa1d04fd9b6b52ed9f4d20a022064e8e2739ef532e6b2cb461321dd20f5a5d63cf34da3056c428475d42c9aff870121025fb5240daab4cee5fa097eef475f3f2e004f7be702c421b6607d8afea1affa9b00000000
- paths:
["0'/0/0"]
- redeemScript: (non-multisig segwit)
00144328adace54072cd069abf108f97cf80420b212b
This is my minimum reproducible code I've got.
/* tslint:disable */
// #ts-check
require('regenerator-runtime');
const bip39 = require('bip39');
const { default: Transport } = require('#ledgerhq/hw-transport-node-hid');
const { default: AppBtc } = require('#ledgerhq/hw-app-btc');
const bitcoin = require('bitcoinjs-lib');
const mnemonics = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const NETWORK = bitcoin.networks.regtest;
/**
* #param {string} pk
* #returns {string}
*/
function compressPublicKey(pk) {
const { publicKey } = bitcoin.ECPair.fromPublicKey(Buffer.from(pk, 'hex'));
return publicKey.toString('hex');
}
/** #returns {Promise<any>} */
async function appBtc() {
const transport = await Transport.create();
const btc = new AppBtc(transport);
return btc;
}
const signTransaction = async() => {
const ledger = await appBtc();
const paths = ["0'/0/0"];
const [ path ] = paths;
const previousTx = "02000000000101869362410c61a69ab9390b2167d08219662196e869626e8b0350f1a8e4075efb0100000017160014ef3fdddccdb6b53e6dd1f5a97299a6ba2e1c11c3ffffffff0240420f000000000017a914f748afee815f78f97672be5a9840056d8ed77f4887df9de6050000000017a9142ff4aa6ffa987335c7bdba58ef4cbfecbe9e49938702473044022061a01bf0fbac4650a9b3d035b3d9282255a5c6040aa1d04fd9b6b52ed9f4d20a022064e8e2739ef532e6b2cb461321dd20f5a5d63cf34da3056c428475d42c9aff870121025fb5240daab4cee5fa097eef475f3f2e004f7be702c421b6607d8afea1affa9b00000000"
const utxo = bitcoin.Transaction.fromHex(previousTx);
const segwit = utxo.hasWitnesses();
const txIndex = 0;
// ecpairs things.
const seed = await bip39.mnemonicToSeed(mnemonics);
const node = bitcoin.bip32.fromSeed(seed, NETWORK);
const ecPrivate = node.derivePath(path);
const ecPublic = bitcoin.ECPair.fromPublicKey(ecPrivate.publicKey, { network: NETWORK });
const p2wpkh = bitcoin.payments.p2wpkh({ pubkey: ecPublic.publicKey, network: NETWORK });
const p2sh = bitcoin.payments.p2sh({ redeem: p2wpkh, network: NETWORK });
const redeemScript = p2sh.redeem.output;
const fromLedger = await ledger.getWalletPublicKey(path, { format: 'p2sh' });
const ledgerPublicKey = compressPublicKey(fromLedger.publicKey);
const bitcoinJsPublicKey = ecPublic.publicKey.toString('hex');
console.log({ ledgerPublicKey, bitcoinJsPublicKey, address: p2sh.address, segwit, fromLedger, redeemScript: redeemScript.toString('hex') });
var tx1 = ledger.splitTransaction(previousTx, true);
const psbt = new bitcoin.Psbt({ network: NETWORK });
psbt.addInput({
hash: utxo.getId(),
index: txIndex,
nonWitnessUtxo: Buffer.from(previousTx, 'hex'),
redeemScript,
});
psbt.addOutput({
address: 'mgWUuj1J1N882jmqFxtDepEC73Rr22E9GU',
value: 5000,
});
psbt.setMaximumFeeRate(1000 * 1000 * 1000); // ignore maxFeeRate we're testnet anyway.
psbt.setVersion(2);
/** #type {string} */
// #ts-ignore
const newTx = psbt.__CACHE.__TX.toHex();
console.log({ newTx });
const splitNewTx = await ledger.splitTransaction(newTx, true);
const outputScriptHex = await ledger.serializeTransactionOutputs(splitNewTx).toString("hex");
const expectedOutscriptHex = '0188130000000000001976a9140ae1441568d0d293764a347b191025c51556cecd88ac';
// stolen from: https://github.com/LedgerHQ/ledgerjs/blob/master/packages/hw-app-btc/tests/Btc.test.js
console.log({ outputScriptHex, expectedOutscriptHex, eq: expectedOutscriptHex === outputScriptHex });
const inputs = [ [tx1, 0, p2sh.redeem.output.toString('hex') /** ??? */] ];
const ledgerSignatures = await ledger.signP2SHTransaction(
inputs,
paths,
outputScriptHex,
0, // lockTime,
undefined, // sigHashType = SIGHASH_ALL ???
utxo.hasWitnesses(),
2, // version??,
);
const signer = {
network: NETWORK,
publicKey: ecPrivate.publicKey,
/** #param {Buffer} $hash */
sign: ($hash) => {
const expectedSignature = ecPrivate.sign($hash); // just for comparison.
const [ ledgerSignature0 ] = ledgerSignatures;
const decodedLedgerSignature = bitcoin.script.signature.decode(Buffer.from(ledgerSignature0, 'hex'));
console.log({
$hash: $hash.toString('hex'),
expectedSignature: expectedSignature.toString('hex'),
actualSignature: decodedLedgerSignature.signature.toString('hex'),
});
// return signature;
return decodedLedgerSignature.signature;
},
};
psbt.signInput(0, signer);
const validated = psbt.validateSignaturesOfInput(0);
psbt.finalizeAllInputs();
const hex = psbt.extractTransaction().toHex();
console.log({ validated, hex });
};
if (process.argv[1] === __filename) {
signTransaction().catch(console.error)
}
Ooof, finally got it working.
My mistake was I was trying to sign a p2sh-p2ms, By following a reference on how to sign a p2sh-p2wsh-p2ms.
And, also, that missing last 2 bit (01), which I think represent SIGHASH_ALL caused an error when I try to decode the signature.
this is my finalized working example.
// #ts-check
require('regenerator-runtime');
const bip39 = require('bip39');
const { default: Transport } = require('#ledgerhq/hw-transport-node-hid');
const { default: AppBtc } = require('#ledgerhq/hw-app-btc');
const serializer = require('#ledgerhq/hw-app-btc/lib/serializeTransaction');
const bitcoin = require('bitcoinjs-lib');
const mnemonics = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const NETWORK = bitcoin.networks.regtest;
const DEFAULT_LOCK_TIME = 0;
const SIGHASH_ALL = 1;
const PATHS = ["m/49'/1'/0'/0/0", "m/49'/1'/0'/0/1"];
async function appBtc() {
const transport = await Transport.create();
const btc = new AppBtc(transport);
return btc;
}
/**
* #param {string} pk
* #returns {string}
*/
function compressPublicKey(pk) {
const {
publicKey
} = bitcoin.ECPair.fromPublicKey(Buffer.from(pk, 'hex'));
return publicKey.toString('hex');
}
/**
* #param {AppBtc} ledger
* #param {bitcoin.Transaction} tx
*/
function splitTransaction(ledger, tx) {
return ledger.splitTransaction(tx.toHex(), tx.hasWitnesses());
}
const signTransaction = async() => {
const seed = await bip39.mnemonicToSeed(mnemonics);
const node = bitcoin.bip32.fromSeed(seed, NETWORK);
const signers = PATHS.map((p) => node.derivePath(p));
const publicKeys = signers.map((s) => s.publicKey);
const p2ms = bitcoin.payments.p2ms({ pubkeys: publicKeys, network: NETWORK, m: 1 });
const p2shP2ms = bitcoin.payments.p2sh({ redeem: p2ms, network: NETWORK });
const previousTx = '02000000000101588e8fc89afea9adb79de2650f0cdba762f7d0880c29a1f20e7b468f97da9f850100000017160014345766130a8f8e83aef8621122ca14fff88e6d51ffffffff0240420f000000000017a914a0546d83e5f8876045d7025a230d87bf69db893287df9de6050000000017a9142ff4aa6ffa987335c7bdba58ef4cbfecbe9e49938702483045022100c654271a891af98e46ca4d82ede8cccb0503a430e50745f959274294c98030750220331b455fed13ff4286f6db699eca06aa0c1c37c45c9f3aed3a77a3b0187ff4ac0121037ebcf3cf122678b9dc89b339017c5b76bee9fedd068c7401f4a8eb1d7e841c3a00000000';
const utxo = bitcoin.Transaction.fromHex(previousTx);
const txIndex = 0;
const destination = p2shP2ms;
const redeemScript = destination.redeem.output;
// const witnessScript = destination.redeem.redeem.output;
const ledgerRedeemScript = redeemScript;
// use witness script if the outgoing transaction was from a p2sh-p2wsh-p2ms instead of p2sh-p2ms
const fee = 1000;
/** #type {number} */
// #ts-ignore
const amount = utxo.outs[txIndex].value;
const withdrawAmount = amount - fee;
const psbt = new bitcoin.Psbt({ network: NETWORK });
const version = 1;
psbt.addInput({
hash: utxo.getId(),
index: txIndex,
nonWitnessUtxo: utxo.toBuffer(),
redeemScript,
});
psbt.addOutput({
address: '2MsK2NdiVEPCjBMFWbjFvQ39mxWPMopp5vp',
value: withdrawAmount
});
psbt.setVersion(version);
/** #type {bitcoin.Transaction} */
// #ts-ignore
const newTx = psbt.__CACHE.__TX;
const ledger = await appBtc();
const inLedgerTx = splitTransaction(ledger, utxo);
const outLedgerTx = splitTransaction(ledger, newTx);
const outputScriptHex = await serializer.serializeTransactionOutputs(outLedgerTx).toString('hex');
/** #param {string} path */
const signer = (path) => {
const ecPrivate = node.derivePath(path);
// actually only publicKey is needed, albeit ledger give an uncompressed one.
// const { publicKey: uncompressedPublicKey } = await ledger.getWalletPublicKey(path);
// const publicKey = compressPublicKey(publicKey);
return {
network: NETWORK,
publicKey: ecPrivate.publicKey,
/** #param {Buffer} $hash */
sign: async ($hash) => {
const ledgerTxSignatures = await ledger.signP2SHTransaction({
inputs: [[inLedgerTx, txIndex, ledgerRedeemScript.toString('hex')]],
associatedKeysets: [ path ],
outputScriptHex,
lockTime: DEFAULT_LOCK_TIME,
segwit: newTx.hasWitnesses(),
transactionVersion: version,
sigHashType: SIGHASH_ALL,
});
const [ ledgerSignature ] = ledgerTxSignatures;
const expectedSignature = ecPrivate.sign($hash);
const finalSignature = (() => {
if (newTx.hasWitnesses()) {
return Buffer.from(ledgerSignature, 'hex');
};
return Buffer.concat([
ledgerSignature,
Buffer.from('01', 'hex'), // SIGHASH_ALL
]);
})();
console.log({
expectedSignature: expectedSignature.toString('hex'),
finalSignature: finalSignature.toString('hex'),
});
const { signature } = bitcoin.script.signature.decode(finalSignature);
return signature;
},
};
}
await psbt.signInputAsync(0, signer(PATHS[0]));
const validate = await psbt.validateSignaturesOfAllInputs();
await psbt.finalizeAllInputs();
const hex = psbt.extractTransaction().toHex();
console.log({ validate, hex });
};
if (process.argv[1] === __filename) {
signTransaction().catch(console.error)
}
My guess is you have a space in the string passed to toByteArray function. This function doesn't trim input. Also doesn't check if input's length is even.
I'm following a tutorial which is using jest to test the javascript. The instructor created a static function called genesis() on a class called Block and it worked for him just fine, but when I tried to do it I got TypeError: block.genesis is not a function. If I remove the static keyword it recognises the function and the test passes.
Here is the class:
const { GENESIS_DATA } = require('./config');
class Block {
constructor({ timestamp, lastHash, hash, data }) {
this.timestamp = timestamp;
this.lastHash = lastHash;
this.hash = hash;
this.data = data;
}
static genesis() {
return new Block(GENESIS_DATA);
}
}
module.exports = Block;
And the test:
const Block = require('./block');
const { GENESIS_DATA } = require('./config');
describe('Block', () => {
const timestamp = 'a-date';
const lastHash = 'a-hash';
const hash = 'another-hash';
const data = ['blockchain', 'data'];
const block = new Block({ timestamp, lastHash, hash, data });
describe('genesis()', () => {
const genesisBlock = block.genesis();
it('returns a block instance', () => {
expect(genesisBlock instanceof Block).toBe(true);
});
it('returns the genesis data', () => {
expect(genesisBlock).toEqual(GENESIS_DATA);
});
});
});
The genesis method is part of the class, not the instance. You want to call Block.genesis() instead of block.genesis()
So I'm creating a singleton class and when I require it from my server.js file it works fine, but when I require it from another file it returns as undefined. I'll try to post relevant code but some will have to be cut out due to work.
server.js
const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
const http = require('http');
const app = express();
const config = require('config');
const FBConfigsListener = require('./server/amq_listeners/fb_configs.listener');
const FBConfigs = require('./server/models/FBConfigs');
//Api file for interacting with mongodb
const api = require('./server/routes/api.routes');
//Parsers
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
//Angular Dist output folder
app.use(express.static(path.join(__dirname, 'dist')));
//Api location
app.use('/api', api);
//Send all other requests to angular
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist/index.html'));
});
//set port
var port = config.get('webserver.port');
app.set('port', port);
const server = http.createServer(app);
server.listen(port, () => console.log(`Running on localhost:${port}`));
models/FBConfigs.js
var ConfigModel = require('./config');
var config = require('config');
var _ = require('lodash');
var FBConfigsListener = require('../amq_listeners/fb_configs.listener');
var AMQAdapter = require('../adapters/amq.adapter');
var uniqid = require('uniqid');
const connectionOptions = config.get('activemq.connectionOptions');
class FBConfigs {
constructor() {
console.log(config.get('environments'));
this.listener = FBConfigsListener;
this.configs = {};
this.unique_keys = ['id'];
this.update_topic = '/topic/fusebuilder.update.config.';
console.log(FBConfigsListener);
//AMQ Client
this.amq_client = AMQAdapter.getInstance(connectionOptions.host, connectionOptions.port);
}
add(key, config) {
if (!(key in this.configs)) {
this.configs[key] = new ConfigModel(this.unique_keys);
}
this.configs[key].add(config);
}
get(key) {
let configs_json = {};
if (key) {
configs_json[key] = JSON.parse(this.configs[key].toString());
} else {
for (let key in this.configs) {
configs_json[key] = JSON.parse(this.configs[key].toString());
}
}
return configs_json;
}
updateByID(key, id, input_config) {
let configs = this.configs[key].get();
for (let config of configs) {
if (input_config.id === config.id) {
this.update(key, _.merge(config, input_config));
}
}
}
//Send update to config topic
update(key, config) {
let topic = this.update_topic + key;
var update_object = {};
if (Array.isArray(config)) {
update_object[key] = [...config];
} else {
update_object[key] = [config];
}
console.log(`Sending ${key} update:${JSON.stringify(update_object)}`);
this.amq_client.sendMessage(topic, update_object);
}
copyTo(key, id, env) {
let selected_env = config.get('environments.' + env);
// let tmp_amq_client = new AMQAdapter(selected_env.host, selected_env.port);
let selected_config = this.configs[key].getByID(id);
console.log(this);
if (key === 'fuses') {
console.log('In FBConfig Copy to for fuses');
const get_fuse_topic = '/topic/fusebuilder.get_fuse';
const tran_id = uniqid();
const sendObj = { fuseName: id, tran_id };
this.amq_client.sendMessage(get_fuse_topic, sendObj);
var startTime = process.hrtime()[0];
var timeout = false;
while (!this.listener.get_copy_fuse_data(tran_id)) {
console.log('Waiting for config');
sleep(100);
if (process.hrtime()[0] - startTime > 3) {
console.log('Timed out');
timeout = true;
break;
}
}
console.log(JSON.stringify(FBConfigsListener.get_copy_fuse_data(tran_id)));
} else {
tmp_amq_client.sendMessage(this.update_topic, selected_config);
}
console.log(`Copy ${key} id:${id} to ${env}`);
}
}
module.exports = new FBConfigs();
amq_listener/fb_configs.listener.js
const config = require('config');
var AMQAdapter = require('../adapters/amq.adapter');
var FBConfigs = require('../models/FBConfigs');
**removed for work**
class FBConfigsListener {
constructor() {
this.instance;
this.copy_fuse_data = {};
//AMQ Client
this.amq_client = AMQAdapter.getInstance(connectionOptions.host, connectionOptions.port);
//Subscribe to configs
this.amq_client.subscribe(config_subscribe_topic, this.config_topic_callback.bind(this));
//Request Tables
this.amq_client.sendMessage(config_request_topic, { tables: config_tables });
//Subscribe to Copy Fuse topic
this.amq_client.subscribe(subscribe_fuse_copy_topic, this.copy_fuse_callback.bind(this));
}
config_topic_callback(err, message) {
let dest = this.amq_client.getDestination(message);
let key = this.get_key_from_topic(dest);
this.amq_client.readMessage(message, body => {
let configs = JSON.parse(body);
if (key in configs) {
for (let config of configs[key]) {
FBConfigs.add(key, config);
}
}
});
}
copy_fuse_callback(err, message) {
this.amq_client.readMessage(message, body => {
const config = JSON.parse(body);
this.copy_fuse_data[config.tran_id] = config;
});
}
//Get Key from the topic and convert using key map if needed
get_key_from_topic(topic) {
let key = topic.split('.')[topic.split('.').length - 1];
key = key in key_map ? key_map[key] : key;
return key;
}
get_copy_fuse_data(id) {
if (id in this.copy_fuse_data) {
return this.copy_fuse_data[id];
} else {
return false;
}
}
}
module.exports = new FBConfigsListener();
Error happens in FBConfigs. FBConfigsListener returns {} so all functions in there are undefined. Even if I do console.log(require('../amq_listeners/fb_configs.listener')) it prints {} But doing the same thing in server.js (with updated path) it prints the module.
Also tips on how to improve my coding style would be appreciated too.
Edit
So I found out that I have a circular dependency between these classes. How can this be fixed while allowing me to call one from the other.
I would suggest you to instantiate your dependencies firstly and store them in some object which you can pass then to your dependent classes. The structure can be
factories/services.js
/*
* Instantiates passed services and passes injector object to them
*/
module.exports = function createServices(injector, services) {
return Object.entries(services)
.reduce((aggregator, [name, serv]) => {
const name_ = camelCase(name);
aggregator.set(name_, new serv(injector));
return aggregator;
}, new Map());
};
lib/service.js
/**
* Base class for classes need any injections
*/
module.exports = class Service {
constructor(injector) {
this.injector = injector;
}
get dependencies() {
return this.injector.dependencies;
}
/*
* Background jobs can be ran here
*/
async startService() {}
/*
* Background jobs can be stopped here
*/
async stopService() {}
};
lib/injector.js
const Service = require('./service');
/*
* Contains all dependencies
*/
module.exports = class Injector {
constructor() {
this.services = new Map();
this._dependencies = {};
}
has(name) {
return this.services.has(name);
}
register(name, service) {
if (this.has(name)) {
throw new Error(`Service ${name} already exists`);
}
if (service instanceof Service === false) {
throw new Error('Argument #2 should be an instance of Service');
}
this.services.set(name, service);
this._dependencies[name] = service;
}
unregister(name) {
if (! this.has(name)) {
throw new Error(`Service ${name} not found`);
}
this.services.delete(name);
delete this._dependencies[name];
}
get dependencies() {
return { ...this._dependencies };
}
/*
* Starts all registered services
*/
async start() {
for (let service of this.services.values()) {
await service.startService();
}
}
/*
* Stops all registered services
*/
async stop() {
for (let service of this.services.values()) {
await service.stopService();
}
}
};
Then import, initialize and bind your services in the main file (don't forget to export just a class, not an object like you do it now).
server.js
const createServices = require('./factories/services.js');
const injector = require('./lib/injector');
const Injector = new injector();
const services = createServices(Injector, [require('./server/amq_listeners/fb_configs.listener'), require('./server/models/FBConfigs')]);
services.forEach((service, name) => {
Injector.register(name, service);
});
// Start services
Injector.start();
Inherit required classes to Service class and you will get an access to all dependencies there (don't forget to call super() from constructor). Like
models/FBConfigs.js
const Service = require('../lib/service');
class FBConfigs extends Service {
constructor(injector) {
super(injector);
const { FBConfigsListener } = this.dependencies;
...your code here
}
async startService() {
...run bg job or init some connection
}
async stopService() {
...stop bg job or close some connection
}
}
module.exports = FBConfigs;
Also you can pass some config object to createServices (I didn't include it here) with keys equal to service names and values containing config object and pass config to appropriate service.
It is caused by that circular dependency. You should avoid it or used very carefully.
In your case the fix is probably pretty simple, move the line var FBConfigs = require('../models/FBConfigs'); from listener at the end of the file as the last line (yes, even after the module.exports).
Edit: Actually it maybe is not enough as I checked the code more in detail. As you are not using Listener in FBConfig constructor, you can create method assignListener, remove this.listener from that constructor and call it later in server.js which will do the this.listener
Or the last solution, which is also "best practice". Do not export the instances. Export the classes only. Then in server.js create these instances after both are required.
Is it possible to test the code below with Jasmine testing tool or any other npm module like rewire or similar?
const AuthValidatorDumb = require('./src/AuthValidatorDumb');
const AuthValidator = require('./src/AuthValidator');
const config = require('../config');
let instance;
if (!instance) {
if (config.get('auth.enabled')) {
instance = AuthValidator;
} else {
instance = AuthValidatorDumb;
}
}
module.exports = instance;
I've got a variant for testing the code above.Suppose you have:
1) The code for index.js in the question above.
2) AuthValidator.js:
class AuthValidator {}
module.exports = AuthValidator;
3) AuthValidatorDumb.js:
class AuthValidatorDumb {}
module.exports = AuthValidatorDumb;
Here is test/index.spec.js:
const proxyquire = require('proxyquire');
const AuthValidator = require('../src/AuthValidator');
const AuthValidatorDumb = require('../src/AuthValidatorDumb');
describe('auth index', () => {
it('should return AuthValidator', () => {
const configMock = { get: () => 'sth' };
const Instance = proxyquire('../index', {
'../config': configMock,
});
expect(new Instance() instanceof AuthValidator).toBeTruthy();
});
it('should return AuthValidatorDumb', () => {
const configMock = { get: () => undefined };
const Instance = proxyquire('../index', {
'../config': configMock,
});
expect(new Instance() instanceof AuthValidatorDumb).toBeTruthy();
});
});