diff --git a/package.json b/package.json index 9c305030..55b42b0b 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "tap-spec": "^4.1.1", "tape": "^4.8.0", "tape-promise": "^3.0.0", - "validate-fptf": "^1.2.1", + "validate-fptf": "^1.3.0", "vbb-stations-autocomplete": "^3.1.0" }, "scripts": { diff --git a/test/lib/util.js b/test/lib/util.js index fbd0d8c5..d5afed72 100644 --- a/test/lib/util.js +++ b/test/lib/util.js @@ -1,98 +1,7 @@ 'use strict' -const validateFptf = require('validate-fptf') const isRoughlyEqual = require('is-roughly-equal') const {DateTime} = require('luxon') -const isValidWGS84 = require('is-coordinates') - -const validateFptfWith = (t, item, allowedTypes, name) => { - try { - validateFptf.recurse(allowedTypes, item, name) - } catch (err) { - t.ifError(err) - } -} - -const assertValidStation = (t, s, coordsOptional = false) => { - validateFptfWith(t, s, ['station'], 'station') - - if (!coordsOptional || (s.location !== null && s.location !== undefined)) { - t.ok(s.location) - assertValidLocation(t, s.location, coordsOptional) - } -} - -const assertValidPoi = (t, p) => { - assertValidLocation(t, p, true) - - t.equal(typeof p.id, 'string') - t.equal(typeof p.name, 'string') - if (p.address !== null && p.address !== undefined) { - t.equal(typeof p.address, 'string') - t.ok(p.address) - } -} - -const assertValidAddress = (t, a) => { - assertValidLocation(t, a, true) - - t.equal(typeof a.address, 'string') -} - -const assertValidLocation = (t, l, coordsOptional = false) => { - t.equal(l.type, 'location') - if (l.name !== null && l.name !== undefined) { - t.equal(typeof l.name, 'string') - t.ok(l.name) - } - - if (l.address !== null && l.address !== undefined) { - t.equal(typeof l.address, 'string') - t.ok(l.address) - } - - const hasLatitude = l.latitude !== null && l.latitude !== undefined - const hasLongitude = l.longitude !== null && l.longitude !== undefined - if (!coordsOptional && hasLatitude) t.equal(typeof l.latitude, 'number') - if (!coordsOptional && hasLongitude) t.equal(typeof l.longitude, 'number') - if ((hasLongitude && !hasLatitude) || (hasLatitude && !hasLongitude)) { - t.fail('should have both .latitude and .longitude') - } - if (hasLatitude && hasLongitude) isValidWGS84([l.longitude, l.latitude]) - - if (!coordsOptional && l.altitude !== null && l.altitude !== undefined) { - t.equal(typeof l.altitude, 'number') - } -} - -const validLineModes = [ - 'train', 'bus', 'watercraft', 'taxi', 'gondola', 'aircraft', - 'car', 'bicycle', 'walking' -] - -const assertValidLine = (t, l) => { - validateFptfWith(t, l, ['line'], 'line') -} - -const isValidDateTime = (w) => { - return !Number.isNaN(+new Date(w)) -} - -const assertValidStopover = (t, s, coordsOptional = false) => { - if ('arrival' in s) t.ok(isValidDateTime(s.arrival)) - if ('departure' in s) t.ok(isValidDateTime(s.departure)) - if (s.arrivalDelay !== null && s.arrivalDelay !== undefined) { - t.equal(typeof s.arrivalDelay, 'number') - } - if (s.departureDelay !== null && s.departureDelay !== undefined) { - t.equal(typeof s.departureDelay, 'number') - } - if (!('arrival' in s) && !('departure' in s)) { - t.fail('stopover doesn\'t contain arrival or departure') - } - t.ok(s.station) - assertValidStation(t, s.station, coordsOptional) -} const hour = 60 * 60 * 1000 const week = 7 * 24 * hour @@ -104,55 +13,14 @@ const createWhen = (timezone, locale) => { locale, }).startOf('week').plus({weeks: 1, hours: 10}).toJSDate() } + const isValidWhen = (actual, expected) => { const ts = +new Date(actual) if (Number.isNaN(ts)) return false - return isRoughlyEqual(12 * hour, +expected, ts) -} - -const assertValidWhen = (t, actual, expected) => { - t.ok(isValidWhen(actual, expected), 'invalid when') -} - -const assertValidTicket = (t, ti) => { - t.strictEqual(typeof ti.name, 'string') - t.ok(ti.name.length > 0) - if (ti.price !== null) { - t.strictEqual(typeof ti.price, 'number') - t.ok(ti.price > 0) - } - if (ti.amount !== null) { - t.strictEqual(typeof ti.amount, 'number') - t.ok(ti.amount > 0) - } - - if ('bike' in ti) t.strictEqual(typeof ti.bike, 'boolean') - if ('shortTrip' in ti) t.strictEqual(typeof ti.shortTrip, 'boolean') - if ('group' in ti) t.strictEqual(typeof ti.group, 'boolean') - if ('fullDay' in ti) t.strictEqual(typeof ti.fullDay, 'boolean') - - if (ti.tariff !== null) { - t.strictEqual(typeof ti.tariff, 'string') - t.ok(ti.tariff.length > 0) - } - if (ti.coverage !== null) { - t.strictEqual(typeof ti.coverage, 'string') - t.ok(ti.coverage.length > 0) - } - if (ti.variant !== null) { - t.strictEqual(typeof ti.variant, 'string') - t.ok(ti.variant.length > 0) - } + // the timestamps might be from long-distance trains + return isRoughlyEqual(14 * hour, +expected, ts) } module.exports = { - assertValidStation, - assertValidPoi, - assertValidAddress, - assertValidLocation, - assertValidLine, - isValidDateTime, - assertValidStopover, - hour, createWhen, isValidWhen, assertValidWhen, - assertValidTicket + hour, createWhen, isValidWhen } diff --git a/test/lib/validate-fptf-with.js b/test/lib/validate-fptf-with.js new file mode 100644 index 00000000..09624a3e --- /dev/null +++ b/test/lib/validate-fptf-with.js @@ -0,0 +1,24 @@ +'use strict' + +const {defaultValidators, createRecurse} = require('validate-fptf') +const validators = require('./validators') + +const create = (cfg, customValidators = {}) => { + const vals = Object.assign({}, defaultValidators) + for (let key of Object.keys(validators)) { + vals[key] = validators[key](cfg) + } + Object.assign(vals, customValidators) + const recurse = createRecurse(vals) + + const validateFptfWith = (t, item, allowedTypes, name) => { + try { + recurse(allowedTypes, item, name) + } catch (err) { + t.ifError(err) // todo: improve error logging + } + } + return validateFptfWith +} + +module.exports = create diff --git a/test/lib/validators.js b/test/lib/validators.js new file mode 100644 index 00000000..1ab565f5 --- /dev/null +++ b/test/lib/validators.js @@ -0,0 +1,193 @@ +'use strict' + +const a = require('assert') +const {defaultValidators} = require('validate-fptf') +const validateRef = require('validate-fptf/lib/reference') +const validateDate = require('validate-fptf/lib/date') + +const {isValidWhen} = require('./util') + +const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o) +const is = val => val !== null && val !== undefined + +const createValidateStation = (cfg) => { + const validateStation = (validate, s, name = 'station') => { + defaultValidators.station(validate, s, name) + + if (!cfg.stationCoordsOptional) { + a.ok(is(s.location), `missing ${name}.location`) + } + a.ok(isObj(s.products), name + '.products must be an object') + for (let product of cfg.products) { + const msg = name + `.products[${product.id}] must be a boolean` + a.strictEqual(typeof s.products[product.id], 'boolean', msg) + } + + if ('lines' in s) { + a.ok(Array.isArray(s.lines), name + `.lines must be an array`) + for (let i = 0; i < s.lines.length; i++) { + validate(['line'], s.lines[i], name + `.lines[${i}]`) + } + } + } + return validateStation +} + +const validatePoi = (validate, poi, name = 'location') => { + defaultValidators.location(validate, poi, name) + validateRef(poi.id, name + '.id') + a.ok(poi.name, name + '.name must not be empty') +} + +const validateAddress = (validate, addr, name = 'location') => { + defaultValidators.location(validate, addr, name) + a.strictEqual(typeof addr.address, 'string', name + '.address must be a string') + a.ok(addr.address, name + '.address must not be empty') +} + +const validateLocation = (validate, loc, name = 'location') => { + a.ok(isObj(loc), name + ' must be an object') + if (loc.type === 'station') validate(['station'], loc, name) + else if ('id' in loc) validatePoi(validate, loc, name) + else validateAddress(validate, loc, name) +} + +const createValidateLine = (cfg) => { + const validLineModes = [] + for (let product of cfg.products) { + if (!validLineModes.includes(product.mode)) { + validLineModes.push(product.mode) + } + } + + const validateLine = (validate, line, name = 'line') => { + defaultValidators.line(validate, line, name) + a.ok(validLineModes.includes(line.mode)) + } + return validateLine +} + +const createValidateStopover = (cfg) => { + const validateStopover = (validate, s, name = 'stopover') => { + if (is(s.arrival)) { + validateDate(s.arrival, name + '.arrival') + a.ok(isValidWhen(s.arrival, cfg.when), name + '.arrival is invalid') + } + if (is(s.departure)) { + validateDate(s.departure, name + '.departure') + a.ok(isValidWhen(s.departure, cfg.when), name + '.departure is invalid') + } + if (!is(s.arrival) && !is(s.departure)) { + asser.fail(name + ' contains neither arrival nor departure') + } + + if (is(s.arrivalDelay)) { + const msg = name + '.arrivalDelay must be a number' + a.strictEqual(typeof s.arrivalDelay, 'number', msg) + } + if (is(s.departureDelay)) { + const msg = name + '.departureDelay must be a number' + a.strictEqual(typeof s.departureDelay, 'number', msg) + } + + validate(['station'], s.station, name + '.station') + } + return validateStopover +} + +const validateTicket = (validate, ti, name = 'ticket') => { + a.strictEqual(typeof ti.name, 'string', name + '.name must be a string') + a.ok(ti.name, name + '.name must not be empty') + + if (is(ti.price)) { + a.strictEqual(typeof ti.price, 'number', name + '.price must be a number') + a.ok(ti.price > 0, name + '.price must be >0') + } + if (is(ti.amount)) { + a.strictEqual(typeof ti.amount, 'number', name + '.amount must be a number') + a.ok(ti.amount > 0, name + '.amount must be >0') + } + + // todo: move to VBB tests + if ('bike' in ti) { + a.strictEqual(typeof ti.bike, 'boolean', name + '.bike must be a boolean') + } + if ('shortTrip' in ti) { + a.strictEqual(typeof ti.shortTrip, 'boolean', name + '.shortTrip must be a boolean') + } + if ('group' in ti) { + a.strictEqual(typeof ti.group, 'boolean', name + '.group must be a boolean') + } + if ('fullDay' in ti) { + a.strictEqual(typeof ti.fullDay, 'boolean', name + '.fullDay must be a boolean') + } + if ('tariff' in ti) { + a.strictEqual(typeof ti.tariff, 'string', name + '.tariff must be a string') + a.ok(ti.tariff, name + '.tariff must not be empty') + } + if ('coverage' in ti) { + a.strictEqual(typeof ti.coverage, 'string', name + '.coverage must be a string') + a.ok(ti.coverage, name + '.coverage must not be empty') + } + if ('variant' in ti) { + a.strictEqual(typeof ti.variant, 'string', name + '.variant must be a string') + a.ok(ti.variant, name + '.variant must not be empty') + } +} + +const createValidateJourneyLeg = (cfg) => { + const validateJourneyLeg = (validate, leg, name = 'journeyLeg') => { + const withFakeScheduleAndOperator = Object.assign({ + schedule: 'foo', // todo: let hafas-client parse a schedule ID + operator: 'bar' // todo: let hafas-client parse the operator + }, leg) + defaultValidators.journeyLeg(validate, withFakeScheduleAndOperator, name) + + if (leg.arrival !== null) { + const msg = name + '.arrival is invalid' + a.ok(isValidWhen(leg.arrival, cfg.when), msg) + } + if (leg.departure !== null) { + const msg = name + '.departure is invalid' + a.ok(isValidWhen(leg.departure, cfg.when), msg) + } + + if ('passed' in leg) { + a.ok(Array.isArray(leg.passed), name + '.passed must be an array') + a.ok(leg.passed.length > 0, name + '.passed must not be empty') + + for (let i = 0; i < leg.passed.length; i++) { + validate('stopover', leg.passed[i], name + `.passed[${i}]`) + } + } + } + return validateJourneyLeg +} + +const validateJourney = (validate, j, name = 'journey') => { + const withFakeId = Object.assign({ + id: 'foo' // todo: let hafas-client parse a journey ID + }, j) + defaultValidators.journey(validate, withFakeId, name) + + if ('tickets' in j) { + a.ok(Array.isArray(j.tickets), name + '.tickets must be an array') + a.ok(j.tickets.length > 0, name + '.tickets must not be empty') + + for (let i = 0; i < j.tickets.length; i++) { + validate(['ticket'], j.tickets[i], name + `.tickets[${i}]`) + } + } +} + +module.exports = { + station: createValidateStation, + location: () => validateLocation, + poi: () => validatePoi, + address: () => validateAddress, + line: createValidateLine, + stopover: createValidateStopover, + ticket: () => validateTicket, + journeyLeg: createValidateJourneyLeg, + journey: () => validateJourney +}