UNPKG

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