UNPKG

@wmfs/tymly-rankings-plugin

Version:

Plugin which handles ranking of data for Tymly framework

390 lines (336 loc) 14.5 kB
/* eslint-env mocha */ 'use strict' const chai = require('chai') const expect = chai.expect const tymly = require('@wmfs/tymly') const path = require('path') const HlPgClient = require('@wmfs/hl-pg-client') const process = require('process') const sqlScriptRunner = require('./fixtures/sql-script-runner.js') const moment = require('moment') const WEIGHTS_TO_UPDATE = require('./fixtures/updated-weights.json') process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason) // application specific logging, throwing an error, or other logic here }) describe('Tests the Ranking State Resource', function () { this.timeout(15000) const REFRESH_ALL_STATE_MACHINE_NAME = 'wmfs_refreshAll_1_0' const REFRESH_STATE_MACHINE_NAME = 'test_refreshRanking_1_0' const SET_REFRESH_STATE_MACHINE_NAME = 'wmfs_setAndRefresh_1_0' const REFRESH_RISK_STATE_MACHINE_NAME = 'test_refreshRiskScore_1_0' const originalScores = [] let statebox, tymlyService, rankingService, rankingModel, riskScoreTrailModel, refreshModel const AuditDate = () => moment([2018, 5, 18]) let TestTimestamp // explicitly opening a db connection as seom setup needs to be carried // out before tymly can be started up const pgConnectionString = process.env.PG_CONNECTION_STRING const client = new HlPgClient(pgConnectionString) before(function () { if (process.env.PG_CONNECTION_STRING && !/^postgres:\/\/[^:]+:[^@]+@(?:localhost|127\.0\.0\.1).*$/.test(process.env.PG_CONNECTION_STRING)) { console.log(`Skipping tests due to unsafe PG_CONNECTION_STRING value (${process.env.PG_CONNECTION_STRING})`) this.skip() } }) describe('setup', () => { it('create test resources', () => { return sqlScriptRunner('./db-scripts/setup.sql', client) }) it('start tymly', done => { tymly.boot( { pluginPaths: [ path.resolve(__dirname, './..'), require.resolve('@wmfs/tymly-pg-plugin'), require.resolve('@wmfs/tymly-test-helpers/plugins/mock-users-plugin') ], blueprintPaths: [ path.resolve(__dirname, './fixtures/blueprint'), path.resolve(__dirname, '../lib/blueprints/ranking-blueprint') ], config: {} }, (err, tymlyServices) => { expect(err).to.eql(null) tymlyService = tymlyServices.tymly rankingService = tymlyServices.rankings statebox = tymlyServices.statebox rankingModel = tymlyServices.storage.models.test_rankingUprns riskScoreTrailModel = tymlyServices.storage.models.test_riskScoreTrail refreshModel = tymlyServices.storage.models.wmfs_rankingRefreshStatus tymlyServices.timestamp.timeProvider = { today () { return moment(TestTimestamp) } } // debug provider done() } ) }) it('should refresh all rankings', async () => { const execDesc = await statebox.startExecution({}, REFRESH_ALL_STATE_MACHINE_NAME, { sendResponse: 'COMPLETE' }) expect(execDesc.status).to.eql('SUCCEEDED') }) it('should check risk score trail model is empty', async () => { const trail = await riskScoreTrailModel.find({}) expect(trail.length).to.eql(0) }) it('should record a risk score trail for a property being adopted', async () => { const ranking = await rankingModel.findOne({ orderBy: ['created'] }) await riskScoreTrailModel.create({ ...ranking, eventName: 'ADOPTED' }) }) it('should refresh all rankings again', async () => { const execDesc = await statebox.startExecution({}, REFRESH_ALL_STATE_MACHINE_NAME, { sendResponse: 'COMPLETE' }) expect(execDesc.status).to.eql('SUCCEEDED') }) it('should check risk score trail model has been recorded after refresh', async () => { const trail = await riskScoreTrailModel.find({ orderBy: ['created'] }) expect(trail.length).to.eql(2) expect(trail[0].eventName).to.eql('ADOPTED') expect(trail[1].eventName).to.eql('REFRESHED') }) it('verify factory data', async () => { const viewData = await client.query('select * from test.factory_scores') const rankingData = await rankingModel.find({ where: { rankingName: { equals: 'factory' } } }) const _statsData = await client.query('select * from test.ranking_uprns_stats where category = \'factory\'') const statsData = _statsData.rows[0] const mergedData = rankingData .map((r, i) => { return { uprn: r.uprn, score: viewData.rows[i].updated_risk_score || viewData.rows[i].original_risk_score, range: r.range } }) .sort((b, c) => { return b.score - c.score }) expect(mergedData[0].range).to.eql('very-low') expect(mergedData[mergedData.length - 1].range).to.eql('very-high') originalScores.push(mergedData[0], mergedData[mergedData.length - 1]) expect(statsData.ranges.veryLow.lowerBound).to.eql(0) expect(statsData.ranges.veryHigh.upperBound).to.eql(mergedData[mergedData.length - 1].score) expect(+statsData.count).to.eql(13) expect(+statsData.mean).to.eql(58.23) expect(+statsData.stdev).to.eql(31.45) }) it('test the service function to find range', async () => { const viewRes = await client.query('select * from test.factory_scores;') const { original_risk_score: originalRiskScore, updated_risk_score: updatedRiskScore } = viewRes.rows[0] const score = updatedRiskScore || originalRiskScore const range = await rankingService.findRange('test.ranking_uprns_stats', 'factory', score) expect(range).to.eql('veryHigh') }) it('test the service function to find distribution', async () => { const viewRes = await client.query('select * from test.factory_scores;') const { original_risk_score: originalRiskScore, updated_risk_score: updatedRiskScore } = viewRes.rows[0] const score = updatedRiskScore || originalRiskScore const dist = await rankingService.findDistribution('test.ranking_uprns_stats', 'factory', score) expect(dist).to.eql(0.0016) }) }) describe('calculated risk scores', () => { describe('update with fire safety level', () => { it('update the ranking model with fire safety level', async () => { await rankingModel.upsert({ uprn: originalScores[0].uprn, rankingName: 'factory', fsManagement: 'high', lastAuditDate: AuditDate(), lastEnforcementAction: 'SATISFACTORY' }, {}) await rankingModel.upsert({ uprn: originalScores[1].uprn, rankingName: 'factory', fsManagement: 'veryLow', lastAuditDate: AuditDate(), lastEnforcementAction: 'ENFORCEMENT' }, {}) // Changing fsManagement and lastEnforcementAction affects the risk score so let's get the updated one const a = await client.query(`select original_risk_score from test.factory_scores where uprn = ${originalScores[0].uprn}`) const b = await client.query(`select original_risk_score from test.factory_scores where uprn = ${originalScores[1].uprn}`) // fsManagement score for high = 20 // lastEnforcementAction score for SATISFACTORY = -16 // A should change by (20 + -16 = 4) expect(a.rows[0].original_risk_score).to.eql(originalScores[0].score + 4) // fsManagement score for veryLow = 60 // lastEnforcementAction score for ENFORCEMENT = 64 // B should change by (60 + 64 = 124) expect(b.rows[0].original_risk_score).to.eql(originalScores[1].score + 124) // Update originalScores originalScores[0].score = a.rows[0].original_risk_score originalScores[1].score = b.rows[0].original_risk_score }) }) const refreshTests = [ { days: 0, a_score: 13, b_score: 62.62 }, { days: 365, a_score: 15.34, b_score: 146.42 }, { days: 730, a_score: 17.54, b_score: 212.45 }, { days: 1460, a_score: 21.10, b_score: 243.92 } ] // a = high, exp = -0.001, score = 26 // b = veryLow, exp = -0.004, score = 246 // mean = 68.07692 // stdev = 57.18453 // ---- Day 0 ---- // medium risk goes to half original score on day 0 // high risk goes to (mean + stddev) / 2 on day 0 // Growth curve intersection // a = 4394 days // b = 830 days // Expected score after 365 // a = 26 / ( 1 + ( 81 * ( e ^ ( (365+4394) * -0.001 ) ) ) ) = 15.34 // b = 246 / ( 1 + ( 81 * ( e ^ ( (365+830) * -0.004 ) ) ) ) = 146.42 // Expected score after 730 // a = 26 / ( 1 + ( 81 * ( e ^ ( (730+4394) * -0.001 ) ) ) ) = 17.54 // b = 246 / ( 1 + ( 81 * ( e ^ ( (730+830) * -0.004 ) ) ) ) = 212.45 // Expected score after 1460 // a = 26 / ( 1 + ( 81 * ( e ^ ( (1460+4394) * -0.001 ) ) ) ) = 21.10 // b = 246 / ( 1 + ( 81 * ( e ^ ( (1460+830) * -0.004 ) ) ) ) = 243.92 for (const rt of refreshTests) { describe(`audit was ${rt.days} ago`, () => { it('refresh ranking', async () => { TestTimestamp = AuditDate().add(rt.days, 'days') const execDesc = await statebox.startExecution( { schema: 'test', category: 'factory' }, REFRESH_STATE_MACHINE_NAME, { sendResponse: 'COMPLETE' } ) expect(execDesc.status).to.eql('SUCCEEDED') }) it('check ranking refresh status model', async () => { const refreshRecords = await refreshModel.find({ where: { key: { equals: 'test_factory' } } }) expect(refreshRecords[0].status).to.eql('ENDED') }) it(`verify calculated risk score on day ${rt.days}`, async () => { const a = await rankingModel.findById(originalScores[0].uprn) const b = await rankingModel.findById(originalScores[1].uprn) expect(+a.updatedRiskScore).to.eql(rt.a_score) expect(+b.updatedRiskScore).to.eql(rt.b_score) }) it('verify projected dates', async () => { const a = await rankingModel.findById(originalScores[0].uprn) const b = await rankingModel.findById(originalScores[1].uprn) // projected dates don't change expect(a.projectedHighRiskCrossover).to.eql(null) expect(moment(a.projectedReturnToOriginal).format('YYYY-MM-DD')).to.eql('2027-04-11') expect(moment(b.projectedHighRiskCrossover).format('YYYY-MM-DD')).to.eql('2019-03-22') expect(moment(b.projectedReturnToOriginal).format('YYYY-MM-DD')).to.eql('2022-12-17') }) }) } }) describe('stats view', () => { it('verify factory stats', async () => { const _statsData = await client.query('select * from test.ranking_uprns_stats where category = \'factory\'') const statsData = _statsData.rows[0] expect(+statsData.count).to.eql(13) expect(+statsData.mean).to.eql(68.08) expect(+statsData.stdev).to.eql(57.18) expect(+statsData.variance).to.eql(3270.07) }) it('verify hotel stats', async () => { const _statsData = await client.query('select * from test.ranking_uprns_stats where category = \'hotel\'') const statsData = _statsData.rows[0] expect(+statsData.count).to.eql(1) expect(+statsData.mean).to.eql(18) expect(+statsData.stdev).to.eql(0) expect(+statsData.variance).to.eql(0) }) it('verify shop stats', async () => { const _statsData = await client.query('select * from test.ranking_uprns_stats where category = \'shop\'') const statsData = _statsData.rows[0] expect(+statsData.count).to.eql(0) expect(+statsData.mean).to.eql(0) expect(+statsData.stdev).to.eql(0) expect(+statsData.variance).to.eql(0) }) }) describe('rankings view', () => { it('run set and refresh state machine', async () => { // lastEnforcement SATISFACTORY was changed from -16 to -8 const execDesc = await statebox.startExecution( { setRegistryKey: { key: 'test_factory', value: WEIGHTS_TO_UPDATE }, refreshRanking: { schema: 'test', category: 'factory' } }, SET_REFRESH_STATE_MACHINE_NAME, { sendResponse: 'COMPLETE' } ) expect(execDesc.status).to.eql('SUCCEEDED') }) it('verify view data has been adjusted', async () => { const a = await client.query(`select * from test.factory_scores where uprn = ${originalScores[0].uprn}`) const b = await client.query(`select * from test.factory_scores where uprn = ${originalScores[1].uprn}`) // Changes expect(a.rows[0].last_enforcement_score).to.eql(-8) expect(a.rows[0].original_risk_score).to.eql(originalScores[0].score + 8) // Remains the same expect(b.rows[0].last_enforcement_score).to.eql(64) expect(b.rows[0].original_risk_score).to.eql(originalScores[1].score) }) }) describe('refresh risk scores state machine', () => { it('refresh risk score', async () => { const execDesc = await statebox.startExecution( { schema: 'test', category: 'factory', uprn: 1 }, REFRESH_RISK_STATE_MACHINE_NAME, { sendResponse: 'COMPLETE' } ) expect(execDesc.status).to.eql('SUCCEEDED') }) it('refresh ranking without passing in any schema/category', async () => { const execDesc = await statebox.startExecution( {}, REFRESH_STATE_MACHINE_NAME, { sendResponse: 'COMPLETE' } ) expect(execDesc.status).to.eql('FAILED') expect(execDesc.errorCode).to.eql('noSchemaOrCategory') expect(execDesc.errorMessage).to.eql('No schema or category on input.') }) }) describe('clean up', () => { it('clean up test resources', () => { return sqlScriptRunner('./db-scripts/cleanup.sql', client) }) it('shutdown Tymly', async () => { await tymlyService.shutdown() }) it('close database connections', function (done) { client.end() done() }) }) })