From c5733d1dfff5bd71ecfc344dc5c7021c5bbc7ead Mon Sep 17 00:00:00 2001 From: Austin Schaffer Date: Tue, 12 Nov 2019 16:41:31 -0500 Subject: [PATCH] [DEV-5312] Add interface for amqp publisher and implement a delta publisher --- package.json.in | 3 +- src/server/db/MongoServerDao.js | 482 ------------------------------ src/system/AmqpPublisher.ts | 42 +++ src/system/DeltaProcessor.ts | 14 +- src/system/DeltaPublisher.ts | 133 +++++++++ test/system/DeltaProcessorTest.ts | 21 +- test/system/DeltaPublisherTest.ts | 49 +++ 7 files changed, 249 insertions(+), 495 deletions(-) delete mode 100644 src/server/db/MongoServerDao.js create mode 100644 src/system/AmqpPublisher.ts create mode 100644 src/system/DeltaPublisher.ts create mode 100644 test/system/DeltaPublisherTest.ts diff --git a/package.json.in b/package.json.in index c0db61e..d81861d 100644 --- a/package.json.in +++ b/package.json.in @@ -38,7 +38,8 @@ "mocha": "5.2.0", "@types/mocha": "5.2.0", "sinon": ">=1.17.4", - "es6-promise": "~3" + "es6-promise": "~3", + "@types/amqplib": "0.5.13" }, "licenses": [ diff --git a/src/server/db/MongoServerDao.js b/src/server/db/MongoServerDao.js deleted file mode 100644 index 34878c4..0000000 --- a/src/server/db/MongoServerDao.js +++ /dev/null @@ -1,482 +0,0 @@ -"use strict"; -/** - * Mongo DB DAO for program server - * - * Copyright (C) 2010-2019 R-T Specialty, LLC. - * - * This file is part of the Liza Data Collection Framework. - * - * liza is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -var __extends = (this && this.__extends) || (function () { - var extendStatics = function (d, b) { - extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; - return extendStatics(d, b); - }; - return function (d, b) { - extendStatics(d, b); - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -var EventEmitter = require('events').EventEmitter; -/** - * Uses MongoDB as a data store - */ -var MongoServerDao = /** @class */ (function (_super) { - __extends(MongoServerDao, _super); - /** - * Initializes DAO - * - * @param {Mongo.Db} db mongo database connection - */ - function MongoServerDao(_db) { - var _this = _super.call(this) || this; - _this._db = _db; - /** Collection used to store quotes */ - _this.COLLECTION = 'quotes'; - /** Sequence (auto-increment) collection */ - _this.COLLECTION_SEQ = 'seq'; - /** Sequence key for quote ids */ - _this.SEQ_QUOTE_ID = 'quoteId'; - /** Sequence quoteId default */ - _this.SEQ_QUOTE_ID_DEFAULT = 200000; - /** Whether the DAO is initialized and ready to be used */ - _this._ready = false; - return _this; - } - /** - * Initializes error events and attempts to connect to the database - * - * connectError event will be emitted on failure. - * - * @param Function callback function to call when connection is complete - * (will not be called if connection fails) - * - * @return MongoServerDao self to allow for method chaining - */ - MongoServerDao.prototype.init = function (callback) { - var dao = this; - // map db error event (on connection error) to our connectError event - this._db.on('error', function (err) { - dao._ready = false; - dao._collection = null; - dao.emit('connectError', err); - }); - this.connect(callback); - return this; - }; - /** - * Attempts to connect to the database - * - * connectError event will be emitted on failure. - * - * @param Function callback function to call when connection is complete - * (will not be called if connection fails) - * - * @return MongoServerDao self to allow for method chaining - */ - MongoServerDao.prototype.connect = function (callback) { - var dao = this; - // attempt to connect to the database - this._db.open(function (err, db) { - // if there was an error, don't bother with anything else - if (err) { - // in some circumstances, it may just be telling us that we're - // already connected (even though the connection may have been - // broken) - if (err.errno !== undefined) { - dao.emit('connectError', err); - return; - } - } - var ready_count = 0; - var check_ready = function () { - if (++ready_count < 2) { - return; - } - // we're ready to roll! - dao._ready = true; - dao.emit('ready'); - // connection was successful; call the callback - if (callback instanceof Function) { - callback.call(dao); - } - }; - // quotes collection - db.collection(dao.COLLECTION, function (_err, collection) { - // for some reason this gets called more than once - if (collection == null) { - return; - } - // initialize indexes - collection.createIndex([['id', 1]], true, function (_err, _index) { - // mark the DAO as ready to be used - dao._collection = collection; - check_ready(); - }); - }); - // seq collection - db.collection(dao.COLLECTION_SEQ, function (err, collection) { - if (err) { - dao.emit('seqError', err); - return; - } - if (collection == null) { - return; - } - dao._seqCollection = collection; - // has the sequence we'll be referencing been initialized? - collection.find({ _id: dao.SEQ_QUOTE_ID }, { limit: 1 }, function (err, cursor) { - if (err) { - dao._initQuoteIdSeq(check_ready); - return; - } - cursor.toArray(function (_err, data) { - if (data.length == 0) { - dao._initQuoteIdSeq(check_ready); - return; - } - check_ready(); - }); - }); - }); - }); - return this; - }; - MongoServerDao.prototype._initQuoteIdSeq = function (callback) { - var dao = this; - this._seqCollection.insert({ - _id: this.SEQ_QUOTE_ID, - val: this.SEQ_QUOTE_ID_DEFAULT, - }, function (err, _docs) { - if (err) { - dao.emit('seqError', err); - return; - } - dao.emit('seqInit', dao.SEQ_QUOTE_ID); - callback.call(dao); - }); - }; - /** - * Saves a quote to the database - * - * A full save will include all metadata. This should not cause any - * problems with race conditions for pending Data API calls on meta - * fields because those results write to individual indexes and do not - * rely on existing data. - * - * @param Quote quote the quote to save - * @param Function success_callback function to call on success - * @param Function failure_callback function to call if save fails - * @param Object save_data quote data to save (optional) - * @param Object push_data quote data to push (optional) - */ - MongoServerDao.prototype.saveQuote = function (quote, success_callback, failure_callback, save_data, push_data) { - var dao = this; - var meta = {}; - // if we're not ready, then we can't save the quote! - if (this._ready === false) { - this.emit('saveQuoteError', { message: 'Database server not ready' }, Error('Database not ready'), quote); - failure_callback.call(this, quote); - return dao; - } - if (save_data === undefined) { - save_data = { - data: quote.getBucket().getData(), - }; - // full save will include all metadata - meta = quote.getMetabucket().getData(); - } - var id = quote.getId(); - // some data should always be saved because the quote will be created if - // it does not yet exist - save_data.id = id; - save_data.pver = quote.getProgramVersion(); - save_data.importDirty = 1; - save_data.lastPremDate = quote.getLastPremiumDate(); - save_data.initialRatedDate = quote.getRatedDate(); - save_data.explicitLock = quote.getExplicitLockReason(); - save_data.explicitLockStepId = quote.getExplicitLockStep(); - save_data.importedInd = +quote.isImported(); - save_data.boundInd = +quote.isBound(); - save_data.lastUpdate = Math.round((new Date()).getTime() / 1000); - // meta will eventually take over for much of the above data - meta.liza_timestamp_initial_rated = [quote.getRatedDate()]; - // save the stack so we can track this call via the oplog - save_data._stack = (new Error()).stack; - // avoid wiping out other metadata (since this may not be a full set) - Object.keys(meta).forEach(function (key) { return save_data['meta.' + key] = meta[key]; }); - // do not push empty objects - var document = (!push_data || !Object.keys(push_data).length) - ? { '$set': save_data } - : { '$set': save_data, '$push': push_data }; - // update the quote data if it already exists (same id), otherwise - // insert it - this._collection.update({ id: id }, document, - // create record if it does not yet exist - { upsert: true }, - // on complete - function (err, _docs) { - // if an error occurred, then we cannot continue - if (err) { - dao.emit('saveQuoteError', err, quote); - // let the caller handle the error - if (failure_callback instanceof Function) { - failure_callback.call(dao, quote); - } - return; - } - // successful - if (success_callback instanceof Function) { - success_callback.call(dao, quote); - } - }); - return this; - }; - /** - * Merges quote data with the existing (rather than overwriting) - * - * @param {Quote} quote quote to save - * @param {Object} data quote data - * @param {Function} scallback successful callback - * @param {Function} fcallback failure callback - */ - MongoServerDao.prototype.mergeData = function (quote, data, scallback, fcallback) { - // we do not want to alter the original data; use it as a prototype - var update = data; - // save the stack so we can track this call via the oplog - var _self = this; - this._collection.update({ id: quote.getId() }, { '$set': update }, {}, function (err, _docs) { - if (err) { - _self.emit('saveQuoteError', err, quote); - if (typeof fcallback === 'function') { - fcallback(quote); - } - return; - } - if (typeof scallback === 'function') { - scallback(quote); - } - }); - return this; - }; - /** - * Merges bucket data with the existing bucket (rather than overwriting the - * entire bucket) - * - * @param {Quote} quote quote to save - * @param {Object} data bucket data - * @param {Function} scallback successful callback - * @param {Function} fcallback failure callback - * - * @return {MongoServerDao} self - */ - MongoServerDao.prototype.mergeBucket = function (quote, data, success, failure) { - var update = {}; - for (var field in data) { - if (!field) { - continue; - } - update['data.' + field] = data[field]; - } - return this.mergeData(quote, update, success, failure); - }; - /** - * Saves the quote state to the database - * - * The quote state includes the current step, the top visited step and the - * explicit lock message. - * - * @param Quote quote the quote to save - * @param Function success_callback function to call on success - * @param Function failure_callback function to call if save fails - * - * @return MongoServerDao self - */ - MongoServerDao.prototype.saveQuoteState = function (quote, success_callback, failure_callback) { - var update = { - currentStepId: quote.getCurrentStepId(), - topVisitedStepId: quote.getTopVisitedStepId(), - topSavedStepId: quote.getTopSavedStepId(), - }; - return this.mergeData(quote, update, success_callback, failure_callback); - }; - MongoServerDao.prototype.saveQuoteClasses = function (quote, classes, success, failure) { - return this.mergeData(quote, { classData: classes }, success, failure); - }; - /** - * Save document metadata (meta field on document) - * - * Only the provided indexes will be modified (that is---data will be - * merged with what is already in the database). - * - * @param {Quote} quote destination quote - * @param {Object} new_meta bucket-formatted data to write - * @param {Function} success callback on success - * @param {Function} failure callback on error - * - * @return {undefined} - */ - MongoServerDao.prototype.saveQuoteMeta = function (quote, new_meta, success, failure) { - var update = {}; - for (var key in new_meta) { - var meta = new_meta[key]; - for (var i in meta) { - update['meta.' + key + '.' + i] = new_meta[key][i]; - } - } - this.mergeData(quote, update, success, failure); - }; - /** - * Saves the quote lock state to the database - * - * @param Quote quote the quote to save - * @param Function success_callback function to call on success - * @param Function failure_callback function to call if save fails - * - * @return MongoServerDao self - */ - MongoServerDao.prototype.saveQuoteLockState = function (quote, success_callback, failure_callback) { - // lock state is saved by default - return this.saveQuote(quote, success_callback, failure_callback, {}); - }; - /** - * Pulls quote data from the database - * - * @param Integer quote_id id of quote - * @param Function( data ) callback function to call when data is available - * - * @return MongoServerDao self to allow for method chaining - */ - MongoServerDao.prototype.pullQuote = function (quote_id, callback) { - var dao = this; - // XXX: TODO: Do not read whole of record into memory; filter out - // revisions! - this._collection.find({ id: quote_id }, { limit: 1 }, function (_err, cursor) { - cursor.toArray(function (_err, data) { - // was the quote found? - if (data.length == 0) { - callback.call(dao, null); - return; - } - // return the quote data - callback.call(dao, data[0]); - }); - }); - return this; - }; - MongoServerDao.prototype.getMinQuoteId = function (callback) { - // just in case it's asynchronous later on - callback.call(this, this.SEQ_QUOTE_ID_DEFAULT); - return this; - }; - MongoServerDao.prototype.getMaxQuoteId = function (callback) { - var dao = this; - this._seqCollection.find({ _id: this.SEQ_QUOTE_ID }, { limit: 1 }, function (_err, cursor) { - cursor.toArray(function (_err, data) { - if (data.length == 0) { - callback.call(dao, 0); - return; - } - // return the max quote id - callback.call(dao, data[0].val); - }); - }); - }; - MongoServerDao.prototype.getNextQuoteId = function (callback) { - var dao = this; - this._seqCollection.findAndModify({ _id: this.SEQ_QUOTE_ID }, [['val', 'descending']], { $inc: { val: 1 } }, { 'new': true }, function (err, doc) { - if (err) { - dao.emit('seqError', err); - callback.call(dao, 0); - return; - } - // return the new id - callback.call(dao, doc.val); - }); - return this; - }; - /** - * Create a new revision with the provided quote data - * - * The revision will contain the whole the quote. If space is a concern, we - * can (in the future) calculate a delta instead (Mike recommends the Git - * model of storing the deltas in previous revisions and the whole of the - * bucket in the most recently created revision). - */ - MongoServerDao.prototype.createRevision = function (quote, callback) { - var _self = this, qid = quote.getId(), data = quote.getBucket().getData(); - this._collection.update({ id: qid }, { '$push': { revisions: { data: data } } }, - // create record if it does not yet exist - { upsert: true }, - // on complete - function (err) { - if (err) { - _self.emit('mkrevError', err); - } - callback(err); - return; - }); - }; - MongoServerDao.prototype.getRevision = function (quote, revid, callback) { - revid = +revid; - // XXX: TODO: Filter out all but the revision we want - this._collection.find({ id: quote.getId() }, { limit: 1 }, function (_err, cursor) { - cursor.toArray(function (_err, data) { - // was the quote found? - if ((data.length === 0) - || (data[0].revisions.length < (revid + 1))) { - callback(null); - return; - } - // return the quote data - callback(data[0].revisions[revid]); - }); - }); - }; - MongoServerDao.prototype.setWorksheets = function (qid, data, callback) { - this._collection.update({ id: qid }, { '$set': { worksheets: { data: data } } }, - // create record if it does not yet exist - { upsert: true }, - // on complete - function (err) { - callback(err); - return; - }); - }; - MongoServerDao.prototype.getWorksheet = function (qid, supplier, index, callback) { - this._collection.find({ id: qid }, { limit: 1 }, function (_err, cursor) { - cursor.toArray(function (_err, data) { - // was the quote found? - if ((data.length === 0) - || (!data[0].worksheets) - || (!data[0].worksheets.data) - || (!data[0].worksheets.data[supplier])) { - callback(null); - return; - } - // return the quote data - callback(data[0].worksheets.data[supplier][index]); - }); - }); - }; - return MongoServerDao; -}(EventEmitter)); -exports.MongoServerDao = MongoServerDao; -; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiTW9uZ29TZXJ2ZXJEYW8uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJNb25nb1NlcnZlckRhby50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FtQkc7Ozs7Ozs7Ozs7Ozs7OztBQVNILElBQU0sWUFBWSxHQUFHLE9BQU8sQ0FBRSxRQUFRLENBQUUsQ0FBQyxZQUFZLENBQUM7QUFJdEQ7O0dBRUc7QUFDSDtJQUFvQyxrQ0FBWTtJQXlCNUM7Ozs7T0FJRztJQUNILHdCQUNxQixHQUFRO1FBRDdCLFlBSUksaUJBQU8sU0FDVjtRQUpvQixTQUFHLEdBQUgsR0FBRyxDQUFLO1FBN0I3QixzQ0FBc0M7UUFDN0IsZ0JBQVUsR0FBVyxRQUFRLENBQUM7UUFFdkMsMkNBQTJDO1FBQ2xDLG9CQUFjLEdBQVcsS0FBSyxDQUFDO1FBRXhDLGlDQUFpQztRQUN4QixrQkFBWSxHQUFXLFNBQVMsQ0FBQztRQUUxQywrQkFBK0I7UUFDdEIsMEJBQW9CLEdBQVcsTUFBTSxDQUFDO1FBRy9DLDBEQUEwRDtRQUNsRCxZQUFNLEdBQVksS0FBSyxDQUFDOztJQW1CaEMsQ0FBQztJQUdEOzs7Ozs7Ozs7T0FTRztJQUNILDZCQUFJLEdBQUosVUFBTSxRQUFrQjtRQUVwQixJQUFJLEdBQUcsR0FBRyxJQUFJLENBQUM7UUFFZixxRUFBcUU7UUFDckUsSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUUsT0FBTyxFQUFFLFVBQVUsR0FBUTtZQUVwQyxHQUFHLENBQUMsTUFBTSxHQUFRLEtBQUssQ0FBQztZQUN4QixHQUFHLENBQUMsV0FBVyxHQUFHLElBQUksQ0FBQztZQUV2QixHQUFHLENBQUMsSUFBSSxDQUFFLGNBQWMsRUFBRSxHQUFHLENBQUUsQ0FBQztRQUNwQyxDQUFDLENBQUMsQ0FBQztRQUVILElBQUksQ0FBQyxPQUFPLENBQUUsUUFBUSxDQUFFLENBQUM7UUFDekIsT0FBTyxJQUFJLENBQUM7SUFDaEIsQ0FBQztJQUdEOzs7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 \ No newline at end of file diff --git a/src/system/AmqpPublisher.ts b/src/system/AmqpPublisher.ts new file mode 100644 index 0000000..bfd6dc3 --- /dev/null +++ b/src/system/AmqpPublisher.ts @@ -0,0 +1,42 @@ +/** + * Amqp Publisher + * + * Copyright (C) 2010-2019 R-T Specialty, LLC. + * + * This file is part of liza. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Publish Amqp message to a queue + */ + +import { DeltaResult } from "../bucket/delta"; +import { Options } from 'amqplib'; + + +export interface AmqpConfig extends Options.Connect { + /** The name of a queue or exchange to publish to */ + exchange: string; +} + + +export interface AmqpPublisher +{ + /** + * Publish quote message to exchange post-rating + * + * @param delta - The delta to publish + */ + publish( delta: DeltaResult ): void; +} diff --git a/src/system/DeltaProcessor.ts b/src/system/DeltaProcessor.ts index 6103f20..67e372d 100644 --- a/src/system/DeltaProcessor.ts +++ b/src/system/DeltaProcessor.ts @@ -23,6 +23,7 @@ import { DeltaDao } from "../system/db/DeltaDao"; import { MongoDeltaType } from "../system/db/MongoDeltaDao"; import { DeltaResult } from "../bucket/delta"; import { DocumentId } from "../document/Document"; +import { AmqpPublisher } from "./AmqpPublisher"; /** @@ -36,12 +37,6 @@ export class DeltaProcessor /** The data delta type */ readonly DELTA_DATA: MongoDeltaType = 'data'; - /** A mapping of which delta type translated to which avro event */ - readonly DELTA_MAP: Record = { - DELTA_RATEDATA: 'rate', - DELTA_DATA: 'update', - }; - /** * Initialize processor @@ -49,7 +44,8 @@ export class DeltaProcessor * @param _collection Mongo collection */ constructor( - private readonly _dao: DeltaDao, + private readonly _dao: DeltaDao, + private readonly _publisher: AmqpPublisher, ) {} @@ -68,9 +64,7 @@ export class DeltaProcessor deltas.forEach( delta => { - // TODO: publish delta - // publisher.publish( delta, self.DELTA_MAP[ delta.type ] ) - console.log( delta, self.DELTA_MAP[ delta.type ] ); + self._publisher.publish( delta ); }); diff --git a/src/system/DeltaPublisher.ts b/src/system/DeltaPublisher.ts new file mode 100644 index 0000000..2606c56 --- /dev/null +++ b/src/system/DeltaPublisher.ts @@ -0,0 +1,133 @@ +/** + * Delta Publisher + * + * Copyright (C) 2010-2019 R-T Specialty, LLC. + * + * This file is part of liza. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Publish delta message to a queue + */ + +import { AmqpPublisher } from "./AmqpPublisher"; +import { DeltaResult } from "../bucket/delta"; +import { + connect as amqpConnect, + Options, + Channel +} from 'amqplib'; + + +export interface AmqpConfig extends Options.Connect { + /** The name of a queue or exchange to publish to */ + exchange: string; +} + + +export class DeltaPublisher implements AmqpPublisher +{ + /** A mapping of which delta type translated to which avro event */ + readonly DELTA_MAP: Record = { + data: 'rate', + ratedata: 'update', + }; + + + /** + * Initialize trait + * + * @param {Object} conf AMQP configuration + * @param {DebugLog} logger logger instance + */ + constructor( + private readonly _conf: AmqpConfig, + private readonly _logger: any + ) {} + + + /** + * Publish quote message to exchange post-rating + * + * @param delta - The delta to publish + */ + publish( delta: DeltaResult ): void + { + // check both as we transition from one to the other + const exchange = this._conf.exchange; + + amqpConnect( this._conf ) + .then( conn => + { + setTimeout( () => conn.close(), 10000 ); + return conn.createChannel(); + } ) + .then( ch => { + ch.assertExchange( exchange, 'fanout', { durable: true } ); + + return this._sendMessage( ch, exchange, delta ); + } ) + .then( () => this._logger.log( + this._logger.PRIORITY_INFO, + "Published " + delta.type + " delta with timestamp '" + + delta.timestamp + "' to quote-update exchange '"+ + exchange + "'" + ) ) + .catch( e => this._logger.log( + this._logger.PRIORITY_ERROR, + "Error publishing " + delta.type + " delta with timestamp '" + + delta.timestamp + "' to quote-update exchange '"+ + exchange + "'" + ": " + e + ) ); + } + + + /** + * Send message to exchange + * + * @param channel - AMQP channel + * @param exchange - exchange name + * @param delta - The delta to publish + * + * @return whether publish was successful + */ + _sendMessage( + channel: Channel, + exchange: string, + delta: DeltaResult, + ): boolean + { + const headers = { + version: 1, + created: Date.now(), + }; + + const event_id = this.DELTA_MAP[ delta.type ]; + + const data = new Buffer( JSON.stringify( { + delta: delta, + event: event_id, + } ) ); + + // we don't use a routing key; fanout exchange + const routing_key = ''; + + return channel.publish( + exchange, + routing_key, + data, + { headers: headers }, + ); + } +} diff --git a/test/system/DeltaProcessorTest.ts b/test/system/DeltaProcessorTest.ts index 26923ae..3e70282 100644 --- a/test/system/DeltaProcessorTest.ts +++ b/test/system/DeltaProcessorTest.ts @@ -20,6 +20,7 @@ */ import { DeltaProcessor as Sut } from '../../src/system/DeltaProcessor'; +import { AmqpPublisher } from '../../src/system/AmqpPublisher'; import { DeltaDao } from '../../src/system/db/DeltaDao'; import { MongoDeltaType } from '../../src/system/db/MongoDeltaDao'; @@ -163,7 +164,11 @@ describe( 'system.DeltaProcessor', () => }, ] ).forEach( ( { given, expected, label } ) => it( label, () => { - const sut = new Sut( createMockDeltaDao() ); + const sut = new Sut( + createMockDeltaDao(), + createMockDeltaPublisher() + ); + const actual = sut.getTimestampSortedDeltas( given ); expect( actual ).to.deep.equal( expected ); @@ -280,7 +285,11 @@ describe( 'system.DeltaProcessor', () => }, ] ).forEach( ( { type, given, expected, label } ) => it( label, () => { - const sut = new Sut( createMockDeltaDao() ); + const sut = new Sut( + createMockDeltaDao(), + createMockDeltaPublisher() + ); + const actual = sut.getDeltas( given, type ); expect( actual ).to.deep.equal( expected ); @@ -297,3 +306,11 @@ function createMockDeltaDao(): DeltaDao markDocumentAsProcessed() { return this }, }; } + + +function createMockDeltaPublisher(): AmqpPublisher +{ + return { + publish() {}, + }; +} diff --git a/test/system/DeltaPublisherTest.ts b/test/system/DeltaPublisherTest.ts new file mode 100644 index 0000000..9f72cd1 --- /dev/null +++ b/test/system/DeltaPublisherTest.ts @@ -0,0 +1,49 @@ +/** + * Delta publisher test + * + * Copyright (C) 2010-2019 R-T Specialty, LLC. + * + * This file is part of the Liza Data Collection Framework. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { + DeltaPublisher as Sut, + AmqpConfig +} from "../../src/system/DeltaPublisher"; + +import { expect, use as chai_use } from 'chai'; +chai_use( require( 'chai-as-promised' ) ); + + +describe( 'server.DeltaPublisher', () => +{ + describe( '#publish', () => + { + it( 'sends a message', () => + { + const conf = createMockConf(); + + console.log( new Sut( conf, {} ) ); + expect( true ).to.be.true + }); + }); +} ); + + +function createMockConf(): AmqpConfig +{ + return {}; +}