mirror of
https://github.com/public-transport/db-vendo-client.git
synced 2025-02-22 22:59:35 +02:00
bahn.de boards (#12)
* parse bahn.de boards * add optional chaining in line.js * unit tests for bahn.de boards * fix product check in line.js for bahn.de boards * add integration tests for bahn.de boards * allow letting hafas decide the amount of vias * split dbweb and dbregioguide profiles; add db profile * commit location-filter.js (forgot that in the last commit) * simplify how db profile works * remove `ezGleis` from coalesce for scheduled platform * un-break parsing of remarks * determine fahrtNr by removing all non-digits * employ enrichStations for board stop property * prevent timeouts in dbweb e2e test from calling `end()` twice * use promises in dbweb e2e tests when waiting for enrichStations to work * replace vias option with stopovers option for dbweb profile; enrich stations when only name is known * change dbweb-departures test covering enrichStation feature for stop and stopovers * remove check for not existing option * move verkehrsmittel.name in front of verkehrsmittel.langText when parsing name in line.js
This commit is contained in:
parent
1e7977a8bb
commit
c671e995cb
43 changed files with 4958 additions and 94 deletions
3
index.js
3
index.js
|
@ -32,6 +32,7 @@ const loadEnrichedStationData = (profile) => new Promise((resolve, reject) => {
|
|||
readStations.full()
|
||||
.on('data', (station) => {
|
||||
items[station.id] = station;
|
||||
items[station.name] = station;
|
||||
})
|
||||
.once('end', () => {
|
||||
if (profile.DEBUG) {
|
||||
|
@ -105,7 +106,7 @@ const createClient = (profile, userAgent, opt = {}) => {
|
|||
const {res} = await profile.request({profile, opt}, userAgent, req);
|
||||
|
||||
const ctx = {profile, opt, common, res};
|
||||
const results = (res[resultsField] || res.items || res.bahnhofstafelAbfahrtPositionen || res.bahnhofstafelAnkunftPositionen)
|
||||
const results = (res[resultsField] || res.items || res.bahnhofstafelAbfahrtPositionen || res.bahnhofstafelAnkunftPositionen || res.entries)
|
||||
.map(res => parse(ctx, res)); // TODO sort?, slice
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,26 +1,67 @@
|
|||
import {createRequire} from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const baseProfile = require('./base.json');
|
||||
const dbnavBase = require('../dbnav/base.json');
|
||||
const dbregioguideBase = require('../dbregioguide/base.json');
|
||||
import {products} from '../../lib/products.js';
|
||||
import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js';
|
||||
|
||||
// journeys()
|
||||
import {formatJourneysReq} from '../dbnav/journeys-req.js';
|
||||
const {journeysEndpoint} = dbnavBase;
|
||||
|
||||
// refreshJourneys()
|
||||
import {formatRefreshJourneyReq} from '../dbnav/journeys-req.js';
|
||||
const {refreshJourneysEndpointTickets, refreshJourneysEndpointPolyline} = dbnavBase;
|
||||
|
||||
// locations()
|
||||
import {formatLocationsReq} from '../dbnav/locations-req.js';
|
||||
import {formatLocationFilter} from '../dbnav/location-filter.js';
|
||||
const {locationsEndpoint} = dbnavBase;
|
||||
|
||||
// stop()
|
||||
import {formatStopReq} from '../dbnav/stop-req.js';
|
||||
import {parseStop} from '../dbnav/parse-stop.js';
|
||||
const {stopEndpoint} = dbnavBase;
|
||||
|
||||
// nearby()
|
||||
import {formatNearbyReq} from '../dbnav/nearby-req.js';
|
||||
const {nearbyEndpoint} = dbnavBase;
|
||||
|
||||
// trip()
|
||||
import {formatTripReq} from './trip-req.js';
|
||||
import {formatLocationFilter} from './location-filter.js';
|
||||
import {formatLocationsReq} from './locations-req.js';
|
||||
const tripEndpoint_dbnav = dbnavBase.tripEndpoint;
|
||||
const tripEndpoint_dbregioguide = dbregioguideBase.tripEndpoint;
|
||||
|
||||
// arrivals(), departures()
|
||||
const {boardEndpoint} = dbregioguideBase;
|
||||
|
||||
const profile = {
|
||||
...baseProfile,
|
||||
locale: 'de-DE',
|
||||
timezone: 'Europe/Berlin',
|
||||
|
||||
products,
|
||||
|
||||
formatJourneysReq,
|
||||
journeysEndpoint,
|
||||
|
||||
formatRefreshJourneyReq,
|
||||
refreshJourneysEndpointTickets, refreshJourneysEndpointPolyline,
|
||||
|
||||
formatLocationsReq, formatLocationFilter,
|
||||
locationsEndpoint,
|
||||
|
||||
formatStopReq, parseStop,
|
||||
stopEndpoint,
|
||||
|
||||
formatNearbyReq,
|
||||
nearbyEndpoint,
|
||||
|
||||
formatTripReq,
|
||||
formatLocationsReq,
|
||||
formatLocationFilter,
|
||||
tripEndpoint_dbnav, tripEndpoint_dbregioguide,
|
||||
|
||||
boardEndpoint,
|
||||
};
|
||||
|
||||
|
||||
export {
|
||||
profile,
|
||||
};
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import {formatTripReq as hafasFormatTripReq} from '../../format/trip-req.js';
|
||||
import {formatTripReq as hafasFormatTripReq} from '../dbnav/trip-req.js';
|
||||
import {formatTripReq as risTripReq} from '../dbregioguide/trip-req.js';
|
||||
|
||||
|
||||
const formatTripReq = ({profile, opt}, id) => {
|
||||
const _profile = {...profile};
|
||||
if (id.includes('#')) {
|
||||
return hafasFormatTripReq({profile, opt}, id);
|
||||
_profile['tripEndpoint'] = profile.tripEndpoint_dbnav;
|
||||
return hafasFormatTripReq({profile: _profile, opt}, id);
|
||||
}
|
||||
return {
|
||||
endpoint: profile.regioGuideTripEndpoint,
|
||||
path: id,
|
||||
method: 'get',
|
||||
};
|
||||
|
||||
_profile['tripEndpoint'] = profile.tripEndpoint_dbregioguide;
|
||||
return risTripReq({profile: _profile, opt}, id);
|
||||
};
|
||||
|
||||
export {
|
||||
|
|
5
p/dbregioguide/base.json
Normal file
5
p/dbregioguide/base.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"tripEndpoint": "https://regio-guide.de/@prd/zupo-travel-information/api/public/ri/journey/",
|
||||
"boardEndpoint": "https://regio-guide.de/@prd/zupo-travel-information/api/public/ri/board/",
|
||||
"defaultLanguage": "en"
|
||||
}
|
18
p/dbregioguide/index.js
Normal file
18
p/dbregioguide/index.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import {createRequire} from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
const baseProfile = require('./base.json');
|
||||
import {products} from '../../lib/products.js';
|
||||
import {formatTripReq} from './trip-req.js';
|
||||
|
||||
const profile = {
|
||||
...baseProfile,
|
||||
locale: 'de-DE',
|
||||
timezone: 'Europe/Berlin',
|
||||
|
||||
products,
|
||||
formatTripReq,
|
||||
};
|
||||
|
||||
export {
|
||||
profile,
|
||||
};
|
11
p/dbregioguide/trip-req.js
Normal file
11
p/dbregioguide/trip-req.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
const formatTripReq = ({profile, opt}, id) => {
|
||||
return {
|
||||
endpoint: profile.tripEndpoint,
|
||||
path: id,
|
||||
method: 'get',
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
formatTripReq,
|
||||
};
|
|
@ -5,7 +5,6 @@
|
|||
"locationsEndpoint": "https://int.bahn.de/web/api/reiseloesung/orte",
|
||||
"nearbyEndpoint": "https://int.bahn.de/web/api/reiseloesung/orte/nearby",
|
||||
"tripEndpoint": "https://int.bahn.de/web/api/reiseloesung/fahrt",
|
||||
"regioGuideTripEndpoint": "https://regio-guide.de/@prd/zupo-travel-information/api/public/ri/journey/",
|
||||
"boardEndpoint": "https://regio-guide.de/@prd/zupo-travel-information/api/public/ri/board/",
|
||||
"boardEndpoint": "https://int.bahn.de/web/api/reiseloesung/",
|
||||
"defaultLanguage": "en"
|
||||
}
|
29
p/dbweb/index.js
Normal file
29
p/dbweb/index.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import {createRequire} from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const baseProfile = require('./base.json');
|
||||
import {products} from '../../lib/products.js';
|
||||
import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js';
|
||||
import {formatLocationFilter} from './location-filter.js';
|
||||
import {formatLocationsReq} from './locations-req.js';
|
||||
import {formatStationBoardReq} from './station-board-req.js';
|
||||
|
||||
const profile = {
|
||||
...baseProfile,
|
||||
locale: 'de-DE',
|
||||
timezone: 'Europe/Berlin',
|
||||
|
||||
products,
|
||||
|
||||
formatJourneysReq,
|
||||
formatRefreshJourneyReq,
|
||||
formatLocationsReq,
|
||||
formatLocationFilter,
|
||||
formatStationBoardReq,
|
||||
|
||||
departuresGetPasslist: true,
|
||||
};
|
||||
|
||||
export {
|
||||
profile,
|
||||
};
|
20
p/dbweb/station-board-req.js
Normal file
20
p/dbweb/station-board-req.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
const formatStationBoardReq = (ctx, station, type) => {
|
||||
const {profile, opt} = ctx;
|
||||
|
||||
return {
|
||||
endpoint: profile.boardEndpoint,
|
||||
path: type === 'departures' ? 'abfahrten' : 'ankuenfte',
|
||||
query: {
|
||||
ortExtId: station,
|
||||
zeit: profile.formatTimeOfDay(profile, opt.when),
|
||||
datum: profile.formatDate(profile, opt.when),
|
||||
mitVias: opt.stopovers || undefined,
|
||||
verkehrsmittel: profile.formatProductsFilter(ctx, opt.products || {}),
|
||||
},
|
||||
method: 'GET',
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
formatStationBoardReq,
|
||||
};
|
|
@ -10,17 +10,17 @@ const createParseArrOrDep = (prefix) => {
|
|||
const {profile, opt} = ctx;
|
||||
const cancelled = profile.parseCancelled(d);
|
||||
const res = {
|
||||
tripId: d.journeyID || d.train?.journeyId || d.zuglaufId,
|
||||
stop: profile.parseLocation(ctx, d.station || d.abfrageOrt),
|
||||
tripId: d.journeyID || d.journeyId || d.train?.journeyId || d.zuglaufId,
|
||||
stop: profile.parseLocation(ctx, d.station || d.abfrageOrt || {bahnhofsId: d.bahnhofsId}),
|
||||
...profile.parseWhen(
|
||||
ctx,
|
||||
null,
|
||||
d.timeSchedule || d.time || d.abgangsDatum || d.ankunftsDatum,
|
||||
d.timeType != 'SCHEDULE' ? d.timePredicted || d.time || d.ezAbgangsDatum || d.ezAnkunftsDatum : null,
|
||||
d.timeSchedule || d.time || d.zeit || d.abgangsDatum || d.ankunftsDatum,
|
||||
d.timeType != 'SCHEDULE' ? d.timePredicted || d.time || d.ezZeit || d.ezAbgangsDatum || d.ezAnkunftsDatum : null,
|
||||
cancelled),
|
||||
...profile.parsePlatform(ctx, d.platformSchedule || d.platform || d.gleis, d.platformPredicted || d.platform || d.ezGleis, cancelled),
|
||||
// prognosisType: TODO
|
||||
direction: d.transport?.direction?.stopPlaces?.length > 0 && profile.parseStationName(ctx, d.transport?.direction?.stopPlaces[0].name) || profile.parseStationName(ctx, d.destination?.name || d.richtung) || null,
|
||||
direction: d.transport?.direction?.stopPlaces?.length > 0 && profile.parseStationName(ctx, d.transport?.direction?.stopPlaces[0].name) || profile.parseStationName(ctx, d.destination?.name || d.richtung || d.terminus) || null,
|
||||
provenance: profile.parseStationName(ctx, d.transport?.origin?.name || d.origin?.name || d.abgangsOrt?.name) || null,
|
||||
line: profile.parseLine(ctx, d) || null,
|
||||
remarks: [],
|
||||
|
@ -39,7 +39,18 @@ const createParseArrOrDep = (prefix) => {
|
|||
if (opt.remarks) {
|
||||
res.remarks = profile.parseRemarks(ctx, d);
|
||||
}
|
||||
// TODO opt.stopovers
|
||||
|
||||
if (opt.stopovers && Array.isArray(d.ueber)) {
|
||||
const stopovers = d.ueber
|
||||
.map(viaName => profile.parseStopover(ctx, {name: viaName}, null));
|
||||
|
||||
if (prefix === ARRIVAL) {
|
||||
res.previousStopovers = stopovers;
|
||||
} else if (prefix === DEPARTURE) {
|
||||
res.nextStopovers = stopovers;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@ import slugg from 'slugg';
|
|||
|
||||
const parseLine = (ctx, p) => {
|
||||
const profile = ctx.profile;
|
||||
const fahrtNr = p.verkehrsmittel?.nummer || p.transport?.number || p.train?.no || p.no || ((p.risZuglaufId || '') + '_').split('_')[1] || p.verkehrsmittelNummer || ((p.mitteltext || '') + ' ').split(' ')[1] || ((p.zugName || '') + ' ').split(' ')[1];
|
||||
const fahrtNr = p.verkehrsmittel?.nummer || p.transport?.number || p.train?.no || p.no || ((p.risZuglaufId || '') + '_').split('_')[1] || p.verkehrsmittelNummer || (p.verkehrmittel?.langText || p.verkehrsmittel?.langText || p.mitteltext || p.zugName || '').replace(/\D/g, '');
|
||||
const res = {
|
||||
type: 'line',
|
||||
id: slugg(p.verkehrsmittel?.langText || p.transport?.journeyDescription || p.risZuglaufId || p.train && p.train.category + ' ' + p.train.lineName + ' ' + p.train.no || p.no && p.name + ' ' + p.no || p.langtext || p.mitteltext || p.zugName), // TODO terrible
|
||||
id: slugg(p.verkehrsmittel?.langText || p.verkehrmittel?.langText || p.transport?.journeyDescription || p.risZuglaufId || p.train && p.train.category + ' ' + p.train.lineName + ' ' + p.train.no || p.no && p.name + ' ' + p.no || p.langtext || p.mitteltext || p.zugName), // TODO terrible
|
||||
fahrtNr: String(fahrtNr),
|
||||
name: p.verkehrsmittel?.langText || p.verkehrsmittel?.name || p.zugName || p.transport && p.transport.category + ' ' + p.transport.line || p.train && p.train.category + ' ' + p.train.lineName || p.name || p.mitteltext || p.langtext,
|
||||
name: p.verkehrsmittel?.name || p.verkehrsmittel?.langText || p.verkehrmittel?.name || p.verkehrmittel?.langText || p.zugName || p.transport && p.transport.category + ' ' + p.transport.line || p.train && p.train.category + ' ' + p.train.lineName || p.name || p.mitteltext || p.langtext,
|
||||
public: true,
|
||||
};
|
||||
|
||||
|
@ -15,8 +15,8 @@ const parseLine = (ctx, p) => {
|
|||
if (adminCode) {
|
||||
res.adminCode = adminCode;
|
||||
}
|
||||
res.productName = p.verkehrsmittel?.kurzText || p.transport?.category || p.train?.category || p.category || p.kurztext;
|
||||
const foundProduct = profile.products.find(pp => pp.vendo == p.verkehrsmittel?.produktGattung || pp.ris == p.transport?.type || pp.ris == p.train?.type || pp.ris == p.type || pp.ris_alt == p.train?.type || pp.ris_alt == p.type || pp.dbnav_short == p.produktGattung);
|
||||
res.productName = p.verkehrsmittel?.kurzText || p.verkehrmittel?.kurzText || p.transport?.category || p.train?.category || p.category || p.kurztext;
|
||||
const foundProduct = profile.products.find(pp => pp.vendo == p.verkehrsmittel?.produktGattung || pp.vendo == p.verkehrmittel?.produktGattung || pp.ris == p.transport?.type || pp.ris == p.train?.type || pp.ris == p.type || pp.ris_alt == p.train?.type || pp.ris_alt == p.type || pp.dbnav_short == p.produktGattung);
|
||||
res.mode = foundProduct?.mode;
|
||||
res.product = foundProduct?.id;
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ const parseLocation = (ctx, l) => {
|
|||
const lid = parse(l.id || l.locationId, {delimiter: '@'});
|
||||
const res = {
|
||||
type: 'location',
|
||||
id: (l.extId || l.evaNr || lid.L || l.evaNumber || l.evaNo || '').replace(leadingZeros, '') || null,
|
||||
id: (l.extId || l.evaNr || lid.L || l.evaNumber || l.evaNo || l.bahnhofsId || '').replace(leadingZeros, '') || null,
|
||||
};
|
||||
const name = l.name || lid.O;
|
||||
|
||||
|
@ -29,12 +29,14 @@ const parseLocation = (ctx, l) => {
|
|||
}
|
||||
|
||||
// addresses and pois might also have fake evaNr sometimes!
|
||||
if (l.type === STATION || l.extId || l.evaNumber || l.evaNo || lid.A == '1') {
|
||||
if (l.type === STATION || l.extId || l.evaNumber || l.evaNo || lid.A == '1' || l.bahnhofsId) {
|
||||
let stop = {
|
||||
type: 'station',
|
||||
id: res.id,
|
||||
name: name,
|
||||
};
|
||||
if (name) {
|
||||
stop.name = name;
|
||||
}
|
||||
if ('number' === typeof res.latitude) {
|
||||
stop.location = res; // todo: remove `.id`
|
||||
}
|
||||
|
@ -57,6 +59,16 @@ const parseLocation = (ctx, l) => {
|
|||
return stop;
|
||||
}
|
||||
|
||||
if (name && common?.locations?.[name] && res.id === null) {
|
||||
delete res.type;
|
||||
delete res.id;
|
||||
|
||||
return {
|
||||
...common.locations[name],
|
||||
...res,
|
||||
};
|
||||
}
|
||||
|
||||
res.name = name;
|
||||
if (l.type === ADDRESS || lid.A == '2') {
|
||||
res.address = name;
|
||||
|
|
|
@ -13,6 +13,7 @@ const parseRemarks = (ctx, ref) => {
|
|||
ref.hims || [],
|
||||
ref.serviceNotiz && [ref.serviceNotiz] || [],
|
||||
ref.messages || [],
|
||||
ref.meldungen || [],
|
||||
ref.meldungenAsObject || [],
|
||||
ref.attributNotizen || [],
|
||||
ref.attributes || [],
|
||||
|
@ -202,11 +203,16 @@ const parseRemarks = (ctx, ref) => {
|
|||
*/
|
||||
|
||||
const parseCancelled = (ref) => {
|
||||
return ref.canceled || ref.cancelled || ref.journeyCancelled || (ref.risNotizen || ref.echtzeitNotizen) && Boolean((ref.risNotizen || ref.echtzeitNotizen).find(r => r.key == 'text.realtime.stop.cancelled'
|
||||
return ref.canceled
|
||||
|| ref.cancelled
|
||||
|| ref.journeyCancelled
|
||||
|| (ref.risNotizen || ref.echtzeitNotizen || ref.meldungen) && Boolean(
|
||||
(ref.risNotizen || ref.echtzeitNotizen || ref.meldungen).find(r => r.key == 'text.realtime.stop.cancelled'
|
||||
|| r.type == 'HALT_AUSFALL'
|
||||
|| r.text == 'Halt entfällt'
|
||||
|| r.text == 'Stop cancelled',
|
||||
));
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
// todo: use import assertions once they're supported by Node.js & ESLint
|
||||
// https://github.com/tc39/proposal-import-assertions
|
||||
import {createRequire} from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
import tap from 'tap';
|
||||
|
||||
import {createClient} from '../index.js';
|
||||
import {profile as rawProfile} from '../p/db/index.js';
|
||||
const res = require('./fixtures/db-departures-regio-guide.json');
|
||||
import {dbDepartures as expected} from './fixtures/db-departures-regio-guide.js';
|
||||
|
||||
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
|
||||
const {profile} = client;
|
||||
|
||||
const opt = {
|
||||
direction: null,
|
||||
duration: 10,
|
||||
linesOfStops: true,
|
||||
remarks: true,
|
||||
stopovers: true,
|
||||
includeRelatedStations: true,
|
||||
when: '2019-08-19T20:30:00+02:00',
|
||||
products: {},
|
||||
};
|
||||
|
||||
tap.test('parses a regio-guide departure correctly', (t) => {
|
||||
const ctx = {profile, opt, common: null, res};
|
||||
const departures = res.items.map(d => profile.parseDeparture(ctx, d));
|
||||
|
||||
t.same(departures, expected);
|
||||
t.end();
|
||||
});
|
|
@ -6,9 +6,9 @@ const require = createRequire(import.meta.url);
|
|||
import tap from 'tap';
|
||||
|
||||
import {createClient} from '../index.js';
|
||||
import {profile as rawProfile} from '../p/db/index.js';
|
||||
const res = require('./fixtures/db-trip-regio-guide.json');
|
||||
import {dbTrip as expected} from './fixtures/db-trip-regio-guide.js';
|
||||
import {profile as rawProfile} from '../p/dbregioguide/index.js';
|
||||
const res = require('./fixtures/dbregioguide-trip.json');
|
||||
import {dbTrip as expected} from './fixtures/dbregioguide-trip.js';
|
||||
|
||||
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
|
||||
const {profile} = client;
|
|
@ -6,7 +6,7 @@ const require = createRequire(import.meta.url);
|
|||
import tap from 'tap';
|
||||
|
||||
import {createClient} from '../index.js';
|
||||
import {profile as rawProfile} from '../p/db/index.js';
|
||||
import {profile as rawProfile} from '../p/dbweb/index.js';
|
||||
const res = require('./fixtures/dbris-arrivals.json');
|
||||
import {dbArrivals as expected} from './fixtures/dbris-arrivals.js';
|
||||
|
||||
|
@ -18,7 +18,7 @@ const opt = {
|
|||
duration: 10,
|
||||
linesOfStops: true,
|
||||
remarks: true,
|
||||
stopovers: true,
|
||||
stopovers: false,
|
||||
includeRelatedStations: true,
|
||||
when: '2019-08-19T20:30:00+02:00',
|
||||
products: {},
|
||||
|
|
92
test/dbweb-departures.js
Normal file
92
test/dbweb-departures.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
// todo: use import assertions once they're supported by Node.js & ESLint
|
||||
// https://github.com/tc39/proposal-import-assertions
|
||||
import {createRequire} from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
import tap from 'tap';
|
||||
|
||||
import {createClient} from '../index.js';
|
||||
import {profile as rawProfile} from '../p/dbweb/index.js';
|
||||
const res = require('./fixtures/dbweb-departures.json');
|
||||
import {dbwebDepartures as expected} from './fixtures/dbweb-departures.js';
|
||||
|
||||
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: true});
|
||||
const {profile} = client;
|
||||
|
||||
const opt = {
|
||||
direction: null,
|
||||
duration: null,
|
||||
linesOfStops: true,
|
||||
remarks: true,
|
||||
stopovers: true,
|
||||
includeRelatedStations: true,
|
||||
when: '2025-02-08T15:37:00',
|
||||
products: {},
|
||||
};
|
||||
|
||||
const osterburken = {
|
||||
type: 'station',
|
||||
id: '8000295',
|
||||
name: 'Osterburken',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8000295',
|
||||
latitude: 49.42992,
|
||||
longitude: 9.422996,
|
||||
},
|
||||
products: {
|
||||
nationalExpress: false,
|
||||
national: false,
|
||||
regionalExp: false,
|
||||
regional: true,
|
||||
suburban: true,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
subway: false,
|
||||
tram: false,
|
||||
taxi: true,
|
||||
},
|
||||
weight: 5.6,
|
||||
};
|
||||
|
||||
const moeckmuehl = {
|
||||
type: 'station',
|
||||
id: '8004050',
|
||||
name: 'Möckmühl',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8004050',
|
||||
latitude: 49.321187,
|
||||
longitude: 9.357977,
|
||||
},
|
||||
products: {
|
||||
nationalExpress: false,
|
||||
national: false,
|
||||
regionalExp: false,
|
||||
regional: true,
|
||||
suburban: false,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
subway: false,
|
||||
tram: false,
|
||||
taxi: false,
|
||||
},
|
||||
distance: 2114,
|
||||
weight: 6.45,
|
||||
};
|
||||
|
||||
const common = {
|
||||
locations: {
|
||||
Osterburken: osterburken,
|
||||
8000295: osterburken,
|
||||
Möckmühl: moeckmuehl,
|
||||
},
|
||||
};
|
||||
|
||||
tap.test('parses a dbweb departure correctly', (t) => {
|
||||
const ctx = {profile, opt, common, res};
|
||||
const departures = res.entries.map(d => profile.parseDeparture(ctx, d));
|
||||
|
||||
t.same(departures, expected);
|
||||
t.end();
|
||||
});
|
|
@ -6,9 +6,9 @@ const require = createRequire(import.meta.url);
|
|||
import tap from 'tap';
|
||||
|
||||
import {createClient} from '../index.js';
|
||||
import {profile as rawProfile} from '../p/db/index.js';
|
||||
const res = require('./fixtures/db-journey.json');
|
||||
import {dbJourney as expected} from './fixtures/db-journey.js';
|
||||
import {profile as rawProfile} from '../p/dbweb/index.js';
|
||||
const res = require('./fixtures/dbweb-journey.json');
|
||||
import {dbwebJourney as expected} from './fixtures/dbweb-journey.js';
|
||||
|
||||
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
|
||||
const {profile} = client;
|
||||
|
@ -31,7 +31,7 @@ const opt = {
|
|||
products: {},
|
||||
};
|
||||
|
||||
tap.test('parses a journey correctly (DB)', (t) => { // TODO DEVI leg
|
||||
tap.test('parses a dbweb journey correctly', (t) => { // TODO DEVI leg
|
||||
const ctx = {profile, opt, common: null, res};
|
||||
const journey = profile.parseJourney(ctx, res.verbindungen[0]);
|
||||
|
|
@ -6,9 +6,9 @@ const require = createRequire(import.meta.url);
|
|||
import tap from 'tap';
|
||||
|
||||
import {createClient} from '../index.js';
|
||||
import {profile as rawProfile} from '../p/db/index.js';
|
||||
const res = require('./fixtures/db-refresh-journey.json');
|
||||
import {dbJourney as expected} from './fixtures/db-refresh-journey.js';
|
||||
import {profile as rawProfile} from '../p/dbweb/index.js';
|
||||
const res = require('./fixtures/dbweb-refresh-journey.json');
|
||||
import {dbJourney as expected} from './fixtures/dbweb-refresh-journey.js';
|
||||
|
||||
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
|
||||
const {profile} = client;
|
||||
|
@ -31,7 +31,7 @@ const opt = {
|
|||
products: {},
|
||||
};
|
||||
|
||||
tap.test('parses a refresh journey correctly (DB)', (t) => {
|
||||
tap.test('parses a refresh journey correctly (dbweb)', (t) => {
|
||||
const ctx = {profile, opt, common: null, res};
|
||||
const journey = profile.parseJourney(ctx, res.verbindungen[0]);
|
||||
|
|
@ -6,9 +6,9 @@ const require = createRequire(import.meta.url);
|
|||
import tap from 'tap';
|
||||
|
||||
import {createClient} from '../index.js';
|
||||
import {profile as rawProfile} from '../p/db/index.js';
|
||||
const res = require('./fixtures/db-trip.json');
|
||||
import {dbTrip as expected} from './fixtures/db-trip.js';
|
||||
import {profile as rawProfile} from '../p/dbweb/index.js';
|
||||
const res = require('./fixtures/dbweb-trip.json');
|
||||
import {dbwebTrip as expected} from './fixtures/dbweb-trip.js';
|
||||
|
||||
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
|
||||
const {profile} = client;
|
|
@ -476,7 +476,6 @@ tap.test('locations named Jungfernheide', async (t) => {
|
|||
t.end();
|
||||
});
|
||||
|
||||
/*
|
||||
tap.test('stop', async (t) => {
|
||||
const s = await client.stop(regensburgHbf);
|
||||
|
||||
|
@ -486,6 +485,7 @@ tap.test('stop', async (t) => {
|
|||
t.end();
|
||||
});
|
||||
|
||||
/*
|
||||
tap.test('line with additionalName', async (t) => {
|
||||
const {departures} = await client.departures(potsdamHbf, {
|
||||
when,
|
||||
|
|
499
test/e2e/dbregioguide.js
Normal file
499
test/e2e/dbregioguide.js
Normal file
|
@ -0,0 +1,499 @@
|
|||
import tap from 'tap';
|
||||
import isRoughlyEqual from 'is-roughly-equal';
|
||||
|
||||
import {createWhen} from './lib/util.js';
|
||||
import {createClient} from '../../index.js';
|
||||
import {profile as dbProfile} from '../../p/dbregioguide/index.js';
|
||||
import {
|
||||
createValidateStation,
|
||||
createValidateTrip,
|
||||
} from './lib/validators.js';
|
||||
import {createValidateFptfWith as createValidate} from './lib/validate-fptf-with.js';
|
||||
import {testJourneysStationToStation} from './lib/journeys-station-to-station.js';
|
||||
import {testJourneysStationToAddress} from './lib/journeys-station-to-address.js';
|
||||
import {testJourneysStationToPoi} from './lib/journeys-station-to-poi.js';
|
||||
import {testEarlierLaterJourneys} from './lib/earlier-later-journeys.js';
|
||||
import {testLegCycleAlternatives} from './lib/leg-cycle-alternatives.js';
|
||||
import {testRefreshJourney} from './lib/refresh-journey.js';
|
||||
import {journeysFailsWithNoProduct} from './lib/journeys-fails-with-no-product.js';
|
||||
import {testDepartures} from './lib/departures.js';
|
||||
import {testArrivals} from './lib/arrivals.js';
|
||||
import {testJourneysWithDetour} from './lib/journeys-with-detour.js';
|
||||
|
||||
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o);
|
||||
const minute = 60 * 1000;
|
||||
|
||||
const T_MOCK = 1747040400 * 1000; // 2025-05-12T08:00:00+01:00
|
||||
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
||||
|
||||
const cfg = {
|
||||
when,
|
||||
stationCoordsOptional: true, // TODO
|
||||
products: dbProfile.products,
|
||||
minLatitude: 46.673100,
|
||||
maxLatitude: 55.030671,
|
||||
minLongitude: 6.896517,
|
||||
maxLongitude: 16.180237,
|
||||
};
|
||||
|
||||
const validate = createValidate(cfg);
|
||||
|
||||
const assertValidPrice = (t, p) => {
|
||||
t.ok(p);
|
||||
if (p.amount !== null) {
|
||||
t.equal(typeof p.amount, 'number');
|
||||
t.ok(p.amount > 0);
|
||||
}
|
||||
if (p.hint !== null) {
|
||||
t.equal(typeof p.hint, 'string');
|
||||
t.ok(p.hint);
|
||||
}
|
||||
};
|
||||
|
||||
const assertValidTickets = (test, tickets) => {
|
||||
test.ok(Array.isArray(tickets));
|
||||
for (let fare of tickets) {
|
||||
test.equal(typeof fare.name, 'string', 'Mandatory field "name" is missing or not a string');
|
||||
test.ok(fare.name);
|
||||
|
||||
test.ok(isObj(fare.priceObj), 'Mandatory field "priceObj" is missing or not an object');
|
||||
test.equal(typeof fare.priceObj.amount, 'number', 'Mandatory field "amount" in "priceObj" is missing or not a number');
|
||||
test.ok(fare.priceObj.amount > 0);
|
||||
if ('currency' in fare.priceObj) {
|
||||
test.equal(typeof fare.priceObj.currency, 'string');
|
||||
}
|
||||
|
||||
// Check optional fields
|
||||
if ('addData' in fare) {
|
||||
test.equal(typeof fare.addData, 'string');
|
||||
}
|
||||
if ('addDataTicketInfo' in fare) {
|
||||
test.equal(typeof fare.addDataTicketInfo, 'string');
|
||||
}
|
||||
if ('addDataTicketDetails' in fare) {
|
||||
test.equal(typeof fare.addDataTicketDetails, 'string');
|
||||
}
|
||||
if ('addDataTravelInfo' in fare) {
|
||||
test.equal(typeof fare.addDataTravelInfo, 'string');
|
||||
}
|
||||
if ('addDataTravelDetails' in fare) {
|
||||
test.equal(typeof fare.firstClass, 'boolean');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const client = createClient(dbProfile, 'public-transport/hafas-client:test', {enrichStations: false});
|
||||
|
||||
const berlinHbf = '8011160';
|
||||
const münchenHbf = '8000261';
|
||||
const jungfernheide = '8011167';
|
||||
const blnSchwedterStr = '732652';
|
||||
const westhafen = '8089116';
|
||||
const wedding = '8089131';
|
||||
const württembergallee = '731084';
|
||||
const regensburgHbf = '8000309';
|
||||
const blnOstbahnhof = '8010255';
|
||||
const blnTiergarten = '8089091';
|
||||
const blnJannowitzbrücke = '8089019';
|
||||
const potsdamHbf = '8012666';
|
||||
const berlinSüdkreuz = '8011113';
|
||||
const kölnHbf = '8000207';
|
||||
|
||||
/*
|
||||
tap.test('journeys – Berlin Schwedter Str. to München Hbf', async (t) => {
|
||||
const res = await client.journeys(blnSchwedterStr, münchenHbf, {
|
||||
results: 4,
|
||||
departure: when,
|
||||
stopovers: true,
|
||||
});
|
||||
|
||||
await testJourneysStationToStation({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
fromId: blnSchwedterStr,
|
||||
toId: münchenHbf,
|
||||
});
|
||||
// todo: find a journey where there pricing info is always available
|
||||
for (let journey of res.journeys) {
|
||||
if (journey.price) {
|
||||
assertValidPrice(t, journey.price);
|
||||
}
|
||||
if (journey.tickets) {
|
||||
assertValidTickets(t, journey.tickets);
|
||||
}
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('refreshJourney – valid tickets', async (t) => {
|
||||
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
|
||||
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
||||
|
||||
const journeysRes = await client.journeys(berlinHbf, münchenHbf, {
|
||||
results: 4,
|
||||
departure: when,
|
||||
stopovers: true,
|
||||
});
|
||||
const refreshWithoutTicketsRes = await client.refreshJourney(journeysRes.journeys[0].refreshToken, {
|
||||
tickets: false,
|
||||
});
|
||||
const refreshWithTicketsRes = await client.refreshJourney(journeysRes.journeys[0].refreshToken, {
|
||||
tickets: true,
|
||||
});
|
||||
for (let res of [refreshWithoutTicketsRes, refreshWithTicketsRes]) {
|
||||
if (res.journey.tickets !== undefined) {
|
||||
assertValidTickets(t, res.journey.tickets);
|
||||
}
|
||||
}
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
// todo: journeys, only one product
|
||||
|
||||
tap.test('journeys – fails with no product', async (t) => {
|
||||
await journeysFailsWithNoProduct({
|
||||
test: t,
|
||||
fetchJourneys: client.journeys,
|
||||
fromId: blnSchwedterStr,
|
||||
toId: münchenHbf,
|
||||
when,
|
||||
products: dbProfile.products,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('Berlin Schwedter Str. to Torfstraße 17', async (t) => {
|
||||
const torfstr = {
|
||||
type: 'location',
|
||||
address: 'Torfstraße 17',
|
||||
latitude: 52.5416823,
|
||||
longitude: 13.3491223,
|
||||
};
|
||||
const res = await client.journeys(blnSchwedterStr, torfstr, {
|
||||
results: 3,
|
||||
departure: when,
|
||||
});
|
||||
|
||||
await testJourneysStationToAddress({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
fromId: blnSchwedterStr,
|
||||
to: torfstr,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('Berlin Schwedter Str. to ATZE Musiktheater', async (t) => {
|
||||
const atze = {
|
||||
type: 'location',
|
||||
id: '991598902',
|
||||
poi: true,
|
||||
name: 'Berlin, Atze Musiktheater für Kinder (Kultur und U',
|
||||
latitude: 52.542417,
|
||||
longitude: 13.350437,
|
||||
};
|
||||
const res = await client.journeys(blnSchwedterStr, atze, {
|
||||
results: 3,
|
||||
departure: when,
|
||||
});
|
||||
|
||||
await testJourneysStationToPoi({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
fromId: blnSchwedterStr,
|
||||
to: atze,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('journeys: via works – with detour', async (t) => {
|
||||
// Going from Westhafen to Wedding via Württembergalle without detour
|
||||
// is currently impossible. We check if the routing engine computes a detour.
|
||||
const res = await client.journeys(westhafen, wedding, {
|
||||
via: württembergallee,
|
||||
results: 1,
|
||||
departure: when,
|
||||
stopovers: true,
|
||||
});
|
||||
|
||||
await testJourneysWithDetour({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
detourIds: [württembergallee],
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
// todo: walkingSpeed "Berlin - Charlottenburg, Hallerstraße" -> jungfernheide
|
||||
// todo: without detour
|
||||
|
||||
|
||||
// todo: with the DB endpoint, earlierRef/laterRef is missing queries many days in the future
|
||||
tap.skip('earlier/later journeys, Jungfernheide -> München Hbf', async (t) => {
|
||||
await testEarlierLaterJourneys({
|
||||
test: t,
|
||||
fetchJourneys: client.journeys,
|
||||
validate,
|
||||
fromId: jungfernheide,
|
||||
toId: münchenHbf,
|
||||
when,
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
if (!process.env.VCR_MODE) {
|
||||
tap.test('journeys – leg cycle & alternatives', async (t) => {
|
||||
await testLegCycleAlternatives({
|
||||
test: t,
|
||||
fetchJourneys: client.journeys,
|
||||
fromId: blnTiergarten,
|
||||
toId: blnJannowitzbrücke,
|
||||
when,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
}
|
||||
|
||||
tap.test('refreshJourney', async (t) => {
|
||||
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
|
||||
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
||||
const validate = createValidate({...cfg, when});
|
||||
|
||||
await testRefreshJourney({
|
||||
test: t,
|
||||
fetchJourneys: client.journeys,
|
||||
refreshJourney: client.refreshJourney,
|
||||
validate,
|
||||
fromId: jungfernheide,
|
||||
toId: münchenHbf,
|
||||
when,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.skip('journeysFromTrip – U Mehringdamm to U Naturkundemuseum, reroute to Spittelmarkt.', async (t) => {
|
||||
const blnMehringdamm = '730939';
|
||||
const blnStadtmitte = '732541';
|
||||
const blnNaturkundemuseum = '732539';
|
||||
const blnSpittelmarkt = '732543';
|
||||
|
||||
const isU6Leg = leg => leg.line && leg.line.name
|
||||
&& leg.line.name.toUpperCase()
|
||||
.replace(/\s+/g, '') === 'U6';
|
||||
|
||||
const sameStopOrStation = (stopA) => (stopB) => {
|
||||
if (stopA.id && stopB.id && stopA.id === stopB.id) {
|
||||
return true;
|
||||
}
|
||||
const statA = stopA.stat && stopA.stat.id || NaN;
|
||||
const statB = stopB.stat && stopB.stat.id || NaN;
|
||||
return statA === statB || stopA.id === statB || stopB.id === statA;
|
||||
};
|
||||
const departureOf = st => Number(new Date(st.departure || st.scheduledDeparture));
|
||||
const arrivalOf = st => Number(new Date(st.arrival || st.scheduledArrival));
|
||||
|
||||
// `journeysFromTrip` only supports queries *right now*, so we can't use `when` as in all
|
||||
// other tests. To make the test less brittle, we pick a connection that is served all night. 🙄
|
||||
const when = new Date();
|
||||
const validate = createValidate({...cfg, when});
|
||||
|
||||
const findTripBetween = async (stopAId, stopBId, products = {}) => {
|
||||
const {journeys} = await client.journeys(stopAId, stopBId, {
|
||||
departure: new Date(when - 10 * minute),
|
||||
transfers: 0, products,
|
||||
results: 8, stopovers: false, remarks: false,
|
||||
});
|
||||
for (const j of journeys) {
|
||||
const l = j.legs.find(isU6Leg);
|
||||
if (!l) {
|
||||
continue;
|
||||
}
|
||||
const t = await client.trip(l.tripId, {
|
||||
stopovers: true, remarks: false,
|
||||
});
|
||||
|
||||
const pastStopovers = t.stopovers
|
||||
.filter(st => departureOf(st) < Date.now()); // todo: <= ?
|
||||
const hasStoppedAtA = pastStopovers
|
||||
.find(sameStopOrStation({id: stopAId}));
|
||||
const willStopAtB = t.stopovers
|
||||
.filter(st => arrivalOf(st) > Date.now()) // todo: >= ?
|
||||
.find(sameStopOrStation({id: stopBId}));
|
||||
|
||||
if (hasStoppedAtA && willStopAtB) {
|
||||
const prevStopover = maxBy(pastStopovers, departureOf);
|
||||
return {trip: t, prevStopover};
|
||||
}
|
||||
}
|
||||
return {trip: null, prevStopover: null};
|
||||
};
|
||||
|
||||
// Find a vehicle from U Mehringdamm to U Stadtmitte (to the north) that is currently
|
||||
// between these two stations.
|
||||
const {trip, prevStopover} = await findTripBetween(blnMehringdamm, blnStadtmitte, {
|
||||
regionalExpress: false, regional: false, suburban: false,
|
||||
});
|
||||
t.ok(trip, 'precondition failed: trip not found');
|
||||
t.ok(prevStopover, 'precondition failed: previous stopover missing');
|
||||
|
||||
// todo: "Error: Suche aus dem Zug: Vor Abfahrt des Zuges"
|
||||
const newJourneys = await client.journeysFromTrip(trip.id, prevStopover, blnSpittelmarkt, {
|
||||
results: 3, stopovers: true, remarks: false,
|
||||
});
|
||||
|
||||
// Validate with fake prices.
|
||||
const withFakePrice = (j) => {
|
||||
const clone = Object.assign({}, j);
|
||||
clone.price = {amount: 123, currency: 'EUR'};
|
||||
return clone;
|
||||
};
|
||||
// todo: there is no such validator!
|
||||
validate(t, newJourneys.map(withFakePrice), 'journeysFromTrip', 'newJourneys');
|
||||
|
||||
for (let i = 0; i < newJourneys.length; i++) {
|
||||
const j = newJourneys[i];
|
||||
const n = `newJourneys[${i}]`;
|
||||
|
||||
const legOnTrip = j.legs.find(l => l.tripId === trip.id);
|
||||
t.ok(legOnTrip, n + ': leg with trip ID not found');
|
||||
t.equal(last(legOnTrip.stopovers).stop.id, blnStadtmitte);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('trip details', async (t) => {
|
||||
const res = await client.journeys(berlinHbf, münchenHbf, {
|
||||
results: 1, departure: when,
|
||||
});
|
||||
|
||||
const p = res.journeys[0].legs.find(l => !l.walking);
|
||||
t.ok(p.tripId, 'precondition failed');
|
||||
t.ok(p.line.name, 'precondition failed');
|
||||
|
||||
const tripRes = await client.trip(p.tripId, {when});
|
||||
|
||||
const validate = createValidate(cfg, {
|
||||
trip: (cfg) => {
|
||||
const validateTrip = createValidateTrip(cfg);
|
||||
const validateTripWithFakeDirection = (val, trip, name) => {
|
||||
validateTrip(val, {
|
||||
...trip,
|
||||
direction: trip.direction || 'foo', // todo, see #49
|
||||
}, name);
|
||||
};
|
||||
return validateTripWithFakeDirection;
|
||||
},
|
||||
});
|
||||
validate(t, tripRes, 'tripResult', 'tripRes');
|
||||
|
||||
t.end();
|
||||
});
|
||||
*/
|
||||
|
||||
tap.test('departures at Berlin Schwedter Str.', async (t) => {
|
||||
const res = await client.departures(blnSchwedterStr, {
|
||||
duration: 5, when,
|
||||
});
|
||||
|
||||
await testDepartures({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
id: blnSchwedterStr,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('departures with station object', async (t) => {
|
||||
const res = await client.departures({
|
||||
type: 'station',
|
||||
id: jungfernheide,
|
||||
name: 'Berlin Jungfernheide',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 1.23,
|
||||
longitude: 2.34,
|
||||
},
|
||||
}, {when});
|
||||
|
||||
validate(t, res, 'departuresResponse', 'res');
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('arrivals at Berlin Schwedter Str.', async (t) => {
|
||||
const res = await client.arrivals(blnSchwedterStr, {
|
||||
duration: 5, when,
|
||||
});
|
||||
|
||||
await testArrivals({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
id: blnSchwedterStr,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
/*
|
||||
tap.test('nearby Berlin Jungfernheide', async (t) => {
|
||||
const nearby = await client.nearby({
|
||||
type: 'location',
|
||||
latitude: 52.530273,
|
||||
longitude: 13.299433,
|
||||
}, {
|
||||
results: 2, distance: 400,
|
||||
});
|
||||
|
||||
validate(t, nearby, 'locations', 'nearby');
|
||||
|
||||
t.equal(nearby.length, 2);
|
||||
|
||||
const s0 = nearby[0];
|
||||
t.equal(s0.id, jungfernheide);
|
||||
t.equal(s0.name, 'Berlin Jungfernheide');
|
||||
t.ok(isRoughlyEqual(0.0005, s0.location.latitude, 52.530408));
|
||||
t.ok(isRoughlyEqual(0.0005, s0.location.longitude, 13.299424));
|
||||
t.ok(s0.distance >= 0);
|
||||
t.ok(s0.distance <= 100);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('locations named Jungfernheide', async (t) => {
|
||||
const locations = await client.locations('Jungfernheide', {
|
||||
results: 10,
|
||||
});
|
||||
|
||||
validate(t, locations, 'locations', 'locations');
|
||||
t.ok(locations.length <= 10);
|
||||
t.ok(locations.some((l) => {
|
||||
return l.station && l.station.id === jungfernheide || l.id === jungfernheide;
|
||||
}), 'Jungfernheide not found');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('stop', async (t) => {
|
||||
const s = await client.stop(regensburgHbf);
|
||||
|
||||
validate(t, s, ['stop', 'station'], 'stop');
|
||||
t.equal(s.id, regensburgHbf);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('line with additionalName', async (t) => {
|
||||
const {departures} = await client.departures(potsdamHbf, {
|
||||
when,
|
||||
duration: 12 * 60, // 12 minutes
|
||||
products: {bus: false, suburban: false, tram: false},
|
||||
});
|
||||
t.ok(departures.some(d => d.line && d.line.additionalName));
|
||||
t.end();
|
||||
});
|
||||
*/
|
525
test/e2e/dbweb.js
Normal file
525
test/e2e/dbweb.js
Normal file
|
@ -0,0 +1,525 @@
|
|||
import tap from 'tap';
|
||||
import isRoughlyEqual from 'is-roughly-equal';
|
||||
|
||||
import {createWhen} from './lib/util.js';
|
||||
import {createClient} from '../../index.js';
|
||||
import {profile as dbProfile} from '../../p/dbweb/index.js';
|
||||
import {
|
||||
createValidateStation,
|
||||
createValidateTrip,
|
||||
} from './lib/validators.js';
|
||||
import {createValidateFptfWith as createValidate} from './lib/validate-fptf-with.js';
|
||||
import {testJourneysStationToStation} from './lib/journeys-station-to-station.js';
|
||||
import {testJourneysStationToAddress} from './lib/journeys-station-to-address.js';
|
||||
import {testJourneysStationToPoi} from './lib/journeys-station-to-poi.js';
|
||||
import {testEarlierLaterJourneys} from './lib/earlier-later-journeys.js';
|
||||
import {testLegCycleAlternatives} from './lib/leg-cycle-alternatives.js';
|
||||
import {testRefreshJourney} from './lib/refresh-journey.js';
|
||||
import {journeysFailsWithNoProduct} from './lib/journeys-fails-with-no-product.js';
|
||||
import {testDepartures} from './lib/departures.js';
|
||||
import {testArrivals} from './lib/arrivals.js';
|
||||
import {testJourneysWithDetour} from './lib/journeys-with-detour.js';
|
||||
|
||||
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o);
|
||||
const minute = 60 * 1000;
|
||||
|
||||
const T_MOCK = 1747040400 * 1000; // 2025-05-12T08:00:00+01:00
|
||||
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
||||
|
||||
const cfg = {
|
||||
when,
|
||||
stationCoordsOptional: true, // TODO
|
||||
products: dbProfile.products,
|
||||
minLatitude: 46.673100,
|
||||
maxLatitude: 55.030671,
|
||||
minLongitude: 6.896517,
|
||||
maxLongitude: 16.180237,
|
||||
};
|
||||
|
||||
const validate = createValidate(cfg);
|
||||
|
||||
const assertValidPrice = (t, p) => {
|
||||
t.ok(p);
|
||||
if (p.amount !== null) {
|
||||
t.equal(typeof p.amount, 'number');
|
||||
t.ok(p.amount > 0);
|
||||
}
|
||||
if (p.hint !== null) {
|
||||
t.equal(typeof p.hint, 'string');
|
||||
t.ok(p.hint);
|
||||
}
|
||||
};
|
||||
|
||||
const assertValidTickets = (test, tickets) => {
|
||||
test.ok(Array.isArray(tickets));
|
||||
for (let fare of tickets) {
|
||||
test.equal(typeof fare.name, 'string', 'Mandatory field "name" is missing or not a string');
|
||||
test.ok(fare.name);
|
||||
|
||||
test.ok(isObj(fare.priceObj), 'Mandatory field "priceObj" is missing or not an object');
|
||||
test.equal(typeof fare.priceObj.amount, 'number', 'Mandatory field "amount" in "priceObj" is missing or not a number');
|
||||
test.ok(fare.priceObj.amount > 0);
|
||||
if ('currency' in fare.priceObj) {
|
||||
test.equal(typeof fare.priceObj.currency, 'string');
|
||||
}
|
||||
|
||||
// Check optional fields
|
||||
if ('addData' in fare) {
|
||||
test.equal(typeof fare.addData, 'string');
|
||||
}
|
||||
if ('addDataTicketInfo' in fare) {
|
||||
test.equal(typeof fare.addDataTicketInfo, 'string');
|
||||
}
|
||||
if ('addDataTicketDetails' in fare) {
|
||||
test.equal(typeof fare.addDataTicketDetails, 'string');
|
||||
}
|
||||
if ('addDataTravelInfo' in fare) {
|
||||
test.equal(typeof fare.addDataTravelInfo, 'string');
|
||||
}
|
||||
if ('addDataTravelDetails' in fare) {
|
||||
test.equal(typeof fare.firstClass, 'boolean');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const client = createClient(dbProfile, 'public-transport/hafas-client:test', {enrichStations: true});
|
||||
|
||||
const berlinHbf = '8011160';
|
||||
const münchenHbf = '8000261';
|
||||
const jungfernheide = '8011167';
|
||||
const blnSchwedterStr = '732652';
|
||||
const westhafen = '8089116';
|
||||
const wedding = '8089131';
|
||||
const württembergallee = '731084';
|
||||
const regensburgHbf = '8000309';
|
||||
const blnOstbahnhof = '8010255';
|
||||
const blnTiergarten = '8089091';
|
||||
const blnJannowitzbrücke = '8089019';
|
||||
const potsdamHbf = '8012666';
|
||||
const berlinSüdkreuz = '8011113';
|
||||
const kölnHbf = '8000207';
|
||||
|
||||
tap.test('journeys – Berlin Schwedter Str. to München Hbf', async (t) => {
|
||||
const res = await client.journeys(blnSchwedterStr, münchenHbf, {
|
||||
results: 4,
|
||||
departure: when,
|
||||
stopovers: true,
|
||||
});
|
||||
|
||||
await testJourneysStationToStation({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
fromId: blnSchwedterStr,
|
||||
toId: münchenHbf,
|
||||
});
|
||||
// todo: find a journey where there pricing info is always available
|
||||
for (let journey of res.journeys) {
|
||||
if (journey.price) {
|
||||
assertValidPrice(t, journey.price);
|
||||
}
|
||||
if (journey.tickets) {
|
||||
assertValidTickets(t, journey.tickets);
|
||||
}
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('refreshJourney – valid tickets', async (t) => {
|
||||
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
|
||||
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
||||
|
||||
const journeysRes = await client.journeys(berlinHbf, münchenHbf, {
|
||||
results: 4,
|
||||
departure: when,
|
||||
stopovers: true,
|
||||
});
|
||||
const refreshWithoutTicketsRes = await client.refreshJourney(journeysRes.journeys[0].refreshToken, {
|
||||
tickets: false,
|
||||
});
|
||||
const refreshWithTicketsRes = await client.refreshJourney(journeysRes.journeys[0].refreshToken, {
|
||||
tickets: true,
|
||||
});
|
||||
for (let res of [refreshWithoutTicketsRes, refreshWithTicketsRes]) {
|
||||
if (res.journey.tickets !== undefined) {
|
||||
assertValidTickets(t, res.journey.tickets);
|
||||
}
|
||||
}
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
// todo: journeys, only one product
|
||||
|
||||
tap.test('journeys – fails with no product', async (t) => {
|
||||
await journeysFailsWithNoProduct({
|
||||
test: t,
|
||||
fetchJourneys: client.journeys,
|
||||
fromId: blnSchwedterStr,
|
||||
toId: münchenHbf,
|
||||
when,
|
||||
products: dbProfile.products,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('Berlin Schwedter Str. to Torfstraße 17', async (t) => {
|
||||
const torfstr = {
|
||||
type: 'location',
|
||||
address: 'Torfstraße 17',
|
||||
latitude: 52.5416823,
|
||||
longitude: 13.3491223,
|
||||
};
|
||||
const res = await client.journeys(blnSchwedterStr, torfstr, {
|
||||
results: 3,
|
||||
departure: when,
|
||||
});
|
||||
|
||||
await testJourneysStationToAddress({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
fromId: blnSchwedterStr,
|
||||
to: torfstr,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('Berlin Schwedter Str. to ATZE Musiktheater', async (t) => {
|
||||
const atze = {
|
||||
type: 'location',
|
||||
id: '991598902',
|
||||
poi: true,
|
||||
name: 'Berlin, Atze Musiktheater für Kinder (Kultur und U',
|
||||
latitude: 52.542417,
|
||||
longitude: 13.350437,
|
||||
};
|
||||
const res = await client.journeys(blnSchwedterStr, atze, {
|
||||
results: 3,
|
||||
departure: when,
|
||||
});
|
||||
|
||||
await testJourneysStationToPoi({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
fromId: blnSchwedterStr,
|
||||
to: atze,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('journeys: via works – with detour', async (t) => {
|
||||
// Going from Westhafen to Wedding via Württembergalle without detour
|
||||
// is currently impossible. We check if the routing engine computes a detour.
|
||||
const res = await client.journeys(westhafen, wedding, {
|
||||
via: württembergallee,
|
||||
results: 1,
|
||||
departure: when,
|
||||
stopovers: true,
|
||||
});
|
||||
|
||||
await testJourneysWithDetour({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
detourIds: [württembergallee],
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
// todo: walkingSpeed "Berlin - Charlottenburg, Hallerstraße" -> jungfernheide
|
||||
// todo: without detour
|
||||
|
||||
|
||||
// todo: with the DB endpoint, earlierRef/laterRef is missing queries many days in the future
|
||||
tap.skip('earlier/later journeys, Jungfernheide -> München Hbf', async (t) => {
|
||||
await testEarlierLaterJourneys({
|
||||
test: t,
|
||||
fetchJourneys: client.journeys,
|
||||
validate,
|
||||
fromId: jungfernheide,
|
||||
toId: münchenHbf,
|
||||
when,
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
if (!process.env.VCR_MODE) {
|
||||
tap.test('journeys – leg cycle & alternatives', async (t) => {
|
||||
await testLegCycleAlternatives({
|
||||
test: t,
|
||||
fetchJourneys: client.journeys,
|
||||
fromId: blnTiergarten,
|
||||
toId: blnJannowitzbrücke,
|
||||
when,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
}
|
||||
|
||||
tap.test('refreshJourney', async (t) => {
|
||||
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
|
||||
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
||||
const validate = createValidate({...cfg, when});
|
||||
|
||||
await testRefreshJourney({
|
||||
test: t,
|
||||
fetchJourneys: client.journeys,
|
||||
refreshJourney: client.refreshJourney,
|
||||
validate,
|
||||
fromId: jungfernheide,
|
||||
toId: münchenHbf,
|
||||
when,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
/*
|
||||
tap.skip('journeysFromTrip – U Mehringdamm to U Naturkundemuseum, reroute to Spittelmarkt.', async (t) => {
|
||||
const blnMehringdamm = '730939';
|
||||
const blnStadtmitte = '732541';
|
||||
const blnNaturkundemuseum = '732539';
|
||||
const blnSpittelmarkt = '732543';
|
||||
|
||||
const isU6Leg = leg => leg.line && leg.line.name
|
||||
&& leg.line.name.toUpperCase()
|
||||
.replace(/\s+/g, '') === 'U6';
|
||||
|
||||
const sameStopOrStation = (stopA) => (stopB) => {
|
||||
if (stopA.id && stopB.id && stopA.id === stopB.id) {
|
||||
return true;
|
||||
}
|
||||
const statA = stopA.stat && stopA.stat.id || NaN;
|
||||
const statB = stopB.stat && stopB.stat.id || NaN;
|
||||
return statA === statB || stopA.id === statB || stopB.id === statA;
|
||||
};
|
||||
const departureOf = st => Number(new Date(st.departure || st.scheduledDeparture));
|
||||
const arrivalOf = st => Number(new Date(st.arrival || st.scheduledArrival));
|
||||
|
||||
// `journeysFromTrip` only supports queries *right now*, so we can't use `when` as in all
|
||||
// other tests. To make the test less brittle, we pick a connection that is served all night. 🙄
|
||||
const when = new Date();
|
||||
const validate = createValidate({...cfg, when});
|
||||
|
||||
const findTripBetween = async (stopAId, stopBId, products = {}) => {
|
||||
const {journeys} = await client.journeys(stopAId, stopBId, {
|
||||
departure: new Date(when - 10 * minute),
|
||||
transfers: 0, products,
|
||||
results: 8, stopovers: false, remarks: false,
|
||||
});
|
||||
for (const j of journeys) {
|
||||
const l = j.legs.find(isU6Leg);
|
||||
if (!l) {
|
||||
continue;
|
||||
}
|
||||
const t = await client.trip(l.tripId, {
|
||||
stopovers: true, remarks: false,
|
||||
});
|
||||
|
||||
const pastStopovers = t.stopovers
|
||||
.filter(st => departureOf(st) < Date.now()); // todo: <= ?
|
||||
const hasStoppedAtA = pastStopovers
|
||||
.find(sameStopOrStation({id: stopAId}));
|
||||
const willStopAtB = t.stopovers
|
||||
.filter(st => arrivalOf(st) > Date.now()) // todo: >= ?
|
||||
.find(sameStopOrStation({id: stopBId}));
|
||||
|
||||
if (hasStoppedAtA && willStopAtB) {
|
||||
const prevStopover = maxBy(pastStopovers, departureOf);
|
||||
return {trip: t, prevStopover};
|
||||
}
|
||||
}
|
||||
return {trip: null, prevStopover: null};
|
||||
};
|
||||
|
||||
// Find a vehicle from U Mehringdamm to U Stadtmitte (to the north) that is currently
|
||||
// between these two stations.
|
||||
const {trip, prevStopover} = await findTripBetween(blnMehringdamm, blnStadtmitte, {
|
||||
regionalExpress: false, regional: false, suburban: false,
|
||||
});
|
||||
t.ok(trip, 'precondition failed: trip not found');
|
||||
t.ok(prevStopover, 'precondition failed: previous stopover missing');
|
||||
|
||||
// todo: "Error: Suche aus dem Zug: Vor Abfahrt des Zuges"
|
||||
const newJourneys = await client.journeysFromTrip(trip.id, prevStopover, blnSpittelmarkt, {
|
||||
results: 3, stopovers: true, remarks: false,
|
||||
});
|
||||
|
||||
// Validate with fake prices.
|
||||
const withFakePrice = (j) => {
|
||||
const clone = Object.assign({}, j);
|
||||
clone.price = {amount: 123, currency: 'EUR'};
|
||||
return clone;
|
||||
};
|
||||
// todo: there is no such validator!
|
||||
validate(t, newJourneys.map(withFakePrice), 'journeysFromTrip', 'newJourneys');
|
||||
|
||||
for (let i = 0; i < newJourneys.length; i++) {
|
||||
const j = newJourneys[i];
|
||||
const n = `newJourneys[${i}]`;
|
||||
|
||||
const legOnTrip = j.legs.find(l => l.tripId === trip.id);
|
||||
t.ok(legOnTrip, n + ': leg with trip ID not found');
|
||||
t.equal(last(legOnTrip.stopovers).stop.id, blnStadtmitte);
|
||||
}
|
||||
});*/
|
||||
|
||||
tap.test('trip details', async (t) => {
|
||||
const res = await client.journeys(berlinHbf, münchenHbf, {
|
||||
results: 1, departure: when,
|
||||
});
|
||||
|
||||
const p = res.journeys[0].legs.find(l => !l.walking);
|
||||
t.ok(p.tripId, 'precondition failed');
|
||||
t.ok(p.line.name, 'precondition failed');
|
||||
|
||||
const tripRes = await client.trip(p.tripId, {when});
|
||||
|
||||
const validate = createValidate(cfg, {
|
||||
trip: (cfg) => {
|
||||
const validateTrip = createValidateTrip(cfg);
|
||||
const validateTripWithFakeDirection = (val, trip, name) => {
|
||||
validateTrip(val, {
|
||||
...trip,
|
||||
direction: trip.direction || 'foo', // todo, see #49
|
||||
}, name);
|
||||
};
|
||||
return validateTripWithFakeDirection;
|
||||
},
|
||||
});
|
||||
validate(t, tripRes, 'tripResult', 'tripRes');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('departures at Berlin Schwedter Str.', async (t) => {
|
||||
const res = await new Promise((resolve) => {
|
||||
let interval = setInterval(async () => { // repeat evaluating `departures()` until stations are enriched
|
||||
const res = await client.departures(blnSchwedterStr, {
|
||||
duration: 5, when,
|
||||
});
|
||||
|
||||
if (res.departures[0].stop.name !== undefined) { // ctx.common.locations have loaded
|
||||
clearInterval(interval);
|
||||
return resolve(res);
|
||||
}
|
||||
}, 4000);
|
||||
});
|
||||
|
||||
await testDepartures({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
id: blnSchwedterStr,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('departures with station object', async (t) => {
|
||||
const res = await new Promise((resolve) => {
|
||||
let interval = setInterval(async () => { // repeat evaluating `departures()` until stations are enriched
|
||||
const res = await client.departures({
|
||||
type: 'station',
|
||||
id: jungfernheide,
|
||||
name: 'Berlin Jungfernheide',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 1.23,
|
||||
longitude: 2.34,
|
||||
},
|
||||
}, {when});
|
||||
|
||||
if (res.departures[0].stop.name !== undefined) { // ctx.common.locations have loaded
|
||||
clearInterval(interval);
|
||||
return resolve(res);
|
||||
}
|
||||
}, 4000);
|
||||
});
|
||||
|
||||
validate(t, res, 'departuresResponse', 'res');
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('arrivals at Berlin Schwedter Str.', async (t) => {
|
||||
const res = await new Promise((resolve) => {
|
||||
let interval = setInterval(async () => { // repeat evaluating `arrivals()` until stations are enriched
|
||||
const res = await client.arrivals(blnSchwedterStr, {
|
||||
duration: 5, when,
|
||||
});
|
||||
|
||||
if (res.arrivals[0].stop.name !== undefined) { // ctx.common.locations have loaded
|
||||
clearInterval(interval);
|
||||
return resolve(res);
|
||||
}
|
||||
}, 4000);
|
||||
});
|
||||
|
||||
await testArrivals({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
id: blnSchwedterStr,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('nearby Berlin Jungfernheide', async (t) => {
|
||||
const nearby = await client.nearby({
|
||||
type: 'location',
|
||||
latitude: 52.530273,
|
||||
longitude: 13.299433,
|
||||
}, {
|
||||
results: 2, distance: 400,
|
||||
});
|
||||
|
||||
validate(t, nearby, 'locations', 'nearby');
|
||||
|
||||
t.equal(nearby.length, 2);
|
||||
|
||||
const s0 = nearby[0];
|
||||
t.equal(s0.id, jungfernheide);
|
||||
t.equal(s0.name, 'Berlin Jungfernheide');
|
||||
t.ok(isRoughlyEqual(0.0005, s0.location.latitude, 52.530408));
|
||||
t.ok(isRoughlyEqual(0.0005, s0.location.longitude, 13.299424));
|
||||
t.ok(s0.distance >= 0);
|
||||
t.ok(s0.distance <= 100);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('locations named Jungfernheide', async (t) => {
|
||||
const locations = await client.locations('Jungfernheide', {
|
||||
results: 10,
|
||||
});
|
||||
|
||||
validate(t, locations, 'locations', 'locations');
|
||||
t.ok(locations.length <= 10);
|
||||
t.ok(locations.some((l) => {
|
||||
return l.station && l.station.id === jungfernheide || l.id === jungfernheide;
|
||||
}), 'Jungfernheide not found');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
/*
|
||||
tap.test('stop', async (t) => {
|
||||
const s = await client.stop(regensburgHbf);
|
||||
|
||||
validate(t, s, ['stop', 'station'], 'stop');
|
||||
t.equal(s.id, regensburgHbf);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('line with additionalName', async (t) => {
|
||||
const {departures} = await client.departures(potsdamHbf, {
|
||||
when,
|
||||
duration: 12 * 60, // 12 minutes
|
||||
products: {bus: false, suburban: false, tram: false},
|
||||
});
|
||||
t.ok(departures.some(d => d.line && d.line.additionalName));
|
||||
t.end();
|
||||
});
|
||||
*/
|
File diff suppressed because one or more lines are too long
1508
test/fixtures/dbweb-departures.js
vendored
Normal file
1508
test/fixtures/dbweb-departures.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
189
test/fixtures/dbweb-departures.json
vendored
Normal file
189
test/fixtures/dbweb-departures.json
vendored
Normal file
|
@ -0,0 +1,189 @@
|
|||
{
|
||||
"entries": [
|
||||
{
|
||||
"bahnhofsId": "8000295",
|
||||
"zeit": "2025-02-08T15:31:00",
|
||||
"ezZeit": "2025-02-08T16:05:00",
|
||||
"gleis": "2",
|
||||
"ueber": [
|
||||
"Osterburken",
|
||||
"Möckmühl",
|
||||
"Bad Friedrichshall Hbf",
|
||||
"Neckarsulm",
|
||||
"Heilbronn Hbf",
|
||||
"Bietigheim-Bissingen",
|
||||
"Ludwigsburg",
|
||||
"Stuttgart Hbf"
|
||||
],
|
||||
"journeyId": "2|#VN#1#ST#1738783727#PI#0#ZI#315246#TA#0#DA#80225#1S#8000260#1T#1437#LS#8000096#LT#1653#PU#80#RT#1#CA#DPN#ZE#19073#ZB#RE 19073#PC#3#FR#8000260#FT#1437#TO#8000096#TT#1653#",
|
||||
"meldungen": [],
|
||||
"verkehrmittel": {
|
||||
"name": "RE 19073",
|
||||
"kurzText": "RE",
|
||||
"mittelText": "RE 8",
|
||||
"langText": "RE 19073",
|
||||
"produktGattung": "REGIONAL"
|
||||
},
|
||||
"terminus": "Stuttgart Hbf"
|
||||
},
|
||||
{
|
||||
"bahnhofsId": "508987",
|
||||
"zeit": "2025-02-08T16:20:00",
|
||||
"ueber": [
|
||||
"Bahnhof, Osterburken",
|
||||
"Rathaus, Osterburken",
|
||||
"RIO, Osterburken",
|
||||
"Merchingen Ort, Ravenstein",
|
||||
"Hüngheim Ort, Ravenstein",
|
||||
"Oberwittstadt Ort, Ravenstein",
|
||||
"Unterwittstadt Ort, Ravenstein",
|
||||
"Erlenbach, Ravenstein"
|
||||
],
|
||||
"journeyId": "2|#VN#1#ST#1738783727#PI#0#ZI#500575#TA#0#DA#80225#1S#508987#1T#1620#LS#506182#LT#1655#PU#80#RT#1#CA#Bus#ZE#844#ZB#Bus 844#PC#5#FR#508987#FT#1620#TO#506182#TT#1655#",
|
||||
"meldungen": [],
|
||||
"verkehrmittel": {
|
||||
"name": "Bus 844",
|
||||
"linienNummer": "844",
|
||||
"kurzText": "Bus",
|
||||
"mittelText": "Bus 844",
|
||||
"langText": "Bus 844",
|
||||
"produktGattung": "BUS"
|
||||
},
|
||||
"terminus": "Erlenbach"
|
||||
},
|
||||
{
|
||||
"bahnhofsId": "8000295",
|
||||
"zeit": "2025-02-08T16:27:00",
|
||||
"ezZeit": "2025-02-08T16:27:00",
|
||||
"gleis": "4",
|
||||
"ueber": [
|
||||
"Osterburken",
|
||||
"Lauda",
|
||||
"Würzburg Hbf"
|
||||
],
|
||||
"journeyId": "2|#VN#1#ST#1738783727#PI#0#ZI#315248#TA#0#DA#80225#1S#8000096#1T#1506#LS#8000260#LT#1721#PU#80#RT#1#CA#DPN#ZE#19074#ZB#RE 19074#PC#3#FR#8000096#FT#1506#TO#8000260#TT#1721#",
|
||||
"meldungen": [],
|
||||
"verkehrmittel": {
|
||||
"name": "RE 19074",
|
||||
"kurzText": "RE",
|
||||
"mittelText": "RE 8",
|
||||
"langText": "RE 19074",
|
||||
"produktGattung": "REGIONAL"
|
||||
},
|
||||
"terminus": "Würzburg Hbf"
|
||||
},
|
||||
{
|
||||
"bahnhofsId": "8000295",
|
||||
"zeit": "2025-02-08T16:31:00",
|
||||
"ezZeit": "2025-02-08T16:39:00",
|
||||
"gleis": "2",
|
||||
"ueber": [
|
||||
"Osterburken",
|
||||
"Möckmühl",
|
||||
"Bad Friedrichshall Hbf",
|
||||
"Neckarsulm",
|
||||
"Heilbronn Hbf",
|
||||
"Bietigheim-Bissingen",
|
||||
"Ludwigsburg",
|
||||
"Stuttgart Hbf"
|
||||
],
|
||||
"journeyId": "2|#VN#1#ST#1738783727#PI#0#ZI#316113#TA#0#DA#80225#1S#8000260#1T#1537#LS#8000096#LT#1756#PU#80#RT#1#CA#DPN#ZE#63379#ZB#RE 63379#PC#3#FR#8000260#FT#1537#TO#8000096#TT#1756#",
|
||||
"meldungen": [
|
||||
{
|
||||
"prioritaet": "NIEDRIG",
|
||||
"text": "Keine rollstuhlgerechte Einrichtung, kein behindertengerechtes WC im Zug. Mobilitätseingeschränkte Reisende wenden sich bzgl. eventuell erforderlicher Umbuchungen an unsere Mobilitätsservice-Zentrale unter 030 65212888."
|
||||
}
|
||||
],
|
||||
"verkehrmittel": {
|
||||
"name": "RE 63379",
|
||||
"kurzText": "RE",
|
||||
"mittelText": "RE 8",
|
||||
"langText": "RE 63379",
|
||||
"produktGattung": "REGIONAL"
|
||||
},
|
||||
"terminus": "Stuttgart Hbf"
|
||||
},
|
||||
{
|
||||
"bahnhofsId": "510853",
|
||||
"zeit": "2025-02-08T16:35:00",
|
||||
"ueber": [
|
||||
"Bahnhof, Osterburken",
|
||||
"Hohenstadt Ort, Ahorn (Baden)",
|
||||
"Eubigheim Kirche, Ahorn (Baden)",
|
||||
"Eubigheim Obereubigheim Ort, Ahorn (Baden)",
|
||||
"Buch Ort, Ahorn (Baden)",
|
||||
"Uiffingen Ort, Boxberg (Baden)",
|
||||
"Angeltürn Ort, Boxberg (Baden)",
|
||||
"Rathaus, Boxberg (Baden)"
|
||||
],
|
||||
"journeyId": "2|#VN#1#ST#1738783727#PI#0#ZI#1108875#TA#4#DA#80225#1S#510853#1T#1635#LS#421730#LT#1713#PU#80#RT#1#CA#rfb#ZE#9839#ZB#RUF 9839#PC#9#FR#510853#FT#1635#TO#421730#TT#1713#",
|
||||
"meldungen": [],
|
||||
"verkehrmittel": {
|
||||
"name": "RUF 9839",
|
||||
"linienNummer": "9839",
|
||||
"kurzText": "RUF",
|
||||
"mittelText": "RUF 9839",
|
||||
"langText": "RUF 9839",
|
||||
"produktGattung": "ANRUFPFLICHTIG"
|
||||
},
|
||||
"terminus": "Rathaus, Boxberg (Baden)"
|
||||
},
|
||||
{
|
||||
"bahnhofsId": "8000295",
|
||||
"zeit": "2025-02-08T16:36:00",
|
||||
"gleis": "1",
|
||||
"ueber": [
|
||||
"Osterburken",
|
||||
"Adelsheim Nord",
|
||||
"Zimmern(b Seckach)",
|
||||
"Seckach",
|
||||
"Eicholzheim",
|
||||
"Oberschefflenz",
|
||||
"Auerbach(b Mosbach, Baden)",
|
||||
"Homburg(Saar)Hbf"
|
||||
],
|
||||
"journeyId": "2|#VN#1#ST#1738783727#PI#0#ZI#248243#TA#0#DA#80225#1S#8000295#1T#1636#LS#8000176#LT#2006#PU#80#RT#1#CA#s#ZE#1#ZB#S 1#PC#4#FR#8000295#FT#1636#TO#8000176#TT#2006#",
|
||||
"meldungen": [],
|
||||
"verkehrmittel": {
|
||||
"name": "S 1",
|
||||
"linienNummer": "1",
|
||||
"kurzText": "S",
|
||||
"mittelText": "S 1",
|
||||
"langText": "S 1",
|
||||
"produktGattung": "SBAHN"
|
||||
},
|
||||
"terminus": "Homburg(Saar)Hbf"
|
||||
},
|
||||
{
|
||||
"bahnhofsId": "8000295",
|
||||
"zeit": "2025-02-08T16:36:00",
|
||||
"gleis": "3",
|
||||
"ueber": [
|
||||
"Osterburken",
|
||||
"Adelsheim Ost",
|
||||
"Sennfeld",
|
||||
"Roigheim",
|
||||
"Möckmühl",
|
||||
"Züttlingen",
|
||||
"Siglingen",
|
||||
"Tübingen Hbf"
|
||||
],
|
||||
"journeyId": "2|#VN#1#ST#1738783727#PI#0#ZI#898552#TA#0#DA#80225#1S#8000295#1T#1636#LS#8000141#LT#1922#PU#80#RT#1#CA#DPN#ZE#19329#ZB#MEX19329#PC#3#FR#8000295#FT#1636#TO#8000141#TT#1922#",
|
||||
"meldungen": [
|
||||
{
|
||||
"prioritaet": "HOCH",
|
||||
"text": "Halt entfällt",
|
||||
"type": "HALT_AUSFALL"
|
||||
}
|
||||
],
|
||||
"verkehrmittel": {
|
||||
"name": "MEX19329",
|
||||
"kurzText": "MEX",
|
||||
"mittelText": "MEX 18",
|
||||
"langText": "MEX19329",
|
||||
"produktGattung": "REGIONAL"
|
||||
},
|
||||
"terminus": "Tübingen Hbf"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
const dbJourney = {
|
||||
const dbwebJourney = {
|
||||
type: 'journey',
|
||||
legs: [
|
||||
{
|
||||
|
@ -304,5 +304,5 @@ const dbJourney = {
|
|||
};
|
||||
|
||||
export {
|
||||
dbJourney,
|
||||
dbwebJourney,
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
const dbTrip = {
|
||||
const dbwebTrip = {
|
||||
trip: {
|
||||
id: 'foo',
|
||||
origin: {
|
||||
|
@ -470,5 +470,5 @@ const dbTrip = {
|
|||
};
|
||||
|
||||
export {
|
||||
dbTrip,
|
||||
dbwebTrip,
|
||||
};
|
47
test/format/db-trip.js
Normal file
47
test/format/db-trip.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import tap from 'tap';
|
||||
|
||||
import {profile as rawProfile} from '../../p/db/index.js';
|
||||
import {createClient} from '../../index.js';
|
||||
|
||||
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
|
||||
const {profile} = client;
|
||||
|
||||
const opt = {
|
||||
stopovers: true,
|
||||
polyline: false,
|
||||
remarks: true,
|
||||
language: 'en',
|
||||
};
|
||||
|
||||
const tripIdHafas = '2|#VN#1#ST#1738783727#PI#0#ZI#222242#TA#0#DA#70225#1S#8000237#1T#1317#LS#8000261#LT#2002#PU#80#RT#1#CA#ICE#ZE#1007#ZB#ICE 1007#PC#0#FR#8000237#FT#1317#TO#8000261#TT#2002#';
|
||||
const tripIdRis = '20250207-e6b2807e-bb48-39f9-89eb-8491ebc4b32c';
|
||||
|
||||
const reqDbNavExpected = {
|
||||
endpoint: 'https://app.vendo.noncd.db.de/mob/zuglauf/',
|
||||
path: '2%7C%23VN%231%23ST%231738783727%23PI%230%23ZI%23222242%23TA%230%23DA%2370225%231S%238000237%231T%231317%23LS%238000261%23LT%232002%23PU%2380%23RT%231%23CA%23ICE%23ZE%231007%23ZB%23ICE%201007%23PC%230%23FR%238000237%23FT%231317%23TO%238000261%23TT%232002%23',
|
||||
headers: {
|
||||
'Accept': 'application/x.db.vendo.mob.zuglauf.v2+json',
|
||||
'Content-Type': 'application/x.db.vendo.mob.zuglauf.v2+json',
|
||||
},
|
||||
method: 'get',
|
||||
};
|
||||
|
||||
const reqDbRegioGuideExpected = {
|
||||
endpoint: 'https://regio-guide.de/@prd/zupo-travel-information/api/public/ri/journey/',
|
||||
path: '20250207-e6b2807e-bb48-39f9-89eb-8491ebc4b32c',
|
||||
method: 'get',
|
||||
};
|
||||
|
||||
tap.test('db trip(): dynamic request formatting', (t) => {
|
||||
const ctx = {profile, opt};
|
||||
t.notHas(client.profile, 'tripEndpoint');
|
||||
|
||||
const reqDbNav = profile.formatTripReq(ctx, tripIdHafas);
|
||||
delete reqDbNav.headers['X-Correlation-ID'];
|
||||
const reqDbRegioGuide = profile.formatTripReq(ctx, tripIdRis);
|
||||
|
||||
t.same(reqDbNav, reqDbNavExpected);
|
||||
t.same(reqDbRegioGuide, reqDbRegioGuideExpected);
|
||||
|
||||
t.end();
|
||||
});
|
47
test/format/dbweb-arrivals-query.js
Normal file
47
test/format/dbweb-arrivals-query.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import tap from 'tap';
|
||||
|
||||
import {createClient} from '../../index.js';
|
||||
import {profile as rawProfile} from '../../p/dbweb/index.js';
|
||||
|
||||
const client = createClient(rawProfile, 'public-transport/hafas-client:test');
|
||||
const {profile} = client;
|
||||
|
||||
const opt = {
|
||||
when: new Date('2025-02-09T23:55:00+01:00'),
|
||||
remarks: true,
|
||||
stopovers: true,
|
||||
language: 'en',
|
||||
};
|
||||
|
||||
const berlinArrivalsQuery = {
|
||||
endpoint: 'https://int.bahn.de/web/api/reiseloesung/',
|
||||
path: 'ankuenfte',
|
||||
query: {
|
||||
ortExtId: '8011160',
|
||||
zeit: '23:55',
|
||||
datum: '2025-02-09',
|
||||
mitVias: true,
|
||||
verkehrsmittel: [
|
||||
'ICE',
|
||||
'EC_IC',
|
||||
'IR',
|
||||
'REGIONAL',
|
||||
'SBAHN',
|
||||
'BUS',
|
||||
'SCHIFF',
|
||||
'UBAHN',
|
||||
'TRAM',
|
||||
'ANRUFPFLICHTIG',
|
||||
],
|
||||
},
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
tap.test('formats an arrivals() request correctly', (t) => {
|
||||
const ctx = {profile, opt};
|
||||
|
||||
const req = profile.formatStationBoardReq(ctx, '8011160', 'arrivals');
|
||||
|
||||
t.same(req, berlinArrivalsQuery);
|
||||
t.end();
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
import tap from 'tap';
|
||||
|
||||
import {createClient} from '../../index.js';
|
||||
import {profile as rawProfile} from '../../p/db/index.js';
|
||||
import {profile as rawProfile} from '../../p/dbweb/index.js';
|
||||
import {data as loyaltyCards} from '../../format/loyalty-cards.js';
|
||||
|
||||
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
|
||||
|
@ -91,7 +91,7 @@ tap.test('formats a journeys() request correctly (DB)', (t) => {
|
|||
});
|
||||
|
||||
|
||||
tap.test('formats a journeys() request with BC correctly (DB)', (t) => {
|
||||
tap.test('formats a journeys() request with BC correctly (dbweb)', (t) => {
|
||||
const ctx = {profile, opt};
|
||||
|
||||
const req = profile.formatJourneysReq(ctx, '8098160', '8000284', new Date('2024-12-07T23:50:12+01:00'), true, null);
|
||||
|
@ -115,7 +115,7 @@ tap.test('formats a journeys() request with BC correctly (DB)', (t) => {
|
|||
t.end();
|
||||
});
|
||||
|
||||
tap.test('formats a journeys() request with unlimited transfers (DB)', (t) => {
|
||||
tap.test('formats a journeys() request with unlimited transfers (dbweb)', (t) => {
|
||||
const _opt = {...opt};
|
||||
const ctx = {profile, opt: _opt};
|
||||
|
Loading…
Add table
Reference in a new issue