weacast/weacast-probe

View on GitHub
test/index.test.js

Summary

Maintainability
A
0 mins
Test Coverage
import path from 'path'
import fs from 'fs-extra'
import _ from 'lodash'
import chai, { util, expect } from 'chai'
import chailint from 'chai-lint'
import spies from 'chai-spies'
import core, { weacast } from 'weacast-core'
import gfs from 'weacast-gfs'
import probe from '../src'

describe('weacast-probe', () => {
  let app, uService, vService, probeService, probeResultService,
    geojson, probeId, spyProbe, spyUpdate, firstForecastTime, nextForecastTime
  const probeOptions = {
    forecast: 'gfs-world',
    elements: ['u-wind', 'v-wind'],
    featureId: ['properties.Airport', 'properties.Ident', 'properties.RevCode']
  }
  before(() => {
    chailint(chai, util)
    chai.use(spies)
    geojson = fs.readJsonSync(path.join(__dirname, 'data', 'runways.geojson'))
    Object.assign(geojson, probeOptions)

    app = weacast()
    return app.db.connect()
    .then(_ => {
      // Disable TTL because we keep past forecast times so that the number of forecasts is predictable for tests
      // but otherwise MongoDB will remove them automatically
      app.db._db.executeDbAdminCommand({ setParameter: 1, ttlMonitorEnabled: false })
    })
  })

  it('is CommonJS compatible', () => {
    expect(typeof probe).to.equal('function')
  })

  it('registers the probes service', async () => {
    app.configure(core)
    await app.configure(gfs)
    uService = app.getService('gfs-world/u-wind')
    expect(uService).toExist()
    vService = app.getService('gfs-world/v-wind')
    expect(vService).toExist()
    app.configure(probe)
    probeService = app.getService('probes')
    expect(probeService).toExist()
    probeResultService = app.getService('probe-results')
    expect(probeResultService).toExist()
    spyProbe = chai.spy.on(probeService, 'probeForecastTime')
    spyUpdate = chai.spy.on(probeService, 'updateFeaturesInDatabase')
  })
  // Let enough time to process
  .timeout(5000)

  it('performs element download process', async () => {
    // Clear any previous data
    uService.Model.remove()
    vService.Model.remove()
    fs.emptyDirSync(app.get('forecastPath'))
    // download both elements in parallel
    return Promise.all([
      uService.updateForecastData(),
      vService.updateForecastData()
    ])
  })
  // Let enough time to download a couple of data
  .timeout(60000)

  it('performs probing element on-demand at forecast time', async () => {
    const forecasts = await uService.find({ paginate: false })
    // We should have 2 forecast times
    expect(forecasts.length).to.equal(2)
    firstForecastTime = forecasts[0].forecastTime
    nextForecastTime = firstForecastTime.clone()
    nextForecastTime.add({ hours: 3 })
    const data = await probeService.create(geojson, { query: { forecastTime: firstForecastTime } })

    expect(spyProbe).to.have.been.called()
    expect(spyUpdate).not.to.have.been.called()
    // This will insure spies are properly reset before jumping to next test due to async ops
    spyProbe.reset()
    spyUpdate.reset()
    // 3 features with data for the forecast times
    expect(data.features.length).to.equal(3)
    data.features.forEach(feature => {
      expect(feature.forecastTime).toExist()
      expect(feature.forecastTime.format()).to.equal(firstForecastTime.format())
      expect(feature.runTime).toExist()
      expect(feature.properties['u-wind']).toExist()
      expect(feature.properties['v-wind']).toExist()
      expect(typeof feature.properties['u-wind']).to.equal('number')
      expect(typeof feature.properties['v-wind']).to.equal('number')
      // Test if derived direction values are also present
      expect(feature.properties['windDirection']).toExist()
      expect(feature.properties['windSpeed']).toExist()
      expect(typeof feature.properties['windDirection']).to.equal('number')
      expect(typeof feature.properties['windSpeed']).to.equal('number')
    })
    // For debug purpose only
    // fs.outputJsonSync(path.join(__dirname, 'data', 'runways-probe.geojson'), data.layer)
  })
  // Let enough time to process
  .timeout(10000)

  it('performs probing element on-demand at specific location for forecast time range', async () => {
    const geometry = {
      type: 'Point',
      coordinates: [ 1.5, 43 ]
    }
    const query = {
      forecastTime: {
        $gte: firstForecastTime,
        $lte: nextForecastTime
      },
      geometry: {
        $geoIntersects: {
          $geometry: geometry
        }
      }
    }

    const data = await probeService.create(Object.assign({}, probeOptions), { query })
    expect(data.features.length).to.equal(1)
    const feature = data.features[0]
    expect(spyProbe).to.have.been.called()
    expect(spyUpdate).not.to.have.been.called()
    // This will insure spies are properly reset before jumping to next test due to async ops
    spyProbe.reset()
    spyUpdate.reset()
    expect(feature.forecastTime).toExist()
    expect(feature.forecastTime['u-wind']).toExist()
    expect(feature.forecastTime['v-wind']).toExist()
    expect(feature.forecastTime['u-wind'].length).to.equal(2)
    expect(feature.forecastTime['v-wind'].length).to.equal(2)
    expect(feature.forecastTime['u-wind'][0].isBefore(feature.forecastTime['u-wind'][1])).beTrue()
    expect(feature.forecastTime['v-wind'][0].isBefore(feature.forecastTime['v-wind'][1])).beTrue()
    expect(feature.runTime).toExist()
    expect(feature.runTime['u-wind']).toExist()
    expect(feature.runTime['v-wind']).toExist()
    expect(feature.runTime['u-wind'].length).to.equal(2)
    expect(feature.runTime['v-wind'].length).to.equal(2)
    expect(feature.runTime['u-wind'][0].isSameOrBefore(feature.runTime['u-wind'][1])).beTrue()
    expect(feature.runTime['v-wind'][0].isSameOrBefore(feature.runTime['v-wind'][1])).beTrue()
    expect(feature.properties['u-wind']).toExist()
    expect(feature.properties['v-wind']).toExist()
    expect(feature.properties['u-wind'].length).to.equal(2)
    expect(feature.properties['v-wind'].length).to.equal(2)
    expect(typeof feature.properties['u-wind'][0]).to.equal('number')
    expect(typeof feature.properties['v-wind'][0]).to.equal('number')
    expect(typeof feature.properties['u-wind'][1]).to.equal('number')
    expect(typeof feature.properties['v-wind'][1]).to.equal('number')
    // Test if derived direction values are also present
    expect(feature.forecastTime['windDirection']).toExist()
    expect(feature.forecastTime['windSpeed']).toExist()
    expect(feature.forecastTime['windDirection'].length).to.equal(2)
    expect(feature.forecastTime['windSpeed'].length).to.equal(2)
    expect(feature.forecastTime['windDirection'][0].isBefore(feature.forecastTime['windDirection'][1])).beTrue()
    expect(feature.forecastTime['windSpeed'][0].isBefore(feature.forecastTime['windSpeed'][1])).beTrue()
    expect(feature.runTime['windDirection']).toExist()
    expect(feature.runTime['windSpeed']).toExist()
    expect(feature.runTime['windDirection'].length).to.equal(2)
    expect(feature.runTime['windSpeed'].length).to.equal(2)
    expect(feature.runTime['windDirection'][0].isSameOrBefore(feature.runTime['windDirection'][1])).beTrue()
    expect(feature.runTime['windSpeed'][0].isSameOrBefore(feature.runTime['windSpeed'][1])).beTrue()
    expect(feature.properties['windDirection']).toExist()
    expect(feature.properties['windSpeed']).toExist()
    expect(feature.properties['windDirection'].length).to.equal(2)
    expect(feature.properties['windSpeed'].length).to.equal(2)
    expect(typeof feature.properties['windDirection'][0]).to.equal('number')
    expect(typeof feature.properties['windSpeed'][0]).to.equal('number')
    expect(typeof feature.properties['windDirection'][1]).to.equal('number')
    expect(typeof feature.properties['windSpeed'][1]).to.equal('number')
  })
  // Let enough time to process
  .timeout(10000)

  it('performs probing single element on-demand at specific location for forecast time range', async () => {
    const geometry = {
      type: 'Point',
      coordinates: [ 1.5, 43 ]
    }
    const query = {
      forecastTime: {
        $gte: firstForecastTime,
        $lte: nextForecastTime
      },
      geometry: {
        $geoIntersects: {
          $geometry: geometry
        }
      }
    }

    const data = await probeService.create(Object.assign({ elements: ['u-wind'] }, _.omit(probeOptions, ['elements'])), { query })
    expect(data.features.length).to.equal(1)
    const feature = data.features[0]
    expect(spyProbe).to.have.been.called()
    expect(spyUpdate).not.to.have.been.called()
    // This will insure spies are properly reset before jumping to next test due to async ops
    spyProbe.reset()
    spyUpdate.reset()
    expect(feature.forecastTime).toExist()
    expect(feature.forecastTime['u-wind']).toExist()
    expect(feature.forecastTime['v-wind']).beUndefined()
    expect(feature.forecastTime['u-wind'].length).to.equal(2)
    expect(feature.forecastTime['u-wind'][0].isBefore(feature.forecastTime['u-wind'][1])).beTrue()
    expect(feature.runTime).toExist()
    expect(feature.runTime['u-wind']).toExist()
    expect(feature.runTime['v-wind']).beUndefined()
    expect(feature.runTime['u-wind'].length).to.equal(2)
    expect(feature.runTime['u-wind'][0].isSameOrBefore(feature.runTime['u-wind'][1])).beTrue()
    expect(feature.properties['u-wind']).toExist()
    expect(feature.properties['v-wind']).beUndefined()
    expect(feature.properties['u-wind'].length).to.equal(2)
    expect(typeof feature.properties['u-wind'][0]).to.equal('number')
    expect(typeof feature.properties['u-wind'][1]).to.equal('number')
  })
  // Let enough time to process
  .timeout(10000)

  it('performs probing element on-demand at specific location for forecast time range without aggregation', async () => {
    const geometry = {
      type: 'Point',
      coordinates: [ 1.5, 43 ]
    }
    const query = {
      forecastTime: {
        $gte: firstForecastTime,
        $lte: nextForecastTime
      },
      geometry: {
        $geoIntersects: {
          $geometry: geometry
        }
      },
      aggregate: false
    }

    const data = await probeService.create(Object.assign({}, probeOptions), { query })
    expect(data.features.length).to.equal(2)
    const features = data.features
    expect(spyProbe).to.have.been.called()
    expect(spyUpdate).not.to.have.been.called()
    // This will insure spies are properly reset before jumping to next test due to async ops
    spyProbe.reset()
    spyUpdate.reset()
    expect(features[0].forecastTime).toExist()
    expect(features[1].forecastTime).toExist()
    expect(features[0].forecastTime.isBefore(features[1].forecastTime)).beTrue()
    expect(features[0].runTime).toExist()
    expect(features[1].runTime).toExist()
    expect(features[0].runTime.isSameOrBefore(features[1].runTime)).beTrue()
    expect(features[0].properties['u-wind']).toExist()
    expect(features[0].properties['v-wind']).toExist()
    expect(features[1].properties['u-wind']).toExist()
    expect(features[1].properties['v-wind']).toExist()
    expect(typeof features[0].properties['u-wind']).to.equal('number')
    expect(typeof features[0].properties['v-wind']).to.equal('number')
    expect(typeof features[1].properties['u-wind']).to.equal('number')
    expect(typeof features[1].properties['v-wind']).to.equal('number')
  })
  // Let enough time to process
  .timeout(10000)

  it('creates probing stream on element', async () => {
    const data = await probeService.create(geojson)
    probeId = data._id
    // No update on creation
    expect(spyProbe).not.to.have.been.called()
    expect(spyUpdate).not.to.have.been.called()
    // Features filtered on creation
    expect(data.features).beUndefined()
  })

  it('performs probing element on forecast update', (done) => {
    uService.Model.drop()
    .then(_ => uService.updateForecastData())
    // We need to register to results update event to know when to proceed
    let updateCount = 0
    probeService.on('results', async event => {
      if (event.probe._id === probeId) updateCount++
      // 3 features over 2 forecast times
      if (updateCount === 2) {
        expect(spyProbe).to.have.been.called()
        expect(spyUpdate).to.have.been.called()
        // This will insure spies are properly reset before jumping to next test due to async ops
        spyProbe.reset()
        spyUpdate.reset()
        const features = await probeResultService.find({ paginate: false, query: { probeId } })
        // Test we have generated new results
        expect(features.length).to.equal(6)
        features.forEach(feature => {
          expect(feature.forecastTime).toExist()
          expect(feature.runTime).toExist()
          expect(feature.properties['u-wind']).toExist()
          expect(feature.properties['v-wind']).toExist()
          expect(typeof feature.properties['u-wind']).to.equal('number')
          expect(typeof feature.properties['v-wind']).to.equal('number')
          // Test if derived direction values are also present
          expect(feature.properties['windDirection']).toExist()
          expect(feature.properties['windSpeed']).toExist()
          expect(typeof feature.properties['windDirection']).to.equal('number')
          expect(typeof feature.properties['windSpeed']).to.equal('number')
        })
        done()
      }
    })
  })
  // Let enough time to download a couple of data
  .timeout(30000)

  it('performs spatial filtering on probe results', async () => {
    let geometry = {
      $near: {
        $geometry: {
          type: 'Point',
          coordinates: geojson.features[0].geometry.coordinates
        },
        $maxDistance: 10000 // 10 Kms around
      }
    }
    let features = await probeResultService.find({
      paginate: false,
      query: {
        probeId,
        geometry,
        forecastTime: firstForecastTime
      }
    })
    // All features should be covered
    expect(features.length).to.equal(3)
    geometry.$near.$maxDistance = 1 // 1 meter around
    features = await probeResultService.find({
      paginate: false,
      query: {
        probeId,
        geometry,
        forecastTime: firstForecastTime
      }
    })
    // No feature except the target one should be covered
    expect(features.length).to.equal(1)
    geometry.$near.$maxDistance = 10000 // 10 Kms around
    // Check as well using shortcut
    features = await probeResultService.find({
      paginate: false,
      query: {
        probeId,
        longitude: geometry.$near.$geometry.coordinates[0],
        latitude: geometry.$near.$geometry.coordinates[1],
        distance: geometry.$near.$maxDistance,
        forecastTime: firstForecastTime
      }
    })
    // All features should be covered
    expect(features.length).to.equal(3)
    geometry.$near.$maxDistance = 1 // 1 meter around
    features = await probeResultService.find({
      paginate: false,
      query: {
        probeId,
        longitude: geometry.$near.$geometry.coordinates[0],
        latitude: geometry.$near.$geometry.coordinates[1],
        distance: geometry.$near.$maxDistance,
        forecastTime: firstForecastTime
      }
    })
    // No feature except the target one should be covered
    expect(features.length).to.equal(1)
  })

  it('performs element value filtering on probe results', async () => {
    let query = { probeId }
    query['properties.windSpeed'] = { $gt: -1, $lt: 0 }
    let features = await probeResultService.find({
      paginate: false,
      query
    })
    // None should be covered since speed is always >= 0
    expect(features.length).to.equal(0)
    query['properties.windSpeed'] = { $gt: 0, $lt: 99999 }
    features = await probeResultService.find({
      paginate: false,
      query
    })
    // All should be covered sicne speed is always >= 0
    expect(features.length).to.equal(6)
  })

  it('performs element aggregation on probe results for forecast time range', async () => {
    let query = {
      probeId,
      forecastTime: {
        $gte: firstForecastTime,
        $lte: nextForecastTime
      },
      'properties.Ident': 'RW30',
      $groupBy: 'properties.Ident',
      $aggregate: ['windDirection', 'windSpeed']
    }
    let features = await probeResultService.find({
      paginate: false,
      query
    })
    expect(features.length).to.equal(1)
    const feature = features[0]
    expect(feature.forecastTime['windDirection']).toExist()
    expect(feature.forecastTime['windSpeed']).toExist()
    expect(feature.forecastTime['windDirection'].length).to.equal(2)
    expect(feature.forecastTime['windSpeed'].length).to.equal(2)
    expect(feature.forecastTime['windDirection'][0].isBefore(feature.forecastTime['windDirection'][1])).beTrue()
    expect(feature.forecastTime['windSpeed'][0].isBefore(feature.forecastTime['windSpeed'][1])).beTrue()
    expect(feature.runTime['windDirection']).toExist()
    expect(feature.runTime['windSpeed']).toExist()
    expect(feature.runTime['windDirection'].length).to.equal(2)
    expect(feature.runTime['windSpeed'].length).to.equal(2)
    expect(feature.runTime['windDirection'][0].isSameOrBefore(feature.runTime['windDirection'][1])).beTrue()
    expect(feature.runTime['windSpeed'][0].isSameOrBefore(feature.runTime['windSpeed'][1])).beTrue()
    expect(feature.properties['windSpeed'].length).to.equal(2)
    expect(feature.properties['windDirection'].length).to.equal(2)
    expect(typeof feature.properties['windDirection'][0]).to.equal('number')
    expect(typeof feature.properties['windSpeed'][0]).to.equal('number')
    expect(typeof feature.properties['windDirection'][1]).to.equal('number')
    expect(typeof feature.properties['windSpeed'][1]).to.equal('number')
  })

  it('performs probing results removal on probe removal', async () => {
    await probeService.remove(probeId)
    const response = await probeResultService.find({ query: { probeId } })
    // Nothing should remain
    expect(response.data.length).to.equal(0)
  })

  // Cleanup
  after(() => {
    app.db._db.executeDbAdminCommand({ setParameter: 1, ttlMonitorEnabled: true })
    app.getService('forecasts').Model.drop()
    probeService.removeAllListeners()
    probeService.Model.drop()
    probeResultService.Model.drop()
    uService.Model.drop()
    vService.Model.drop()
    fs.removeSync(app.get('forecastPath'))
  })
})