mirror of
				https://github.com/public-transport/db-vendo-client.git
				synced 2025-11-04 01:56:33 +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