From dc7a731ef41ff8641c939b4753b7e763f030fcbd Mon Sep 17 00:00:00 2001 From: maxcbc Date: Thu, 16 Nov 2023 16:08:56 +0000 Subject: [PATCH] FSR-1044 | Bug: 500 Error in station-csv Route Due to Invalid Telemetry Timestamps (#554) * Bug: 500 Error in station-csv Route Due to Invalid Telemetry Timestamps https://eaflood.atlassian.net/browse/FSR-1044 * Fix issue where forecast station csv would error if no telemetry is in DB Also fix issue where it would error if station doesn't exist, now will respond 404 --------- Co-authored-by: Max Bladen-Clark --- server/routes/station-csv.js | 105 +++++++++++++++-------------------- test/routes/station-csv.js | 30 ++++++++++ 2 files changed, 75 insertions(+), 60 deletions(-) diff --git a/server/routes/station-csv.js b/server/routes/station-csv.js index 5cd8fd912..95a28ab4b 100644 --- a/server/routes/station-csv.js +++ b/server/routes/station-csv.js @@ -1,5 +1,6 @@ const floodService = require('../services/flood') const moment = require('moment-timezone') +const boom = require('@hapi/boom') module.exports = { method: 'GET', @@ -12,85 +13,69 @@ module.exports = { const station = await floodService.getStationById(id, direction) + if (!station) { + return boom.notFound('Station not found') + } + const stationName = station.external_name.replace(/[^a-zA-Z0-9]+/g, '-') - const [telemetry, thresholds] = await Promise.all([ + const [rawTelemetry, thresholds] = await Promise.all([ floodService.getStationTelemetry(id, direction), floodService.getStationForecastThresholds(id) ]) - this.telemetry = telemetry - - this.telemetry.forEach(function (item) { - item.type = 'observed' - item.ts = moment.utc(item.ts).format() - }) + const telemetry = rawTelemetry.map(item => ({ + ...item, + type: 'observed', + ts: moment.utc(item.ts).format() + })) // Forecast station - if (thresholds.length) { - const values = await floodService.getStationForecastData(station.wiski_id) - - const forecast = values.SetofValues[0].Value + const includeForecast = !!thresholds.length + if (includeForecast && telemetry.length) { + const forecastStart = moment(telemetry[0].ts) + const truncateDate = moment(forecastStart).add(36, 'hours') + const { SetofValues: [{ Value: forecast } = { Value: [] }] = [] } = await floodService.getStationForecastData(station.wiski_id) - const forecastData = forecast.map(item => { + for (const item of forecast) { const itemDate = item.$.date const itemTime = item.$.time - const date = moment(`${itemDate} ${itemTime}`).format('YYYY-MM-DDTHH:mm') + 'Z' - return { ts: date, _: item._, type: 'forecast' } - }) - - // Truncate forecast data to be 36 hours from forecast creation - const forecastStart = moment(this.telemetry[0].ts) - - this.truncateDate = moment(forecastStart).add(36, 'hours') - - forecastData.forEach(function (value) { - value.ts = moment(value.ts) - - if (value.ts.isBefore(forecastStart) || value.ts.isAfter(this.truncateDate)) { - return + const date = moment(`${itemDate}T${itemTime}Z`) + + if (!date.isBefore(forecastStart) && !date.isAfter(truncateDate)) { + telemetry.push({ + ts: moment.utc(date).format(), + _: item._, + type: 'forecast' + }) } - value.ts = moment.utc(value.ts).format() - - this.telemetry.push(value) - }, this) + } } - this.telemetry.sort(function (a, b) { - return new Date(a.ts) - new Date(b.ts) - }) - - if (thresholds.length) { - this.csvString = [ - [ - 'Timestamp (UTC)', - 'Height (m)', - 'Type(observed/forecast)' - ], - ...this.telemetry.map(item => [ + const csvString = [ + [ + 'Timestamp (UTC)', + 'Height (m)', + 'Type(observed/forecast)' + ], + ...telemetry + .sort((a, b) => new Date(a.ts) - new Date(b.ts)) + .map(item => [ item.ts, item._, item.type ]) - ] - .map(e => e.join(',')) - .join('\n') - } else { - this.csvString = [ - [ - 'Timestamp (UTC)', - 'Height (m)' - ], - ...this.telemetry.map(item => [ - item.ts, - item._ - ]) - ] - .map(e => e.join(',')) - .join('\n') - } + ] + .reduce((acc, [ts, height, type]) => { + acc += `${ts},${height}` + if (includeForecast) { + acc += `,${type}` + } + return `${acc}\n` + }, '') + .trim() - const response = h.response(this.csvString) + const response = h.response(csvString) response.type('text/csv') response.header('Content-disposition', `attachment; filename=${stationName}-height-data.csv`) return response diff --git a/test/routes/station-csv.js b/test/routes/station-csv.js index ad72d36fd..0ff0b58cb 100644 --- a/test/routes/station-csv.js +++ b/test/routes/station-csv.js @@ -310,4 +310,34 @@ lab.experiment('Routes test - station-csv', () => { Code.expect(response.result).to.equal('Timestamp (UTC),Height (m)\n2020-03-13T01:30:00Z,1.354') Code.expect(response.headers['content-type']).to.include('text/csv') }) + + lab.test('GET /station-csv/1337 station not found', async () => { + const options = { + method: 'GET', + url: '/station-csv/1337' + } + const floodService = require('../../server/services/flood') + + const fakeStationData = () => {} + + const fakeTelemetryData = () => [ + { + ts: '2020-03-13T01:30Z', + _: 1.354, + err: false + } + ] + + const fakeThresholdsData = () => [] + + sandbox.stub(floodService, 'getStationById').callsFake(fakeStationData) + sandbox.stub(floodService, 'getStationTelemetry').callsFake(fakeTelemetryData) + sandbox.stub(floodService, 'getStationForecastThresholds').callsFake(fakeThresholdsData) + + const response = await server.inject(options) + Code.expect(response.statusCode).to.equal(404) + Code.expect(response.result.statusCode).to.equal(404) + Code.expect(response.result.error).to.equal('Not Found') + Code.expect(response.result.message).to.equal('Station not found') + }) })