From 5b410005cd1bcaaeafaf145c186c3b2c1be81993 Mon Sep 17 00:00:00 2001 From: Jeffrey Fisher Date: Tue, 24 Apr 2018 10:45:26 -0400 Subject: [PATCH 1/9] RatingService: ensure #postProcessRaterData is called before save --- src/server/service/RatingService.js | 4 +- src/test/server/service/RatingServiceStub.js | 86 ++++++++++++++++++++ test/server/service/RatingServiceTest.js | 55 +++++++++++++ 3 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 src/test/server/service/RatingServiceStub.js create mode 100644 test/server/service/RatingServiceTest.js diff --git a/src/server/service/RatingService.js b/src/server/service/RatingService.js index 4df89fb..65a6215 100644 --- a/src/server/service/RatingService.js +++ b/src/server/service/RatingService.js @@ -154,7 +154,7 @@ module.exports = Class( 'RatingService', { actions = actions || []; - _self._postProcessRaterData( + _self.postProcessRaterData( rate_data, actions, program, quote ); @@ -251,7 +251,7 @@ module.exports = Class( 'RatingService', * * @return {undefined} */ - _postProcessRaterData: function( data, actions, program, quote ) + 'virtual protected postProcessRaterData': function( data, actions, program, quote ) { var meta = data._cmpdata || {}; diff --git a/src/test/server/service/RatingServiceStub.js b/src/test/server/service/RatingServiceStub.js new file mode 100644 index 0000000..4b0b58c --- /dev/null +++ b/src/test/server/service/RatingServiceStub.js @@ -0,0 +1,86 @@ +/** + * Tests RatingService + * + * Copyright (C) 2018 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 . + */ + +'use strict' + + +exports.getStubs = function() +{ + const program_id = 'foo'; + const program = { + getId: () => program_id, + }; + + // rate reply + const stub_rate_data = {}; + + const rater = { + rate: ( quote, session, indv, callback ) => callback( stub_rate_data ), + }; + + const raters = { + byId: () => rater, + }; + + const logger = { + log: () => {}, + }; + + const server = { + sendResponse: () => {}, + sendError: () => {}, + }; + + const dao = { + mergeBucket: () => {}, + saveQuoteClasses: () => {}, + setWorksheets: () => {}, + }; + + const session = { + isInternal: () => false, + }; + + const request = { + getSession: () => session, + }; + const response = {}; + + const quote = { + getProgramId: () => program_id, + getProgram: () => program, + getId: () => 0, + }; + + return { + program: program, + stub_rate_data: stub_rate_data, + rater: rater, + raters: raters, + logger: logger, + server: server, + dao: dao, + session: session, + request: request, + response: response, + quote: quote, + }; +}; diff --git a/test/server/service/RatingServiceTest.js b/test/server/service/RatingServiceTest.js new file mode 100644 index 0000000..ba2c13e --- /dev/null +++ b/test/server/service/RatingServiceTest.js @@ -0,0 +1,55 @@ +/** + * Tests RatingService + * + * Copyright (C) 2018 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 . + */ + +'use strict' + +const { expect } = require( 'chai' ); +const Sut = require( '../../../' ).server.service.RatingService; +const RatingServiceStub = require( '../../../' ).test.server.service.RatingServiceStub; + +describe( 'RatingService', () => +{ + describe( "protected API", () => + { + it( "calls #postProcessRaterData after rating before save", done => + { + let processed = false; + + const { logger, server, raters, dao, request, response, quote } = RatingServiceStub.getStubs(); + + dao.mergeBucket = () => + { + expect( processed ).to.equal( true ); + done(); + }; + + const sut = Sut.extend( + { + 'override postProcessRaterData'( data, actions, program, quote ) + { + processed = true; + } + } )( logger, dao, server, raters ); + + sut.request( request, response, quote, 'something', () => {} ); + } ); + } ); +} ); From 7d0dc971628a239cc334ae4abe93bb6c775c5500 Mon Sep 17 00:00:00 2001 From: Jeffrey Fisher Date: Tue, 24 Apr 2018 15:30:19 -0400 Subject: [PATCH 2/9] Add HttpDataApiUrlData --- src/dapi/http/HttpDataApi.js | 29 +++++- src/dapi/http/HttpDataApiUrlData.js | 127 +++++++++++++++++++++++ src/dapi/http/NodeHttpImpl.js | 2 +- src/dapi/http/XhrHttpImpl.js | 2 +- test/dapi/http/HttpDataApiUrlDataTest.js | 123 ++++++++++++++++++++++ 5 files changed, 276 insertions(+), 7 deletions(-) create mode 100644 src/dapi/http/HttpDataApiUrlData.js create mode 100644 test/dapi/http/HttpDataApiUrlDataTest.js diff --git a/src/dapi/http/HttpDataApi.js b/src/dapi/http/HttpDataApi.js index 0db81b1..cbb23e4 100644 --- a/src/dapi/http/HttpDataApi.js +++ b/src/dapi/http/HttpDataApi.js @@ -139,14 +139,33 @@ module.exports = Class( 'HttpDataApi' ) this._validateDataType( data ); - this._impl.requestData( - this._url, - this._method, + this.requestData( this._url, this._method, data, callback ); + + return this; + }, + + + /** + * Request data from underlying HttpImpl + * + * Subtypes may override this method to alter any aspect of the request + * before sending. + * + * @param {string} url destination URL + * @param {string} method RFC-2616-compliant HTTP method + * @param {Object|string} data request params + * @param {function(?Error, ?string)} callback server response callback + * + * @return {HttpDataApi} self + */ + 'virtual protected requestData'( url, method, data, callback ) + { + return this._impl.requestData( + url, + method, this.encodeData( data ), callback ); - - return this; }, diff --git a/src/dapi/http/HttpDataApiUrlData.js b/src/dapi/http/HttpDataApiUrlData.js new file mode 100644 index 0000000..52edfb9 --- /dev/null +++ b/src/dapi/http/HttpDataApiUrlData.js @@ -0,0 +1,127 @@ +/** + * Provide data as part of URL + * + * Copyright (C) 2018 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 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 . + */ + +'use strict'; + +const { Trait } = require( 'easejs' ); +const HttpDataApi = require( './HttpDataApi' ); + + +/** + * Place fields from given data in the URL + * + * All remaining fields are passed to the underlying supertype. + */ +module.exports = Trait( 'HttpDataApiUrlData' ) + .extend( HttpDataApi, +{ + /** + * Fields to take from data and place in URL + * @type {string} + */ + 'private _fields': [], + + + /** + * Initialize with URL field list + * + * @param {Array} fields list of fields to include in URL + */ + __mixin( fields ) + { + this._fields = fields; + }, + + + /** + * Concatenate chosen fields with URL + * + * The previously specified fields will have their values delimited by '/' + * and will be concatenated with the URL. All used fields in DATA will be + * removed before being passed to the supertype. METHOD and CALLBACK are + * proxied as-is. + * + * @param {string} url destination URL + * @param {string} method RFC-2616-compliant HTTP method + * @param {Object|string} data request params + * @param {function(Error, Object)} callback server response callback + * + * @return {HttpImpl} self + */ + 'override public requestData'( url, method, data, callback ) + { + const [ values, filtered_data ] = this._getFieldValues( data ); + + const params = values.map( ( [ , value ] ) => value ); + const missing = values.filter( ( [ , value ] ) => value === undefined ); + + if ( missing.length > 0 ) + { + callback( + Error( + "Missing URL parameters: " + + missing.map( ( [ field ] ) => field ).join( ", " ) + ), + null + ); + + return this; + } + + const built_url = ( params.length > 0 ) + ? url + '/' + params.join( '/' ) + : url; + + return this.__super( built_url, method, filtered_data, callback ); + }, + + + /** + * Associate fields with their respective values from DATA + * + * The returned values are of the form `[ [ field, value ], ... ]`. + * The returned data object is a copy of the original and is stripped + * of the respective fields. + * + * @param {Object} data source data + * + * @return {Array} values and copy of data stripped of those fields + */ + 'private _getFieldValues'( data ) + { + const fieldset = new Set( this._fields ); + const values = this._fields.map( field => [ field, data[ field ] ] ); + + // copy of data with fields stripped + const new_data = Object.keys( data ).reduce( ( dest, key ) => + { + if ( fieldset.has( key ) ) + { + return dest; + } + + dest[ key ] = data[ key ]; + return dest; + }, {} ); + + return [ values, new_data ]; + }, +} ); diff --git a/src/dapi/http/NodeHttpImpl.js b/src/dapi/http/NodeHttpImpl.js index 35e687d..9f402f0 100644 --- a/src/dapi/http/NodeHttpImpl.js +++ b/src/dapi/http/NodeHttpImpl.js @@ -87,7 +87,7 @@ module.exports = Class( 'NodeHttpImpl' ) * * @return {HttpImpl} self */ - 'public requestData'( url, method, data, callback ) + 'virtual public requestData'( url, method, data, callback ) { const options = this._parseUrl( url ); const protocol = options.protocol.replace( /:$/, '' ); diff --git a/src/dapi/http/XhrHttpImpl.js b/src/dapi/http/XhrHttpImpl.js index 28eccac..b2e4d63 100644 --- a/src/dapi/http/XhrHttpImpl.js +++ b/src/dapi/http/XhrHttpImpl.js @@ -71,7 +71,7 @@ module.exports = Class( 'XhrHttpImpl' ) * * @return {HttpImpl} self */ - 'public requestData': function( url, method, data, callback ) + 'virtual public requestData': function( url, method, data, callback ) { if ( typeof data !== 'string' ) { diff --git a/test/dapi/http/HttpDataApiUrlDataTest.js b/test/dapi/http/HttpDataApiUrlDataTest.js new file mode 100644 index 0000000..31673eb --- /dev/null +++ b/test/dapi/http/HttpDataApiUrlDataTest.js @@ -0,0 +1,123 @@ +/** + * Tests HttpDataApiUrlData + * + * Copyright (C) 2018 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 . + */ + +'use strict' + +const { Class } = require( 'easejs' ); +const { expect } = require( 'chai' ); + +const { + dapi: { + http: { + HttpDataApi, + HttpImpl, + HttpDataApiUrlData: Sut, + }, + }, +} = require( '../../../' ); + + +describe( 'HttpDataApiUrlData', () => +{ + [ + { + fields: [], + data: { + foo: "foodata", + bar: "bardata", + }, + base_url: "base", + after_data: "foo=foodata&bar=bardata", + expected: "base", + }, + { + fields: [ 'foo', 'bar' ], + data: { + foo: "foodata", + bar: "bardata", + baz: "shouldnotuse", + }, + base_url: "base2", + after_data: "baz=shouldnotuse", + expected: "base2/foodata/bardata", + }, + ].forEach( ( { fields, data, base_url, after_data, expected }, i ) => + it( `can include portion of data in url (#${i})`, done => + { + const expected_method = 'PUT'; + + const impl = Class.implement( HttpImpl ).extend( + { + 'virtual requestData'( url, method, given_data, callback ) + { + expect( url ).to.equal( expected ); + expect( method ).to.equal( expected_method ); + expect( given_data ).to.deep.equal( after_data ); + + // should not have mutated the original object + // (they shouldn't be the same object) + expect( data ).to.not.equal( given_data ); + + // should be done + callback(); + }, + + setOptions: () => {}, + } )(); + + const sut = HttpDataApi.use( Sut( fields ) )( + base_url, expected_method, impl + ); + + const result = sut.request( data, done ); + } ) + ); + + + it( "throws error if param is missing from data", done => + { + const impl = Class.implement( HttpImpl ).extend( + { + 'virtual requestData': ( url, method, data, callback ) => {}, + setOptions: () => {}, + } )(); + + const sut = HttpDataApi.use( Sut( [ 'foo' ] ) )( + '', 'PUT', impl + ); + + // missing `foo` in data + sut.request( { unused: "missing foo" }, ( err, data ) => + { + expect( err ).to.be.instanceof( Error ); + expect( err.message ).to.match( /\bfoo\b/ ); + + expect( data ).to.equal( null ); + + done(); + } ); + } ); +} ); + + + + + From d8338b50e0fb28a7866f1411dab0b028860ce950 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Wed, 25 Apr 2018 15:35:16 -0400 Subject: [PATCH 3/9] RatingServiceSubmitNotify: Add trait * src/server/service/RatingServiceSubmitNotify.js: New trait. * test/server/service/RatingServiceSubmitNotifyTest.js: Respective test. --- .../service/RatingServiceSubmitNotify.js | 81 ++++++++++++ .../service/RatingServiceSubmitNotifyTest.js | 115 ++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 src/server/service/RatingServiceSubmitNotify.js create mode 100644 test/server/service/RatingServiceSubmitNotifyTest.js diff --git a/src/server/service/RatingServiceSubmitNotify.js b/src/server/service/RatingServiceSubmitNotify.js new file mode 100644 index 0000000..7a2c689 --- /dev/null +++ b/src/server/service/RatingServiceSubmitNotify.js @@ -0,0 +1,81 @@ +/** + * Notification on all submit + * + * Copyright (C) 2018 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 . + */ + +'use strict'; + +const { Trait } = require( 'easejs' ); +const DslRaterContext = require( '../rater/DslRaterContext' ) +const RatingService = require( './RatingService' ); + + +/** + * Triggers DataApi when no results are available + * + * This information is currently stored in `__prem_avail_count`. In the + * future, it may be worth accepting a parameter to configure this at + * runtime. + */ +module.exports = Trait( 'RatingServiceSubmitNotify' ) + .extend( RatingService, +{ + /** + * DataApi to trigger + * @type {DataApi} + */ + 'private _dapi': null, + + + /** + * Initialize mixin with DataApi to trigger + * + * @param {DataApi} dapi DataApi to trigger + */ + __mixin( dapi ) + { + this._dapi = dapi; + }, + + + /** + * Trigger previously provided DataApi when no results are available + * + * Result count is determined by DATA.__prem_avail_count. + * + * @param {Object} data rating data returned + * @param {Array} actions actions to send to client + * @param {Program} program program used to perform rating + * @param {Quote} quote quote used for rating + * + * @return {undefined} + */ + 'override protected postProcessRaterData'( data, actions, program, quote ) + { + const quote_id = quote.getId(); + const avail = ( data.__prem_avail_count || [ 0 ] )[ 0 ]; + + if ( avail === 0 ) + { + this._dapi.request( { quote_id: quote_id }, () => {} ); + } + + this.__super( data, actions, program, quote ); + }, +} ); diff --git a/test/server/service/RatingServiceSubmitNotifyTest.js b/test/server/service/RatingServiceSubmitNotifyTest.js new file mode 100644 index 0000000..dc0605b --- /dev/null +++ b/test/server/service/RatingServiceSubmitNotifyTest.js @@ -0,0 +1,115 @@ +/** + * Tests RatingServiceSubmitNotify + * + * Copyright (C) 2018 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 . + */ + +'use strict' + +const { Class } = require( 'easejs' ); +const { expect } = require( 'chai' ); + + +const { + dapi: { + DataApi, + }, + server: { + service: { + RatingServiceSubmitNotify: Sut, + RatingService, + }, + }, + test: { + server: { + service: { + RatingServiceStub, + }, + }, + }, +} = require( '../../../' ); + + +describe( 'RatingServiceSubmitNotify', () => +{ + [ + { + prem_avail_count: [ 0 ], + expected_request: true, + }, + { + prem_avail_count: [ 2 ], + expected_request: false, + }, + { + // this shouldn't happen; ignore all but first index + prem_avail_count: [ 2, 2 ], + expected_request: false, + }, + ].forEach( ( { prem_avail_count, expected_request }, i ) => + it( `sends request on post process if no premiums (#${i})`, done => + { + const { + stub_rate_data, + logger, + server, + raters, + dao, + request, + response, + quote, + } = RatingServiceStub.getStubs(); + + const quote_id = 1234; + let requested = false; + + const dapi = Class.implement( DataApi ).extend( + { + // warning: if an expectation fails, because of how + // RatingService handles errors, it will cause the test to + // _hang_ rather than throw the assertion error + request( data, callback ) + { + expect( data ).to.deep.equal( { quote_id: quote_id } ); + + requested = true; + }, + } )(); + + const sut = RatingService.use( Sut( dapi ) )( + logger, dao, server, raters + ); + + quote.getId = () => quote_id; + + // one of the methods that is called by the supertype + let save_called = false; + dao.setWorksheets = () => save_called = true; + + stub_rate_data.__prem_avail_count = prem_avail_count; + + sut.request( request, response, quote, 'something', () => + { + expect( requested ).to.equal( expected_request ); + expect( save_called ).to.be.true; + + done(); + } ); + } ) + ); +} ); From 2d1582059f5c7248111218bb3990e50b1d3ef81e Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 26 Apr 2018 15:30:04 -0400 Subject: [PATCH 4/9] RatingServiceSubmitNotify: Notify only once * src/server/db/MongoServerDao.js (getDocumentField,setDocumentField): New methods. * src/server/service/RatingServiceSubmitNotify.js (postProcessRaterData): Only notify when notification flag is not set. (_getNotifyState, _setNotified): New methods. * test/server/service/RatingServiceSubmitNotifyTest.js: Modify accordingly. --- src/server/db/MongoServerDao.js | 67 ++++++++++++++++++- .../service/RatingServiceSubmitNotify.js | 62 +++++++++++++++-- .../service/RatingServiceSubmitNotifyTest.js | 51 +++++++++++++- 3 files changed, 173 insertions(+), 7 deletions(-) diff --git a/src/server/db/MongoServerDao.js b/src/server/db/MongoServerDao.js index 48d12df..20fe8be 100644 --- a/src/server/db/MongoServerDao.js +++ b/src/server/db/MongoServerDao.js @@ -783,5 +783,70 @@ module.exports = Class( 'MongoServerDao' ) } ); }, -} ); + + /** + * Set arbitrary data on a document + * + * @param {number} qid quote/document id + * @param {string} key field key + * @param {*} value field value + * @param {function(?Error)} callback completion callback + * + * @return {undefined} + */ + 'public setDocumentField'( qid, key, value, callback ) + { + this._collection.update( + { id: qid }, + { '$set': { [key]: value } }, + + // create record if it does not yet exist + { upsert: true }, + + // on complete + function( err ) + { + callback && callback( err ); + return; + } + ); + }, + + + /** + * Retrieve arbitrary data on a document + * + * @param {number} qid quote/document id + * @param {string} key field key + * @param {function(?Error)} callback completion callback + * + * @return {undefined} + */ + 'public getDocumentField'( qid, key, callback ) + { + this._collection.find( + { id: qid }, + { limit: 1 }, + function( err, cursor ) + { + if ( err !== null ) + { + callback( err, null ); + return; + } + + cursor.toArray( function( err, data ) + { + if ( err !== null ) + { + callback( err, null ); + return; + } + + callback( null, data[ key ] ); + } ); + } + ); + }, +} ); diff --git a/src/server/service/RatingServiceSubmitNotify.js b/src/server/service/RatingServiceSubmitNotify.js index 7a2c689..cfcf210 100644 --- a/src/server/service/RatingServiceSubmitNotify.js +++ b/src/server/service/RatingServiceSubmitNotify.js @@ -32,6 +32,10 @@ const RatingService = require( './RatingService' ); * This information is currently stored in `__prem_avail_count`. In the * future, it may be worth accepting a parameter to configure this at * runtime. + * + * Notification status will persist using the provided DAO. The next time + * such a notification is requested, it will only occur if the flag is not + * set. */ module.exports = Trait( 'RatingServiceSubmitNotify' ) .extend( RatingService, @@ -42,15 +46,23 @@ module.exports = Trait( 'RatingServiceSubmitNotify' ) */ 'private _dapi': null, + /** + * Data store for notification flag + * @type {ServerDao} + */ + 'private _notifyDao': null, + /** * Initialize mixin with DataApi to trigger * - * @param {DataApi} dapi DataApi to trigger + * @param {DataApi} dapi DataApi to trigger + * @param {ServerDao} dao data store for notification flag */ - __mixin( dapi ) + __mixin( dapi, dao ) { - this._dapi = dapi; + this._dapi = dapi; + this._notifyDao = dao; }, @@ -73,9 +85,51 @@ module.exports = Trait( 'RatingServiceSubmitNotify' ) if ( avail === 0 ) { - this._dapi.request( { quote_id: quote_id }, () => {} ); + this._getNotifyState( quote_id, notified => + { + if ( notified === true ) + { + return; + } + + this._dapi.request( { quote_id: quote_id }, () => {} ); + this._setNotified( quote_id ); + } ); } this.__super( data, actions, program, quote ); }, + + + /** + * Get value of notification flag + * + * @param {number} quote_id id of quote + * @param {function(boolean)} callback callback to call when complete + * + * @return {undefined} + */ + 'private _getNotifyState'( quote_id, callback ) + { + this._notifyDao.getDocumentField( + quote_id, + 'submitNotified', + ( err, value ) => callback( value ) + ); + }, + + + /** + * Set notification flag + * + * @param {number} quote_id id of quote + * + * @return {undefined} + */ + 'private _setNotified'( quote_id ) + { + this._notifyDao.setDocumentField( + quote_id, 'submitNotified', true + ); + }, } ); diff --git a/test/server/service/RatingServiceSubmitNotifyTest.js b/test/server/service/RatingServiceSubmitNotifyTest.js index dc0605b..2a23107 100644 --- a/test/server/service/RatingServiceSubmitNotifyTest.js +++ b/test/server/service/RatingServiceSubmitNotifyTest.js @@ -50,18 +50,39 @@ describe( 'RatingServiceSubmitNotify', () => [ { prem_avail_count: [ 0 ], + prev_called: false, expected_request: true, }, { prem_avail_count: [ 2 ], + prev_called: false, expected_request: false, }, { // this shouldn't happen; ignore all but first index prem_avail_count: [ 2, 2 ], + prev_called: false, expected_request: false, }, - ].forEach( ( { prem_avail_count, expected_request }, i ) => + + // save as above, but already saved + { + prem_avail_count: [ 0 ], + prev_called: true, + expected_request: false, + }, + { + prem_avail_count: [ 2 ], + prev_called: true, + expected_request: false, + }, + { + // this shouldn't happen; ignore all but first index + prem_avail_count: [ 2, 2 ], + prev_called: true, + expected_request: false, + }, + ].forEach( ( { prem_avail_count, expected_request, prev_called }, i ) => it( `sends request on post process if no premiums (#${i})`, done => { const { @@ -91,7 +112,7 @@ describe( 'RatingServiceSubmitNotify', () => }, } )(); - const sut = RatingService.use( Sut( dapi ) )( + const sut = RatingService.use( Sut( dapi, dao ) )( logger, dao, server, raters ); @@ -101,6 +122,27 @@ describe( 'RatingServiceSubmitNotify', () => let save_called = false; dao.setWorksheets = () => save_called = true; + // whether the notify flag is actually set + let notify_saved = false; + + // request for notification status + dao.getDocumentField = ( qid, key, callback ) => + { + expect( qid ).to.equal( quote_id ); + expect( key ).to.equal( 'submitNotified' ); + + callback( null, prev_called ); + }; + + dao.setDocumentField = ( qid, key, value, callback ) => + { + expect( qid ).to.equal( quote_id ); + expect( key ).to.equal( 'submitNotified' ); + expect( value ).to.equal( true ); + + notify_saved = true; + }; + stub_rate_data.__prem_avail_count = prem_avail_count; sut.request( request, response, quote, 'something', () => @@ -108,6 +150,11 @@ describe( 'RatingServiceSubmitNotify', () => expect( requested ).to.equal( expected_request ); expect( save_called ).to.be.true; + // only save notification status if we're notifying + expect( notify_saved ).to.equal( + !prev_called && expected_request + ); + done(); } ); } ) From d9c442b8e214d957eb61a201dc47e944f6e545c9 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 26 Apr 2018 15:32:22 -0400 Subject: [PATCH 5/9] QuoteDataApi: Correct constructor argument error --- src/dapi/QuoteDataApi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dapi/QuoteDataApi.js b/src/dapi/QuoteDataApi.js index 16e35b7..1c34eed 100644 --- a/src/dapi/QuoteDataApi.js +++ b/src/dapi/QuoteDataApi.js @@ -61,7 +61,7 @@ module.exports = Class( 'QuoteDataApi' ) if ( !( Class.isA( DataApi, dapi ) ) ) { throw TypeError( - 'Expected object of type DataApi; given: ' + data_api + 'Expected object of type DataApi; given: ' + dapi ); } From ae0d9b3862514d8628f7e8899a25613284038c1c Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Fri, 27 Apr 2018 16:06:33 -0400 Subject: [PATCH 6/9] RatingServiceSubmitNotify: Create dapi dynamically with session Session information needs to be available for request session spoofing. --- src/server/service/RatingService.js | 15 ++++---- .../service/RatingServiceSubmitNotify.js | 33 ++++++++++-------- src/test/server/service/RatingServiceStub.js | 1 + .../service/RatingServiceSubmitNotifyTest.js | 34 ++++++++++--------- test/server/service/RatingServiceTest.js | 14 ++++++-- 5 files changed, 59 insertions(+), 38 deletions(-) diff --git a/src/server/service/RatingService.js b/src/server/service/RatingService.js index 65a6215..15fcbb0 100644 --- a/src/server/service/RatingService.js +++ b/src/server/service/RatingService.js @@ -155,7 +155,7 @@ module.exports = Class( 'RatingService', actions = actions || []; _self.postProcessRaterData( - rate_data, actions, program, quote + request, rate_data, actions, program, quote ); const class_dest = {}; @@ -244,14 +244,17 @@ module.exports = Class( 'RatingService', /** * Process rater data returned from a rater * - * @param {Object} data rating data returned - * @param {Array} actions actions to send to client - * @param {Program} program program used to perform rating - * @param {Quote} quote quote used for rating + * @param {UserRequest} request user request to satisfy + * @param {Object} data rating data returned + * @param {Array} actions actions to send to client + * @param {Program} program program used to perform rating + * @param {Quote} quote quote used for rating * * @return {undefined} */ - 'virtual protected postProcessRaterData': function( data, actions, program, quote ) + 'virtual protected postProcessRaterData': function( + request, data, actions, program, quote + ) { var meta = data._cmpdata || {}; diff --git a/src/server/service/RatingServiceSubmitNotify.js b/src/server/service/RatingServiceSubmitNotify.js index cfcf210..c333561 100644 --- a/src/server/service/RatingServiceSubmitNotify.js +++ b/src/server/service/RatingServiceSubmitNotify.js @@ -41,10 +41,10 @@ module.exports = Trait( 'RatingServiceSubmitNotify' ) .extend( RatingService, { /** - * DataApi to trigger - * @type {DataApi} + * Function returning DataApi to trigger + * @type {Function(UserSession):DataApi} */ - 'private _dapi': null, + 'private _dapif': null, /** * Data store for notification flag @@ -56,12 +56,12 @@ module.exports = Trait( 'RatingServiceSubmitNotify' ) /** * Initialize mixin with DataApi to trigger * - * @param {DataApi} dapi DataApi to trigger - * @param {ServerDao} dao data store for notification flag + * @param {Function(UserSession):DataApi} dapif Function producing DataApi + * @param {ServerDao} dao store for notification flag */ - __mixin( dapi, dao ) + __mixin( dapif, dao ) { - this._dapi = dapi; + this._dapif = dapif; this._notifyDao = dao; }, @@ -71,14 +71,17 @@ module.exports = Trait( 'RatingServiceSubmitNotify' ) * * Result count is determined by DATA.__prem_avail_count. * - * @param {Object} data rating data returned - * @param {Array} actions actions to send to client - * @param {Program} program program used to perform rating - * @param {Quote} quote quote used for rating + * @param {UserRequest} request user request + * @param {Object} data rating data returned + * @param {Array} actions actions to send to client + * @param {Program} program program used to perform rating + * @param {Quote} quote quote used for rating * * @return {undefined} */ - 'override protected postProcessRaterData'( data, actions, program, quote ) + 'override protected postProcessRaterData'( + request, data, actions, program, quote + ) { const quote_id = quote.getId(); const avail = ( data.__prem_avail_count || [ 0 ] )[ 0 ]; @@ -92,12 +95,14 @@ module.exports = Trait( 'RatingServiceSubmitNotify' ) return; } - this._dapi.request( { quote_id: quote_id }, () => {} ); + this._dapif( request ) + .request( { quote_id: quote_id }, () => {} ); + this._setNotified( quote_id ); } ); } - this.__super( data, actions, program, quote ); + this.__super( request, data, actions, program, quote ); }, diff --git a/src/test/server/service/RatingServiceStub.js b/src/test/server/service/RatingServiceStub.js index 4b0b58c..c58d1cf 100644 --- a/src/test/server/service/RatingServiceStub.js +++ b/src/test/server/service/RatingServiceStub.js @@ -61,6 +61,7 @@ exports.getStubs = function() const request = { getSession: () => session, + getSessionIdName: () => {}, }; const response = {}; diff --git a/test/server/service/RatingServiceSubmitNotifyTest.js b/test/server/service/RatingServiceSubmitNotifyTest.js index 2a23107..b4f5922 100644 --- a/test/server/service/RatingServiceSubmitNotifyTest.js +++ b/test/server/service/RatingServiceSubmitNotifyTest.js @@ -86,33 +86,35 @@ describe( 'RatingServiceSubmitNotify', () => it( `sends request on post process if no premiums (#${i})`, done => { const { - stub_rate_data, - logger, - server, - raters, dao, + logger, + quote, + raters, request, response, - quote, + server, + stub_rate_data, } = RatingServiceStub.getStubs(); const quote_id = 1234; let requested = false; - const dapi = Class.implement( DataApi ).extend( - { - // warning: if an expectation fails, because of how - // RatingService handles errors, it will cause the test to - // _hang_ rather than throw the assertion error - request( data, callback ) + const dapif = given_request => + Class.implement( DataApi ).extend( { - expect( data ).to.deep.equal( { quote_id: quote_id } ); + // warning: if an expectation fails, because of how + // RatingService handles errors, it will cause the test to + // _hang_ rather than throw the assertion error + request( data, callback ) + { + expect( given_request ).to.equal( request ); + expect( data ).to.deep.equal( { quote_id: quote_id } ); - requested = true; - }, - } )(); + requested = true; + }, + } )(); - const sut = RatingService.use( Sut( dapi, dao ) )( + const sut = RatingService.use( Sut( dapif, dao ) )( logger, dao, server, raters ); diff --git a/test/server/service/RatingServiceTest.js b/test/server/service/RatingServiceTest.js index ba2c13e..948af9a 100644 --- a/test/server/service/RatingServiceTest.js +++ b/test/server/service/RatingServiceTest.js @@ -33,7 +33,15 @@ describe( 'RatingService', () => { let processed = false; - const { logger, server, raters, dao, request, response, quote } = RatingServiceStub.getStubs(); + const { + logger, + server, + raters, + dao, + request, + response, + quote, + } = RatingServiceStub.getStubs(); dao.mergeBucket = () => { @@ -43,7 +51,9 @@ describe( 'RatingService', () => const sut = Sut.extend( { - 'override postProcessRaterData'( data, actions, program, quote ) + 'override postProcessRaterData'( + request, data, actions, program, quote + ) { processed = true; } From 3bed93e7507be0536e962eec879f4abe3a053345 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Mon, 30 Apr 2018 11:00:54 -0400 Subject: [PATCH 7/9] SpoofedNodeHttpImpl: Fix docblocks (User{Session=>Request}) Wrong object! --- src/dapi/http/SpoofedNodeHttpImpl.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dapi/http/SpoofedNodeHttpImpl.js b/src/dapi/http/SpoofedNodeHttpImpl.js index ac68812..f8d05fb 100644 --- a/src/dapi/http/SpoofedNodeHttpImpl.js +++ b/src/dapi/http/SpoofedNodeHttpImpl.js @@ -37,7 +37,7 @@ module.exports = Trait( 'SpoofedNodeHttpImpl' ) { /** * Session to spoof - * @type {UserSession} + * @type {UserRequest} */ 'private _request': null, @@ -45,11 +45,11 @@ module.exports = Trait( 'SpoofedNodeHttpImpl' ) /** * Use session for spoofing requests * - * @param {UserSession} session session to spoof + * @param {UserRequest} request session to spoof */ - __mixin( session ) + __mixin( request ) { - this._request = session; + this._request = request; }, From 25cb1f62c1adaa136ff0e0f769e33d5180736b3b Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Mon, 30 Apr 2018 11:14:31 -0400 Subject: [PATCH 8/9] controller: Integrate RatingServiceSubmitNotify This is not an ideal location. --- src/server/daemon/controller.js | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/server/daemon/controller.js b/src/server/daemon/controller.js index e6571f2..3171315 100644 --- a/src/server/daemon/controller.js +++ b/src/server/daemon/controller.js @@ -43,6 +43,12 @@ const { }, dapi: { + http: { + HttpDataApi, + HttpDataApiUrlData, + NodeHttpImpl, + SpoofedNodeHttpImpl, + }, DataApiFactory, DataApiManager, }, @@ -69,6 +75,7 @@ const { }, RatingService, + RatingServiceSubmitNotify, TokenedService, TokenDao, }, @@ -118,7 +125,30 @@ exports.init = function( logger, enc_service, conf ) server_cache = _createCache( server ); server.init( server_cache, exports.rater ); - rating_service = RatingService( logger, dao, server, exports.rater ); + const createSubmitDapi = request => HttpDataApi + .use( HttpDataApiUrlData( [ 'quote_id' ] ) ) + ( + 'http://localhost:2222', + 'PUT', + + NodeHttpImpl + .use( SpoofedNodeHttpImpl( request ) ) + ( + { + http: require( 'http' ), + https: require( 'https' ), + }, + require( 'url' ), + this._origin + ), + '' + ); + + rating_service = RatingService + .use( RatingServiceSubmitNotify( createSubmitDapi, dao ) ) + ( + logger, dao, server, exports.rater + ); // TODO: exports.init needs to support callbacks; this will work, but // only because it's unlikely that we'll get a request within From ea30e94527d746df7fd805b2d4a981d5c8771d5f Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Mon, 30 Apr 2018 11:26:43 -0400 Subject: [PATCH 9/9] Integrate noResultsUrl configuration This is not the ideal implementation. Instantiation of RatingService and such should be moved out of the controller entirely, but we don't have the time for that right now. --- conf/vanilla-server.json | 3 ++- src/server/daemon/Daemon.js | 17 ++++++++++------- src/server/daemon/controller.js | 8 +++++--- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/conf/vanilla-server.json b/conf/vanilla-server.json index 41e90b8..452dc4c 100644 --- a/conf/vanilla-server.json +++ b/conf/vanilla-server.json @@ -49,7 +49,8 @@ "remote": { "host": "localhost", "domain": "" - } + }, + "noResultsUrl": "" }, "c1export": { "host": "localhost", diff --git a/src/server/daemon/Daemon.js b/src/server/daemon/Daemon.js index 282197f..7a13769 100644 --- a/src/server/daemon/Daemon.js +++ b/src/server/daemon/Daemon.js @@ -113,7 +113,8 @@ module.exports = AbstractClass( 'Daemon', this._createDebugLog(), this._createAccessLog(), this._conf.get( 'skey' ), - ] ).then( ([ debug_log, access_log, skey ]) => + this._conf.get( 'services.rating.noResultsUrl' ), + ] ).then( ([ debug_log, access_log, skey, no_results_url ]) => { this._debugLog = debug_log; this._accessLog = access_log; @@ -122,7 +123,7 @@ module.exports = AbstractClass( 'Daemon', this._rater = liza.server.rater.ProcessManager(); this._encService = this.getEncryptionService(); this._memcache = this.getMemcacheClient(); - this._routers = this.getRouters( skey ); + this._routers = this.getRouters( skey, no_results_url ); } ) .then( () => this._startDaemon() ); }, @@ -182,14 +183,16 @@ module.exports = AbstractClass( 'Daemon', }, - 'protected getProgramController': function( skey ) + 'protected getProgramController': function( skey, no_results_url ) { var controller = require( './controller' ); - controller.rater = this._rater; + + controller.rater = this._rater; + controller.no_results_url = no_results_url || ""; if ( skey ) { - controller.skey = skey; + controller.skey = skey; } return controller; @@ -276,10 +279,10 @@ module.exports = AbstractClass( 'Daemon', 'abstract protected getEncryptionService': [], - 'protected getRouters': function( skey ) + 'protected getRouters': function( skey, no_results_url ) { return [ - this.getProgramController( skey ), + this.getProgramController( skey, no_results_url ), this.getScriptsController(), this.getClientErrorController(), ]; diff --git a/src/server/daemon/controller.js b/src/server/daemon/controller.js index 3171315..b8a0950 100644 --- a/src/server/daemon/controller.js +++ b/src/server/daemon/controller.js @@ -100,8 +100,9 @@ var sflag = {}; // TODO: kluge to get liza somewhat decoupled from lovullo (rating module) -exports.rater = {}; -exports.skey = ""; +exports.rater = {}; +exports.skey = ""; +exports.no_results_url = ""; exports.init = function( logger, enc_service, conf ) @@ -125,10 +126,11 @@ exports.init = function( logger, enc_service, conf ) server_cache = _createCache( server ); server.init( server_cache, exports.rater ); + // TODO: do none of this if no_results_url is provided const createSubmitDapi = request => HttpDataApi .use( HttpDataApiUrlData( [ 'quote_id' ] ) ) ( - 'http://localhost:2222', + exports.no_results_url, 'PUT', NodeHttpImpl