diff --git a/backend/services/service.dataSourceRules.coffee b/backend/services/service.dataSourceRules.coffee index 4230a3be4..6849839b2 100644 --- a/backend/services/service.dataSourceRules.coffee +++ b/backend/services/service.dataSourceRules.coffee @@ -70,7 +70,7 @@ module.exports = _putRules {data_source_id: dataSourceId, data_source_type: dataSourceType, data_type: dataListType}, rules deleteRules: (dataSourceId, dataSourceType, dataListType) -> - _deleteRules data_source_id: dataSourceId, data_source_type: dataSourceType, data_type: dataListType + _deleteRules {data_source_id: dataSourceId, data_source_type: dataSourceType, data_type: dataListType} getListRules: (dataSourceId, dataSourceType, dataListType, list) -> _getRules data_source_id: dataSourceId, data_source_type: dataSourceType, data_type: dataListType, list: list @@ -82,7 +82,7 @@ module.exports = _putRules {data_source_id: dataSourceId, data_source_type: dataSourceType, data_type: dataListType, list: list}, rules deleteListRules: (dataSourceId, dataSourceType, dataListType, list) -> - _deleteRules data_source_id: dataSourceId, data_source_type: dataSourceType, data_type: dataListType, list: list + _deleteRules {data_source_id: dataSourceId, data_source_type: dataSourceType, data_type: dataListType, list: list} getRule: (dataSourceId, dataSourceType, dataListType, list, ordering) -> _getRules data_source_id: dataSourceId, data_source_type: dataSourceType, data_type: dataListType, list: list, ordering: ordering @@ -100,4 +100,4 @@ module.exports = throw new PartiallyHandledError(error) deleteRule: (dataSourceId, dataSourceType, dataListType, list, ordering) -> - _deleteRules data_source_id: dataSourceId, data_source_type: dataSourceType, data_type: dataListType, list: list, ordering: ordering + _deleteRules {data_source_id: dataSourceId, data_source_type: dataSourceType, data_type: dataListType, list: list, ordering: ordering} diff --git a/spec/backend/services/service.dataSourceRules.spec.coffee b/spec/backend/services/service.dataSourceRules.spec.coffee new file mode 100644 index 000000000..2aca43d1c --- /dev/null +++ b/spec/backend/services/service.dataSourceRules.spec.coffee @@ -0,0 +1,390 @@ +_ = require 'lodash' +rewire = require 'rewire' +svc = rewire '../../../backend/services/service.dataSourceRules.coffee' +{PartiallyHandledError, isUnhandled} = require '../../../backend/utils/errors/util.error.partiallyHandledError' +tables = require '../../../backend/config/tables' +sqlMockUtil = require '../../specUtils/sqlMock.coffee' + + +describe 'service.dataSourceRules.coffee', -> + describe 'private api', -> + beforeEach -> + @rulesTableSqlMock = new sqlMockUtil.SqlMock + groupName: 'config' + tableHandle: 'dataNormalization' + + svc.__set__('tables', @rulesTableSqlMock) + @_addRulesFn = svc.__get__('_addRules') + + @query = + data_source_id: 'CoreLogic' + data_source_type: 'county' + data_type: 'tax' + list: 'general' + + @rules = [ + config: + DataType: "Int" + nullZero: true + output: "Int Param" + input: "\"\"" + required: false + data_source_type: "county" + data_type: "tax" + , + config: + DataType: "Int" + nullZero: true + output: "Another Int Param" + input: "\"\"" + required: false + data_source_type: "county" + data_type: "tax" + ] + + it 'should have valid _addRules insert query, without a given order count', (done) -> + expectedInsertRules = [ + config: + DataType: "Int" + nullZero: true + output: "Int Param" + input: "\"\"" + required: false + data_source_type: "county" + data_type: "tax" + data_source_id: "CoreLogic" + list: "general" + ordering: 0 + , + config: + DataType: "Int" + nullZero: true + output: "Another Int Param" + input: "\"\"" + required: false + data_source_type: "county" + data_type: "tax" + data_source_id: "CoreLogic" + list: "general" + ordering: 1 + ] + + @_addRulesFn(@query, @rules) + + @rulesTableSqlMock.insertSpy.calledOnce.should.be.true + @rulesTableSqlMock.insertSpy.calledWith(expectedInsertRules).should.be.true + done() + + + it 'should have valid _addRules insert query, with a given order count', (done) -> + count = [ + list: 'general' + count: 1 + , + list: 'base' + count: 3 + ] + expectedInsertRules = [ + config: + DataType: "Int" + nullZero: true + output: "Int Param" + input: "\"\"" + required: false + data_source_type: "county" + data_type: "tax" + data_source_id: "CoreLogic" + list: "general" + ordering:2 + , + config: + DataType: "Int" + nullZero: true + output: "Another Int Param" + input: "\"\"" + required: false + data_source_type: "county" + data_type: "tax" + data_source_id: "CoreLogic" + list: "general" + ordering: 3 + ] + + @_addRulesFn(@query, @rules, count) + + @rulesTableSqlMock.insertSpy.calledOnce.should.be.true + @rulesTableSqlMock.insertSpy.calledWith(expectedInsertRules).should.be.true + done() + + + describe 'rules', -> + beforeEach -> + @rulesTableSqlMock = new sqlMockUtil.SqlMock + groupName: 'config' + tableHandle: 'dataNormalization' + + svc.__set__('tables', @rulesTableSqlMock) + + @query = + data_source_id: 'CoreLogic' + data_source_type: 'county' + data_type: 'tax' + + @rules = [ + config: + DataType: "Int" + nullZero: true + output: "Int Param" + input: "\"\"" + required: false + data_source_type: "county" + data_type: "tax" + list: 'general' + ] + + it 'should have valid getRules query', (done) -> + expectedQuery = """select * from "config_data_normalization" where""" + + """ "data_source_id" = 'CoreLogic' and "data_source_type" = 'county' and "data_type" = 'tax'""" + + svc.getRules(@query.data_source_id, @query.data_source_type, @query.data_type) + + @rulesTableSqlMock.selectSpy.calledOnce.should.be.true + @rulesTableSqlMock.whereSpy.calledOnce.should.be.true + @rulesTableSqlMock.whereSpy.calledWith(@query).should.be.true + + done() + + it 'should have valid createRules query', (done) -> + expectedQuery = """select max(ordering) as count, list from "config_data_normalization" where "data_source_id" = """ + + """'CoreLogic' and "data_source_type" = 'county' and "data_type" = 'tax' group by "list" """.trim() + + svc.createRules(@query.data_source_id, @query.data_source_type, @query.data_type, @rules) + @rulesTableSqlMock.toString().should.equal expectedQuery + @rulesTableSqlMock.selectSpy.calledOnce.should.be.true + @rulesTableSqlMock.groupBySpy.calledOnce.should.be.true + @rulesTableSqlMock.whereSpy.calledOnce.should.be.true + @rulesTableSqlMock.thenSpy().callCount.should.equal 2 + @rulesTableSqlMock.catchSpy().callCount.should.equal 1 + + # first .then callback should perform insert + @rulesTableSqlMock.insertSpy.calledOnce.should.be.false + fn = @rulesTableSqlMock.getThenCallback(0) + callback1_param = [ + list: 'general' + count: 1 + ] + fn(callback1_param) + @rulesTableSqlMock.insertSpy.calledOnce.should.be.true + + done() + + it 'should have valid putRules query', (done) -> + svc.__with__('dbs', sqlMockUtil.SqlMock.dbs)( + => + expectedQuery = """delete from "config_data_normalization" where "data_source_id" = 'CoreLogic' and "data_source_type" = 'county' and "data_type" = 'tax'""" + + svc.putRules(@query.data_source_id, @query.data_source_type, @query.data_type, @rules) + + @rulesTableSqlMock.toString().should.equal expectedQuery + @rulesTableSqlMock.insertSpy.calledOnce.should.be.false + @rulesTableSqlMock.deleteSpy.calledOnce.should.be.true + @rulesTableSqlMock.getThenCallback(0)() + @rulesTableSqlMock.getThenCallback(1)() + @rulesTableSqlMock.insertSpy.calledOnce.should.be.true + @rulesTableSqlMock.commitSpy.calledOnce.should.be.true + + done() + ) + + it 'should have valid deleteRules query', (done) -> + expectedQuery = """delete from "config_data_normalization" where "data_source_id" = 'CoreLogic' and "data_source_type" = 'county' and "data_type" = 'tax'""" + + svc.deleteRules(@query.data_source_id, @query.data_source_type, @query.data_type) + + @rulesTableSqlMock.toString().should.equal expectedQuery + @rulesTableSqlMock.whereSpy.calledWith(@query).should.be.true + @rulesTableSqlMock.deleteSpy.calledOnce.should.be.true + @rulesTableSqlMock.thenSpy().callCount.should.equal 1 + @rulesTableSqlMock.catchSpy().callCount.should.equal 1 + + + fn = @rulesTableSqlMock.getThenCallback(0) + fn(-1).should.be.false + fn(0).should.be.true + fn(1).should.be.true + + done() + + + describe 'list rules', -> + beforeEach -> + @rulesTableSqlMock = new sqlMockUtil.SqlMock + groupName: 'config' + tableHandle: 'dataNormalization' + + svc.__set__('tables', @rulesTableSqlMock) + + @query = + data_source_id: 'CoreLogic' + data_source_type: 'county' + data_type: 'tax' + list: 'general' + + @rules = [ + config: + DataType: "Int" + nullZero: true + output: "Int Param" + input: "\"\"" + required: false + data_source_type: "county" + data_type: "tax" + ] + + it 'should have valid getListRules query', (done) -> + expectedQuery = """select * from "config_data_normalization" where""" + + """ "data_source_id" = 'CoreLogic' and "data_source_type" = 'county' and "data_type" = 'tax' and "list" = 'general'""" + + svc.getListRules(@query.data_source_id, @query.data_source_type, @query.data_type, @query.list) + + @rulesTableSqlMock.toString().should.equal expectedQuery + @rulesTableSqlMock.whereSpy.calledOnce.should.be.true + @rulesTableSqlMock.whereSpy.calledWith(@query).should.be.true + + done() + + it 'should have valid createListRules query', (done) -> + expectedQuery = """select max(ordering) as count, list from "config_data_normalization" where "data_source_id" = """ + + """'CoreLogic' and "data_source_type" = 'county' and "data_type" = 'tax' group by "list" """.trim() + + svc.createRules(@query.data_source_id, @query.data_source_type, @query.data_type, @query.list, @rules) + @rulesTableSqlMock.toString().should.equal expectedQuery + @rulesTableSqlMock.selectSpy.calledOnce.should.be.true + @rulesTableSqlMock.groupBySpy.calledOnce.should.be.true + @rulesTableSqlMock.whereSpy.calledOnce.should.be.true + @rulesTableSqlMock.thenSpy().callCount.should.equal 2 + @rulesTableSqlMock.catchSpy().callCount.should.equal 1 + + # first .then callback should perform insert + @rulesTableSqlMock.insertSpy.calledOnce.should.be.false + callback1_param = [ + list: 'general' + count: 1 + ] + fn = @rulesTableSqlMock.getThenCallback(0) + fn(callback1_param) + @rulesTableSqlMock.insertSpy.calledOnce.should.be.true + + done() + + it 'should have valid putListRules query', (done) -> + svc.__with__('dbs', sqlMockUtil.SqlMock.dbs)( + => + + expectedQuery = """delete from "config_data_normalization" where "data_source_id" = 'CoreLogic' and "data_source_type" = 'county' and "data_type" = 'tax' and "list" = 'general'""" + + svc.putListRules(@query.data_source_id, @query.data_source_type, @query.data_type, @query.list, @rules) + + @rulesTableSqlMock.toString().should.equal expectedQuery + @rulesTableSqlMock.insertSpy.calledOnce.should.be.false + @rulesTableSqlMock.deleteSpy.calledOnce.should.be.true + @rulesTableSqlMock.getThenCallback(0)() + @rulesTableSqlMock.getThenCallback(1)() + @rulesTableSqlMock.insertSpy.calledOnce.should.be.true + @rulesTableSqlMock.commitSpy.calledOnce.should.be.true + + done() + ) + + it 'should have valid deleteListRules query', (done) -> + expectedQuery = """delete from "config_data_normalization" where "data_source_id" = 'CoreLogic' and "data_source_type" = 'county' and "data_type" = 'tax' and "list" = 'general'""" + + svc.deleteListRules(@query.data_source_id, @query.data_source_type, @query.data_type, @query.list) + + @rulesTableSqlMock.toString().should.equal expectedQuery + @rulesTableSqlMock.whereSpy.calledWith(@query).should.be.true + @rulesTableSqlMock.deleteSpy.calledOnce.should.be.true + + fn = @rulesTableSqlMock.getThenCallback(0) + fn(-1).should.be.false + fn(0).should.be.true + fn(1).should.be.true + + done() + + + describe 'simple rule api', -> + beforeEach -> + @rulesTableSqlMock = new sqlMockUtil.SqlMock + groupName: 'config' + tableHandle: 'dataNormalization' + + svc.__set__('tables', @rulesTableSqlMock) + + @query = + data_source_id: 'CoreLogic' + data_source_type: 'county' + data_type: 'tax' + list: 'general' + ordering: 0 + + @rules = [ + config: + DataType: "Int" + nullZero: true + output: "Int Param" + input: "\"\"" + required: false + data_source_type: "county" + data_type: "tax" + ] + + it 'should have valid getRule query', (done) -> + expectedQuery = """select * from "config_data_normalization" where""" + + """ "data_source_id" = 'CoreLogic' and "data_source_type" = 'county' and "data_type" = 'tax' and "list" = 'general' and "ordering" = '0'""" + + svc.getRule(@query.data_source_id, @query.data_source_type, @query.data_type, @query.list, @query.ordering) + + @rulesTableSqlMock.toString().should.equal expectedQuery + @rulesTableSqlMock.whereSpy.calledOnce.should.be.true + @rulesTableSqlMock.whereSpy.calledWith(@query).should.be.true + + callbackParam = @rules + fn = @rulesTableSqlMock.getThenCallback(0) + fn(callbackParam).should.equal @rules[0] + + done() + + it 'should have valid updateRule query', (done) -> + svc.updateRule(@query.data_source_id, @query.data_source_type, @query.data_type, @query.list, @query.ordering, @rules[0]) + + @rulesTableSqlMock.updateSpy.calledOnce.should.be.true + @rulesTableSqlMock.updateSpy.calledWith(_.extend(@rules[0], @query)).should.be.true + @rulesTableSqlMock.whereSpy.calledOnce.should.be.true + @rulesTableSqlMock.whereSpy.calledWith(@query).should.be.true + @rulesTableSqlMock.thenSpy().callCount.should.equal 1 + @rulesTableSqlMock.catchSpy().callCount.should.equal 1 + + fn = @rulesTableSqlMock.getThenCallback(0) + fn(1).should.be.true + fn(0).should.be.false + + done() + + it 'should have valid deleteRule query', (done) -> + svc.__with__('dbs', sqlMockUtil.SqlMock.dbs)( + => + + expectedQuery = """delete from "config_data_normalization" where "data_source_id" = 'CoreLogic' and "data_source_type" = 'county' and "data_type" = 'tax' and "list" = 'general' and "ordering" = '0'""" + + svc.deleteRule(@query.data_source_id, @query.data_source_type, @query.data_type, @query.list, @query.ordering) + + @rulesTableSqlMock.toString().should.equal expectedQuery + @rulesTableSqlMock.deleteSpy.calledOnce.should.be.true + @rulesTableSqlMock.whereSpy.calledOnce.should.be.true + @rulesTableSqlMock.whereSpy.calledWith(@query).should.be.true + @rulesTableSqlMock.thenSpy().callCount.should.equal 1 + @rulesTableSqlMock.catchSpy().callCount.should.equal 1 + + done() + ) diff --git a/spec/specUtils/sqlMock.coffee b/spec/specUtils/sqlMock.coffee new file mode 100644 index 000000000..dba7cdee0 --- /dev/null +++ b/spec/specUtils/sqlMock.coffee @@ -0,0 +1,148 @@ +_ = require 'lodash' +tables = require '../../backend/config/tables' + + +class SqlMock + ### Helper class for shielding database from sql queries during tests. Advantages include: + 1) database operators will not act on database, to avoid inadvertently insert/update/delete + on data during complex query tests + 2) simplified handlers for assessing operator calls (via sinon api) + 3) simplified assessment of flow through '.then' query callback evaluators, no matter + how many are chained, and easily test input/output of callbacks + + ### + constructor: (@options) -> + if !@options.groupName? + throw new Error('\'groupName\' is a required option for SqlMock class') + if !@options.tableHandle? + throw new Error('\'tableHandle\' is a required option for SqlMock class') + + # dynamic instance hooks for the mock sql calls + @[@options.groupName] = @ + @[@options.tableHandle] = (trx) => + if trx? + @commitSpy = sinon.spy(trx, 'commit') + @rollbackSpy = sinon.spy(trx, 'rollback') + return @ + + # spy on query-evaluators + @_thenSpy = sinon.spy(@, 'then') + @_catchSpy = sinon.spy(@, 'catch') + + # spy on query-operators + @selectSpy = sinon.spy() + @groupBySpy = sinon.spy() + @whereSpy = sinon.spy() + @insertSpy = sinon.spy() + @updateSpy = sinon.spy() + @deleteSpy = sinon.spy() + + @init() + + @dbs: + get: (main) -> + temptrx = + commit: -> + rollback: -> + transaction: (callback) -> + callback(temptrx) + + thenSpy: -> + @_thenSpy + + getThenCallback: (idx) -> + @_thenSpy.getCall(idx).args[0] + + catchSpy: -> + @_catchSpy + + getCatchCallback: (idx) -> + @_catchSpy.getCall(idx).args[0] + + init: () -> + @initSvc() + @initMaintenanceContainers() + + initMaintenanceContainers: () -> + @_queryChainFlag = false + @_queryArgChain = [] + + initSvc: () -> + @_svc = tables[@options.groupName][@options.tableHandle] + + # bootstrap + @_svc = @_svc() # bootstrap + + resetSpies: () -> + @selectSpy.reset() + @groupBySpy.reset() + @whereSpy.reset() + @insertSpy.reset() + @updateSpy.reset() + @deleteSpy.reset() + + _appendArgChain: (operator, args) -> + @_queryArgChain.push + operator: operator + args: args + + _appendThenChain: (callback) -> + @thenCallbacks.push callback + + _appendCatchChain: (err) -> + @catchCallbacks.push err + + _quickQuery: () -> + if !@_queryChainFlag + for link in @_queryArgChain + @_svc = @_svc[link.operator](link.args) + @_queryChainFlag = true + @_svc + + #### public operators #### + select: (query) -> + @selectSpy(query) + @_appendArgChain('select', query) + @ + + groupBy: (cols) -> + @groupBySpy(cols) + @_appendArgChain('groupBy', cols) + @ + + where: (query) -> + @whereSpy(query) + @_appendArgChain('where', query) + @ + + insert: (args) -> + @insertSpy(args) + @_appendArgChain('insert', args) + @ + + update: (args) -> + @updateSpy(args) + @_appendArgChain('update', args) + @ + + delete: (args) -> + @deleteSpy(args) + @_appendArgChain('delete', args) + @ + + #### public evaluators #### + then: (callback) -> + @ + + catch: (err) -> + @ + + toString: () -> + @_quickQuery().toString() + + toSQL: () -> + @_quickQuery().toSQL() + + +module.exports = + SqlMock: SqlMock \ No newline at end of file