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:
dabund24 2025-02-09 00:46:21 +01:00 committed by GitHub
parent 1e7977a8bb
commit c671e995cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 4958 additions and 94 deletions

View file

@ -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 {

View file

@ -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,
};

View file

@ -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
View 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
View 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,
};

View file

@ -0,0 +1,11 @@
const formatTripReq = ({profile, opt}, id) => {
return {
endpoint: profile.tripEndpoint,
path: id,
method: 'get',
};
};
export {
formatTripReq,
};

View file

@ -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
View 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,
};

View 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,
};

View file

@ -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;
};

View file

@ -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;

View file

@ -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;

View file

@ -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'
|| r.type == 'HALT_AUSFALL'
|| r.text == 'Halt entfällt'
|| r.text == '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 {

View file

@ -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();
});

View file

@ -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;

View file

@ -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
View 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();
});

View file

@ -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]);

View file

@ -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]);

View file

@ -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;

View file

@ -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
View 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
View 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

File diff suppressed because it is too large Load diff

189
test/fixtures/dbweb-departures.json vendored Normal file
View 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"
}
]
}

View file

@ -1,4 +1,4 @@
const dbJourney = {
const dbwebJourney = {
type: 'journey',
legs: [
{
@ -304,5 +304,5 @@ const dbJourney = {
};
export {
dbJourney,
dbwebJourney,
};

View file

@ -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
View 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();
});

View 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();
});

View file

@ -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};