UNPKG

13.3 kBJavaScriptView Raw
1/* eslint-env mocha */
2'use strict'
3
4const chai = require('chai')
5const expect = chai.expect
6const tymly = require('@wmfs/tymly')
7const path = require('path')
8const HlPgClient = require('@wmfs/hl-pg-client')
9const process = require('process')
10const sqlScriptRunner = require('./fixtures/sql-script-runner.js')
11const moment = require('moment')
12const WEIGHTS_TO_UPDATE = require('./fixtures/updated-weights.json')
13
14process.on('unhandledRejection', (reason, p) => {
15 console.log('Unhandled Rejection at: Promise', p, 'reason:', reason)
16 // application specific logging, throwing an error, or other logic here
17})
18
19describe('Tests the Ranking State Resource', function () {
20 this.timeout(15000)
21
22 const REFRESH_ALL_STATE_MACHINE_NAME = 'wmfs_refreshAll_1_0'
23 const REFRESH_STATE_MACHINE_NAME = 'test_refreshRanking_1_0'
24 const SET_REFRESH_STATE_MACHINE_NAME = 'wmfs_setAndRefresh_1_0'
25 const REFRESH_RISK_STATE_MACHINE_NAME = 'test_refreshRiskScore_1_0'
26
27 const originalScores = []
28
29 let statebox, tymlyService, rankingService, rankingModel, refreshModel
30 const AuditDate = () => moment([2018, 5, 18])
31 let TestTimestamp
32
33 // explicitly opening a db connection as seom setup needs to be carried
34 // out before tymly can be started up
35 const pgConnectionString = process.env.PG_CONNECTION_STRING
36 const client = new HlPgClient(pgConnectionString)
37
38 before(function () {
39 if (process.env.PG_CONNECTION_STRING && !/^postgres:\/\/[^:]+:[^@]+@(?:localhost|127\.0\.0\.1).*$/.test(process.env.PG_CONNECTION_STRING)) {
40 console.log(`Skipping tests due to unsafe PG_CONNECTION_STRING value (${process.env.PG_CONNECTION_STRING})`)
41 this.skip()
42 }
43 })
44
45 describe('setup', () => {
46 it('create test resources', () => {
47 return sqlScriptRunner('./db-scripts/setup.sql', client)
48 })
49
50 it('start tymly', done => {
51 tymly.boot(
52 {
53 pluginPaths: [
54 path.resolve(__dirname, './..'),
55 require.resolve('@wmfs/tymly-pg-plugin')
56 ],
57 blueprintPaths: [
58 path.resolve(__dirname, './fixtures/blueprint'),
59 path.resolve(__dirname, '../lib/blueprints/ranking-blueprint')
60 ],
61 config: {}
62 },
63 (err, tymlyServices) => {
64 expect(err).to.eql(null)
65 tymlyService = tymlyServices.tymly
66 rankingService = tymlyServices.rankings
67 statebox = tymlyServices.statebox
68 rankingModel = tymlyServices.storage.models.test_rankingUprns
69 refreshModel = tymlyServices.storage.models.wmfs_rankingRefreshStatus
70 tymlyServices.timestamp.timeProvider = {
71 today () {
72 return moment(TestTimestamp)
73 }
74 } // debug provider
75
76 done()
77 }
78 )
79 })
80
81 it('should refresh all rankings', async () => {
82 const execDesc = await statebox.startExecution({}, REFRESH_ALL_STATE_MACHINE_NAME, { sendResponse: 'COMPLETE' })
83 expect(execDesc.status).to.eql('SUCCEEDED')
84 })
85
86 it('verify factory data', async () => {
87 const viewData = await client.query('select * from test.factory_scores')
88 const rankingData = await rankingModel.find({
89 where: {
90 rankingName: { equals: 'factory' }
91 }
92 })
93
94 const _statsData = await client.query('select * from test.ranking_uprns_stats where category = \'factory\'')
95 const statsData = _statsData.rows[0]
96
97 const mergedData = rankingData
98 .map((r, i) => {
99 return {
100 uprn: r.uprn,
101 score: viewData.rows[i].updated_risk_score || viewData.rows[i].original_risk_score,
102 range: r.range
103 }
104 })
105 .sort((b, c) => {
106 return b.score - c.score
107 })
108
109 expect(mergedData[0].range).to.eql('very-low')
110 expect(mergedData[mergedData.length - 1].range).to.eql('very-high')
111
112 originalScores.push(mergedData[0], mergedData[mergedData.length - 1])
113
114 expect(statsData.ranges.veryLow.lowerBound).to.eql(0)
115 expect(statsData.ranges.veryHigh.upperBound).to.eql(mergedData[mergedData.length - 1].score)
116 expect(+statsData.count).to.eql(13)
117 expect(+statsData.mean).to.eql(58.23)
118 expect(+statsData.stdev).to.eql(31.45)
119 })
120
121 it('test the service function to find range', async () => {
122 const viewRes = await client.query('select * from test.factory_scores;')
123 const { original_risk_score: originalRiskScore, updated_risk_score: updatedRiskScore } = viewRes.rows[0]
124 const score = updatedRiskScore || originalRiskScore
125 const range = await rankingService.findRange('test.ranking_uprns_stats', 'factory', score)
126 expect(range).to.eql('veryHigh')
127 })
128
129 it('test the service function to find distribution', async () => {
130 const viewRes = await client.query('select * from test.factory_scores;')
131 const { original_risk_score: originalRiskScore, updated_risk_score: updatedRiskScore } = viewRes.rows[0]
132 const score = updatedRiskScore || originalRiskScore
133 const dist = await rankingService.findDistribution('test.ranking_uprns_stats', 'factory', score)
134 expect(dist).to.eql(0.0016)
135 })
136 })
137
138 describe('calculated risk scores', () => {
139 describe('update with fire safety level', () => {
140 it('update the ranking model with fire safety level', async () => {
141 await rankingModel.upsert({
142 uprn: originalScores[0].uprn,
143 rankingName: 'factory',
144 fsManagement: 'high',
145 lastAuditDate: AuditDate(),
146 lastEnforcementAction: 'SATISFACTORY'
147 }, {})
148 await rankingModel.upsert({
149 uprn: originalScores[1].uprn,
150 rankingName: 'factory',
151 fsManagement: 'veryLow',
152 lastAuditDate: AuditDate(),
153 lastEnforcementAction: 'ENFORCEMENT'
154 }, {})
155
156 // Changing fsManagement and lastEnforcementAction affects the risk score so let's get the updated one
157 const a = await client.query(`select original_risk_score from test.factory_scores where uprn = ${originalScores[0].uprn}`)
158 const b = await client.query(`select original_risk_score from test.factory_scores where uprn = ${originalScores[1].uprn}`)
159
160 // fsManagement score for high = 20
161 // lastEnforcementAction score for SATISFACTORY = -16
162 // A should change by (20 + -16 = 4)
163 expect(a.rows[0].original_risk_score).to.eql(originalScores[0].score + 4)
164
165 // fsManagement score for veryLow = 60
166 // lastEnforcementAction score for ENFORCEMENT = 64
167 // B should change by (60 + 64 = 124)
168 expect(b.rows[0].original_risk_score).to.eql(originalScores[1].score + 124)
169
170 // Update originalScores
171 originalScores[0].score = a.rows[0].original_risk_score
172 originalScores[1].score = b.rows[0].original_risk_score
173 })
174 })
175
176 const refreshTests = [
177 {
178 days: 0,
179 a_score: 13,
180 b_score: 62.62
181 },
182 {
183 days: 365,
184 a_score: 15.34,
185 b_score: 146.42
186 },
187 {
188 days: 730,
189 a_score: 17.54,
190 b_score: 212.45
191 },
192 {
193 days: 1460,
194 a_score: 21.10,
195 b_score: 243.92
196 }
197 ]
198
199 // a = high, exp = -0.001, score = 26
200 // b = veryLow, exp = -0.004, score = 246
201
202 // mean = 68.07692
203 // stdev = 57.18453
204
205 // ---- Day 0 ----
206 // medium risk goes to half original score on day 0
207 // high risk goes to (mean + stddev) / 2 on day 0
208 // Growth curve intersection
209 // a = 4394 days
210 // b = 830 days
211 // Expected score after 365
212 // a = 26 / ( 1 + ( 81 * ( e ^ ( (365+4394) * -0.001 ) ) ) ) = 15.34
213 // b = 246 / ( 1 + ( 81 * ( e ^ ( (365+830) * -0.004 ) ) ) ) = 146.42
214 // Expected score after 730
215 // a = 26 / ( 1 + ( 81 * ( e ^ ( (730+4394) * -0.001 ) ) ) ) = 17.54
216 // b = 246 / ( 1 + ( 81 * ( e ^ ( (730+830) * -0.004 ) ) ) ) = 212.45
217 // Expected score after 1460
218 // a = 26 / ( 1 + ( 81 * ( e ^ ( (1460+4394) * -0.001 ) ) ) ) = 21.10
219 // b = 246 / ( 1 + ( 81 * ( e ^ ( (1460+830) * -0.004 ) ) ) ) = 243.92
220
221 for (const rt of refreshTests) {
222 describe(`audit was ${rt.days} ago`, () => {
223 it('refresh ranking', async () => {
224 TestTimestamp = AuditDate().add(rt.days, 'days')
225
226 const execDesc = await statebox.startExecution(
227 { schema: 'test', category: 'factory' },
228 REFRESH_STATE_MACHINE_NAME,
229 { sendResponse: 'COMPLETE' }
230 )
231 expect(execDesc.status).to.eql('SUCCEEDED')
232 })
233 it('check ranking refresh status model', async () => {
234 const refreshRecords = await refreshModel.find({ where: { key: { equals: 'test_factory' } } })
235 expect(refreshRecords[0].status).to.eql('ENDED')
236 })
237 it(`verify calculated risk score on day ${rt.days}`, async () => {
238 const a = await rankingModel.findById(originalScores[0].uprn)
239 const b = await rankingModel.findById(originalScores[1].uprn)
240
241 expect(+a.updatedRiskScore).to.eql(rt.a_score)
242 expect(+b.updatedRiskScore).to.eql(rt.b_score)
243 })
244
245 it('verify projected dates', async () => {
246 const a = await rankingModel.findById(originalScores[0].uprn)
247 const b = await rankingModel.findById(originalScores[1].uprn)
248
249 // projected dates don't change
250 expect(a.projectedHighRiskCrossover).to.eql(null)
251 expect(moment(a.projectedReturnToOriginal).format('YYYY-MM-DD')).to.eql('2027-04-11')
252
253 expect(moment(b.projectedHighRiskCrossover).format('YYYY-MM-DD')).to.eql('2019-03-22')
254 expect(moment(b.projectedReturnToOriginal).format('YYYY-MM-DD')).to.eql('2022-12-17')
255 })
256 })
257 }
258 })
259
260 describe('stats view', () => {
261 it('verify factory stats', async () => {
262 const _statsData = await client.query('select * from test.ranking_uprns_stats where category = \'factory\'')
263 const statsData = _statsData.rows[0]
264
265 expect(+statsData.count).to.eql(13)
266 expect(+statsData.mean).to.eql(68.08)
267 expect(+statsData.stdev).to.eql(57.18)
268 expect(+statsData.variance).to.eql(3270.07)
269 })
270 it('verify hotel stats', async () => {
271 const _statsData = await client.query('select * from test.ranking_uprns_stats where category = \'hotel\'')
272 const statsData = _statsData.rows[0]
273
274 expect(+statsData.count).to.eql(1)
275 expect(+statsData.mean).to.eql(18)
276 expect(+statsData.stdev).to.eql(0)
277 expect(+statsData.variance).to.eql(0)
278 })
279 it('verify shop stats', async () => {
280 const _statsData = await client.query('select * from test.ranking_uprns_stats where category = \'shop\'')
281 const statsData = _statsData.rows[0]
282
283 expect(+statsData.count).to.eql(0)
284 expect(+statsData.mean).to.eql(0)
285 expect(+statsData.stdev).to.eql(0)
286 expect(+statsData.variance).to.eql(0)
287 })
288 })
289
290 describe('rankings view', () => {
291 it('run set and refresh state machine', async () => {
292 // lastEnforcement SATISFACTORY was changed from -16 to -8
293 const execDesc = await statebox.startExecution(
294 {
295 setRegistryKey: {
296 key: 'test_factory',
297 value: WEIGHTS_TO_UPDATE
298 },
299 refreshRanking: {
300 schema: 'test',
301 category: 'factory'
302 }
303 },
304 SET_REFRESH_STATE_MACHINE_NAME,
305 { sendResponse: 'COMPLETE' }
306 )
307 expect(execDesc.status).to.eql('SUCCEEDED')
308 })
309
310 it('verify view data has been adjusted', async () => {
311 const a = await client.query(`select * from test.factory_scores where uprn = ${originalScores[0].uprn}`)
312 const b = await client.query(`select * from test.factory_scores where uprn = ${originalScores[1].uprn}`)
313
314 // Changes
315 expect(a.rows[0].last_enforcement_score).to.eql(-8)
316 expect(a.rows[0].original_risk_score).to.eql(originalScores[0].score + 8)
317
318 // Remains the same
319 expect(b.rows[0].last_enforcement_score).to.eql(64)
320 expect(b.rows[0].original_risk_score).to.eql(originalScores[1].score)
321 })
322 })
323
324 describe('refresh risk scores state machine', () => {
325 it('refresh risk score', async () => {
326 const execDesc = await statebox.startExecution(
327 {
328 schema: 'test',
329 category: 'factory',
330 uprn: 1
331 },
332 REFRESH_RISK_STATE_MACHINE_NAME,
333 { sendResponse: 'COMPLETE' }
334 )
335 expect(execDesc.status).to.eql('SUCCEEDED')
336 })
337
338 it('refresh ranking without passing in any schema/category', async () => {
339 const execDesc = await statebox.startExecution(
340 {},
341 REFRESH_STATE_MACHINE_NAME,
342 { sendResponse: 'COMPLETE' }
343 )
344
345 expect(execDesc.status).to.eql('FAILED')
346 expect(execDesc.errorCode).to.eql('noSchemaOrCategory')
347 expect(execDesc.errorMessage).to.eql('No schema or category on input.')
348 })
349 })
350
351 describe('clean up', () => {
352 it('clean up test resources', () => {
353 return sqlScriptRunner('./db-scripts/cleanup.sql', client)
354 })
355
356 it('shutdown Tymly', async () => {
357 await tymlyService.shutdown()
358 })
359
360 it('close database connections', function (done) {
361 client.end()
362 done()
363 })
364 })
365})