split dbweb and dbregioguide profiles; add db profile

This commit is contained in:
dabund24 2025-02-07 12:55:08 +01:00
parent 239c279573
commit d3707db7a2
36 changed files with 1272 additions and 52 deletions

View file

@ -0,0 +1,80 @@
{
"journeys": {
"profileName": "dbnav",
"profileMethods": [
{
"methodName": "formatJourneysReq",
"moduleName": "journeys-req.js"
}
],
"baseKeys": ["journeysEndpoint"]
},
"refreshJourneys": {
"profileName": "dbnav",
"profileMethods": [
{
"methodName": "formatRefreshJourneyReq",
"moduleName": "journeys-req.js"
}
],
"baseKeys": ["refreshJourneysEndpointTickets", "refreshJourneysEndpointPolyline"]
},
"locations": {
"profileName": "dbnav",
"profileMethods": [
{
"methodName": "formatLocationFilter",
"moduleName": "location-filter.js"
},
{
"methodName": "formatLocationsReq",
"moduleName": "locations-req.js"
}
],
"baseKeys": ["locationsEndpoint"]
},
"stop": {
"profileName": "dbnav",
"profileMethods": [
{
"methodName": "formatStopReq",
"moduleName": "stop-req.js"
},
{
"methodName": "parseStop",
"moduleName": "parse-stop.js"
}
],
"baseKeys": ["stopEndpoint"]
},
"nearby": {
"profileName": "dbnav",
"profileMethods": [
{
"methodName": "formatNearbyReq",
"moduleName": "nearby-req.js"
}
],
"baseKeys": ["nearbyEndpoint"]
},
"trip": {
"profileName": "db",
"profileMethods": [
{
"methodName": "formatTripReq",
"moduleName": "trip-req.js"
}
],
"baseKeys": ["tripEndpoint"]
},
"board": {
"profileName": "dbregioguide",
"profileMethods": [
{
"methodName": "formatStationBoardReq",
"moduleName": "station-board-req.js"
}
],
"baseKeys": ["boardEndpoint"]
}
}

View file

@ -1,28 +1,51 @@
import {createRequire} from 'module'; import {createRequire} from 'module';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const dynamicProfileData = require('./dynamicProfileData.json');
const baseProfile = require('./base.json'); const dbnavBase = require('../dbnav/base.json');
const dbregioguideBase = require('../dbregioguide/base.json');
const dbwebBase = require('../dbweb/base.json');
import {products} from '../../lib/products.js'; import {products} from '../../lib/products.js';
import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js';
import {formatTripReq} from './trip-req.js';
import {formatLocationFilter} from './location-filter.js';
import {formatLocationsReq} from './locations-req.js';
import {formatStationBoardReq} from './station-board-req.js';
const profile = { const profile = {
...baseProfile,
locale: 'de-DE', locale: 'de-DE',
timezone: 'Europe/Berlin', timezone: 'Europe/Berlin',
products, products,
formatJourneysReq,
formatRefreshJourneyReq,
formatTripReq,
formatLocationsReq,
formatLocationFilter,
formatStationBoardReq,
}; };
// add profile methods
for (const {profileName, profileMethods} of Object.values(dynamicProfileData)) {
for (const {methodName, moduleName} of profileMethods) {
try {
// TODO use `import()` with top-level await once updated to es2022
profile[methodName] = require(`../${profileName}/${moduleName}`)[methodName];
} catch { /* use implementation from default profile if module doesn't exist */ }
}
}
const bases = {
dbnav: dbnavBase,
dbregioguide: dbregioguideBase,
dbweb: dbwebBase,
};
// add endpoint bases
for (const {profileName, baseKeys} of Object.values(dynamicProfileData)) {
if (profileName !== 'db') { // only add endpoint(s) from specified profile
for (const baseKey of baseKeys) {
profile[baseKey] = bases[profileName][baseKey];
}
continue;
}
// add endpoints from all profiles with the profile names as key suffixes and dynamically decide which to use later
for (const [profileName, profileBases] of Object.entries(bases)) {
for (const baseKey of baseKeys) {
profile[`${baseKey}_${profileName}`] = profileBases[baseKey];
}
}
}
export { export {
profile, profile,
}; };

View file

@ -1,15 +1,16 @@
import {formatTripReq as hafasFormatTripReq} from '../../format/trip-req.js'; import {formatTripReq as hafasFormatTripReq} from '../dbnav/trip-req.js';
import {formatTripReq as risTripReq} from '../dbregioguide/trip-req.js';
const formatTripReq = ({profile, opt}, id) => { const formatTripReq = ({profile, opt}, id) => {
const _profile = {...profile};
if (id.includes('#')) { if (id.includes('#')) {
return hafasFormatTripReq({profile, opt}, id); _profile['tripEndpoint'] = profile.tripEndpoint_dbnav;
return hafasFormatTripReq({profile: _profile, opt}, id);
} }
return {
endpoint: profile.regioGuideTripEndpoint, _profile['tripEndpoint'] = profile.tripEndpoint_dbregioguide;
path: id, return risTripReq({profile: _profile, opt}, id);
method: 'get',
};
}; };
export { export {

5
p/dbregioguide/base.json Normal file
View file

@ -0,0 +1,5 @@
{
"tripEndpoint": "https://regio-guide.de/@prd/zupo-travel-information/api/public/ri/journey/",
"boardEndpoint": "https://regio-guide.de/@prd/zupo-travel-information/api/public/ri/board/",
"defaultLanguage": "en"
}

18
p/dbregioguide/index.js Normal file
View file

@ -0,0 +1,18 @@
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
import {products} from '../../lib/products.js';
import {formatTripReq} from './trip-req.js';
const profile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
products,
formatTripReq,
};
export {
profile,
};

View file

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

View file

@ -5,7 +5,6 @@
"locationsEndpoint": "https://int.bahn.de/web/api/reiseloesung/orte", "locationsEndpoint": "https://int.bahn.de/web/api/reiseloesung/orte",
"nearbyEndpoint": "https://int.bahn.de/web/api/reiseloesung/orte/nearby", "nearbyEndpoint": "https://int.bahn.de/web/api/reiseloesung/orte/nearby",
"tripEndpoint": "https://int.bahn.de/web/api/reiseloesung/fahrt", "tripEndpoint": "https://int.bahn.de/web/api/reiseloesung/fahrt",
"regioGuideTripEndpoint": "https://regio-guide.de/@prd/zupo-travel-information/api/public/ri/journey/",
"boardEndpoint": "https://int.bahn.de/web/api/reiseloesung/", "boardEndpoint": "https://int.bahn.de/web/api/reiseloesung/",
"defaultLanguage": "en" "defaultLanguage": "en"
} }

26
p/dbweb/index.js Normal file
View file

@ -0,0 +1,26 @@
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,
};
export {
profile,
};

View file

@ -0,0 +1,60 @@
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/db/index.js';
const dynamicProfileData = require('../p/db/dynamicProfileData.json');
const dbnavBase = require('../p/dbnav/base.json');
const dbwebBase = require('../p/dbweb/base.json');
const dbregioguideBase = require('../p/dbregioguide/base.json');
import tap from 'tap';
const client = createClient(rawProfile, 'public-transport/hafas-client:test');
tap.test('db: determine base urls', (t) => {
const fqdns = {
dbnav: 'app.vendo.noncd.db.de',
dbregioguide: 'regio-guide.de',
dbweb: 'int.bahn.de',
};
for (const {profileName, baseKeys} of Object.values(dynamicProfileData)) {
if (profileName !== 'db') { // endpoint(s) is(/are) static. Check if correct fqdn is contained in base(s)
for (const baseKey of baseKeys) {
t.ok(client.profile[baseKey].includes(fqdns[profileName]), [`base url for ${profileName} profile should include ${fqdns[profileName]}`]);
}
continue;
}
// endpoint(s) is(/are) dynamic. Check if actual base key does not exist yet. Also check, if bases for all profiles are properly stored in other aux entries for later use
for (const baseKey of baseKeys) {
for (const [profileName, fqdn] of Object.entries(fqdns)) {
t.notHas(client.profile, baseKey, [`db profile should not contain the key ${baseKey}, since it is dynamically dispatched at runtime`]);
t.ok(client.profile[`${baseKey}_${profileName}`].includes(fqdn), [`key ${baseKey}_${profileName} of db profile should include ${fqdns[profileName]}`]);
}
}
}
t.end();
});
tap.test('db: dynamic client method', async (t) => {
t.equal(dynamicProfileData.trip.profileName, 'db', ['if this fails, check a different client method in this test']);
t.equal(client.profile.formatTripReq, (await import('../p/db/trip-req.js')).formatTripReq);
t.notHas(client.profile, 'tripEndpoint');
t.equal(client.profile.tripEndpoint_dbnav, dbnavBase.tripEndpoint);
t.equal(client.profile.tripEndpoint_dbweb, dbwebBase.tripEndpoint);
t.equal(client.profile.tripEndpoint_dbregioguide, dbregioguideBase.tripEndpoint);
t.end();
});
tap.test('db: static client method', async (t) => {
t.equal(dynamicProfileData.nearby.profileName, 'dbnav', ['if this fails, check a different client method in this test']);
t.equal(client.profile.formatNearbyReq, (await import('../p/dbnav/nearby-req.js')).formatNearbyReq);
t.equal(client.profile.nearbyEndpoint, dbnavBase.nearbyEndpoint);
t.end();
});

View file

@ -6,9 +6,9 @@ const require = createRequire(import.meta.url);
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../index.js'; import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/db/index.js'; import {profile as rawProfile} from '../p/dbregioguide/index.js';
const res = require('./fixtures/db-trip-regio-guide.json'); const res = require('./fixtures/dbregioguide-trip.json');
import {dbTrip as expected} from './fixtures/db-trip-regio-guide.js'; import {dbTrip as expected} from './fixtures/dbregioguide-trip.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false}); const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
const {profile} = client; const {profile} = client;

View file

@ -6,7 +6,7 @@ const require = createRequire(import.meta.url);
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../index.js'; 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'); const res = require('./fixtures/dbris-arrivals.json');
import {dbArrivals as expected} from './fixtures/dbris-arrivals.js'; import {dbArrivals as expected} from './fixtures/dbris-arrivals.js';

View file

@ -6,9 +6,9 @@ const require = createRequire(import.meta.url);
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../index.js'; 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/db-departures.json'); const res = require('./fixtures/dbweb-departures.json');
import {dbDepartures as expected} from './fixtures/db-departures.js'; import {dbwebDepartures as expected} from './fixtures/dbweb-departures.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false}); const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
const {profile} = client; const {profile} = client;
@ -25,7 +25,7 @@ const opt = {
vias: 5, vias: 5,
}; };
tap.test('parses a db departure correctly', (t) => { tap.test('parses a dbweb departure correctly', (t) => {
const ctx = {profile, opt, common: null, res}; const ctx = {profile, opt, common: null, res};
const departures = res.entries.map(d => profile.parseDeparture(ctx, d)); const departures = res.entries.map(d => profile.parseDeparture(ctx, d));

View file

@ -6,9 +6,9 @@ const require = createRequire(import.meta.url);
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../index.js'; 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/db-journey.json'); const res = require('./fixtures/dbweb-journey.json');
import {dbJourney as expected} from './fixtures/db-journey.js'; import {dbwebJourney as expected} from './fixtures/dbweb-journey.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false}); const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
const {profile} = client; const {profile} = client;
@ -31,7 +31,7 @@ const opt = {
products: {}, 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 ctx = {profile, opt, common: null, res};
const journey = profile.parseJourney(ctx, res.verbindungen[0]); const journey = profile.parseJourney(ctx, res.verbindungen[0]);

View file

@ -6,9 +6,9 @@ const require = createRequire(import.meta.url);
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../index.js'; 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/db-refresh-journey.json'); const res = require('./fixtures/dbweb-refresh-journey.json');
import {dbJourney as expected} from './fixtures/db-refresh-journey.js'; import {dbJourney as expected} from './fixtures/dbweb-refresh-journey.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false}); const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
const {profile} = client; const {profile} = client;
@ -31,7 +31,7 @@ const opt = {
products: {}, 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 ctx = {profile, opt, common: null, res};
const journey = profile.parseJourney(ctx, res.verbindungen[0]); const journey = profile.parseJourney(ctx, res.verbindungen[0]);

View file

@ -6,9 +6,9 @@ const require = createRequire(import.meta.url);
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../index.js'; 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/db-trip.json'); const res = require('./fixtures/dbweb-trip.json');
import {dbTrip as expected} from './fixtures/db-trip.js'; import {dbwebTrip as expected} from './fixtures/dbweb-trip.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false}); const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
const {profile} = client; const {profile} = client;

View file

@ -476,7 +476,6 @@ tap.test('locations named Jungfernheide', async (t) => {
t.end(); t.end();
}); });
/*
tap.test('stop', async (t) => { tap.test('stop', async (t) => {
const s = await client.stop(regensburgHbf); const s = await client.stop(regensburgHbf);
@ -486,6 +485,7 @@ tap.test('stop', async (t) => {
t.end(); t.end();
}); });
/*
tap.test('line with additionalName', async (t) => { tap.test('line with additionalName', async (t) => {
const {departures} = await client.departures(potsdamHbf, { const {departures} = await client.departures(potsdamHbf, {
when, when,

499
test/e2e/dbregioguide.js Normal file
View file

@ -0,0 +1,499 @@
import tap from 'tap';
import isRoughlyEqual from 'is-roughly-equal';
import {createWhen} from './lib/util.js';
import {createClient} from '../../index.js';
import {profile as dbProfile} from '../../p/dbregioguide/index.js';
import {
createValidateStation,
createValidateTrip,
} from './lib/validators.js';
import {createValidateFptfWith as createValidate} from './lib/validate-fptf-with.js';
import {testJourneysStationToStation} from './lib/journeys-station-to-station.js';
import {testJourneysStationToAddress} from './lib/journeys-station-to-address.js';
import {testJourneysStationToPoi} from './lib/journeys-station-to-poi.js';
import {testEarlierLaterJourneys} from './lib/earlier-later-journeys.js';
import {testLegCycleAlternatives} from './lib/leg-cycle-alternatives.js';
import {testRefreshJourney} from './lib/refresh-journey.js';
import {journeysFailsWithNoProduct} from './lib/journeys-fails-with-no-product.js';
import {testDepartures} from './lib/departures.js';
import {testArrivals} from './lib/arrivals.js';
import {testJourneysWithDetour} from './lib/journeys-with-detour.js';
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o);
const minute = 60 * 1000;
const T_MOCK = 1747040400 * 1000; // 2025-05-12T08:00:00+01:00
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
const cfg = {
when,
stationCoordsOptional: true, // TODO
products: dbProfile.products,
minLatitude: 46.673100,
maxLatitude: 55.030671,
minLongitude: 6.896517,
maxLongitude: 16.180237,
};
const validate = createValidate(cfg);
const assertValidPrice = (t, p) => {
t.ok(p);
if (p.amount !== null) {
t.equal(typeof p.amount, 'number');
t.ok(p.amount > 0);
}
if (p.hint !== null) {
t.equal(typeof p.hint, 'string');
t.ok(p.hint);
}
};
const assertValidTickets = (test, tickets) => {
test.ok(Array.isArray(tickets));
for (let fare of tickets) {
test.equal(typeof fare.name, 'string', 'Mandatory field "name" is missing or not a string');
test.ok(fare.name);
test.ok(isObj(fare.priceObj), 'Mandatory field "priceObj" is missing or not an object');
test.equal(typeof fare.priceObj.amount, 'number', 'Mandatory field "amount" in "priceObj" is missing or not a number');
test.ok(fare.priceObj.amount > 0);
if ('currency' in fare.priceObj) {
test.equal(typeof fare.priceObj.currency, 'string');
}
// Check optional fields
if ('addData' in fare) {
test.equal(typeof fare.addData, 'string');
}
if ('addDataTicketInfo' in fare) {
test.equal(typeof fare.addDataTicketInfo, 'string');
}
if ('addDataTicketDetails' in fare) {
test.equal(typeof fare.addDataTicketDetails, 'string');
}
if ('addDataTravelInfo' in fare) {
test.equal(typeof fare.addDataTravelInfo, 'string');
}
if ('addDataTravelDetails' in fare) {
test.equal(typeof fare.firstClass, 'boolean');
}
}
};
const client = createClient(dbProfile, 'public-transport/hafas-client:test', {enrichStations: false});
const berlinHbf = '8011160';
const münchenHbf = '8000261';
const jungfernheide = '8011167';
const blnSchwedterStr = '732652';
const westhafen = '8089116';
const wedding = '8089131';
const württembergallee = '731084';
const regensburgHbf = '8000309';
const blnOstbahnhof = '8010255';
const blnTiergarten = '8089091';
const blnJannowitzbrücke = '8089019';
const potsdamHbf = '8012666';
const berlinSüdkreuz = '8011113';
const kölnHbf = '8000207';
/*
tap.test('journeys Berlin Schwedter Str. to München Hbf', async (t) => {
const res = await client.journeys(blnSchwedterStr, münchenHbf, {
results: 4,
departure: when,
stopovers: true,
});
await testJourneysStationToStation({
test: t,
res,
validate,
fromId: blnSchwedterStr,
toId: münchenHbf,
});
// todo: find a journey where there pricing info is always available
for (let journey of res.journeys) {
if (journey.price) {
assertValidPrice(t, journey.price);
}
if (journey.tickets) {
assertValidTickets(t, journey.tickets);
}
}
t.end();
});
tap.test('refreshJourney valid tickets', async (t) => {
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
const journeysRes = await client.journeys(berlinHbf, münchenHbf, {
results: 4,
departure: when,
stopovers: true,
});
const refreshWithoutTicketsRes = await client.refreshJourney(journeysRes.journeys[0].refreshToken, {
tickets: false,
});
const refreshWithTicketsRes = await client.refreshJourney(journeysRes.journeys[0].refreshToken, {
tickets: true,
});
for (let res of [refreshWithoutTicketsRes, refreshWithTicketsRes]) {
if (res.journey.tickets !== undefined) {
assertValidTickets(t, res.journey.tickets);
}
}
t.end();
});
// todo: journeys, only one product
tap.test('journeys fails with no product', async (t) => {
await journeysFailsWithNoProduct({
test: t,
fetchJourneys: client.journeys,
fromId: blnSchwedterStr,
toId: münchenHbf,
when,
products: dbProfile.products,
});
t.end();
});
tap.test('Berlin Schwedter Str. to Torfstraße 17', async (t) => {
const torfstr = {
type: 'location',
address: 'Torfstraße 17',
latitude: 52.5416823,
longitude: 13.3491223,
};
const res = await client.journeys(blnSchwedterStr, torfstr, {
results: 3,
departure: when,
});
await testJourneysStationToAddress({
test: t,
res,
validate,
fromId: blnSchwedterStr,
to: torfstr,
});
t.end();
});
tap.test('Berlin Schwedter Str. to ATZE Musiktheater', async (t) => {
const atze = {
type: 'location',
id: '991598902',
poi: true,
name: 'Berlin, Atze Musiktheater für Kinder (Kultur und U',
latitude: 52.542417,
longitude: 13.350437,
};
const res = await client.journeys(blnSchwedterStr, atze, {
results: 3,
departure: when,
});
await testJourneysStationToPoi({
test: t,
res,
validate,
fromId: blnSchwedterStr,
to: atze,
});
t.end();
});
tap.test('journeys: via works with detour', async (t) => {
// Going from Westhafen to Wedding via Württembergalle without detour
// is currently impossible. We check if the routing engine computes a detour.
const res = await client.journeys(westhafen, wedding, {
via: württembergallee,
results: 1,
departure: when,
stopovers: true,
});
await testJourneysWithDetour({
test: t,
res,
validate,
detourIds: [württembergallee],
});
t.end();
});
// todo: walkingSpeed "Berlin - Charlottenburg, Hallerstraße" -> jungfernheide
// todo: without detour
// todo: with the DB endpoint, earlierRef/laterRef is missing queries many days in the future
tap.skip('earlier/later journeys, Jungfernheide -> München Hbf', async (t) => {
await testEarlierLaterJourneys({
test: t,
fetchJourneys: client.journeys,
validate,
fromId: jungfernheide,
toId: münchenHbf,
when,
});
t.end();
});
if (!process.env.VCR_MODE) {
tap.test('journeys leg cycle & alternatives', async (t) => {
await testLegCycleAlternatives({
test: t,
fetchJourneys: client.journeys,
fromId: blnTiergarten,
toId: blnJannowitzbrücke,
when,
});
t.end();
});
}
tap.test('refreshJourney', async (t) => {
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
const validate = createValidate({...cfg, when});
await testRefreshJourney({
test: t,
fetchJourneys: client.journeys,
refreshJourney: client.refreshJourney,
validate,
fromId: jungfernheide,
toId: münchenHbf,
when,
});
t.end();
});
tap.skip('journeysFromTrip U Mehringdamm to U Naturkundemuseum, reroute to Spittelmarkt.', async (t) => {
const blnMehringdamm = '730939';
const blnStadtmitte = '732541';
const blnNaturkundemuseum = '732539';
const blnSpittelmarkt = '732543';
const isU6Leg = leg => leg.line && leg.line.name
&& leg.line.name.toUpperCase()
.replace(/\s+/g, '') === 'U6';
const sameStopOrStation = (stopA) => (stopB) => {
if (stopA.id && stopB.id && stopA.id === stopB.id) {
return true;
}
const statA = stopA.stat && stopA.stat.id || NaN;
const statB = stopB.stat && stopB.stat.id || NaN;
return statA === statB || stopA.id === statB || stopB.id === statA;
};
const departureOf = st => Number(new Date(st.departure || st.scheduledDeparture));
const arrivalOf = st => Number(new Date(st.arrival || st.scheduledArrival));
// `journeysFromTrip` only supports queries *right now*, so we can't use `when` as in all
// other tests. To make the test less brittle, we pick a connection that is served all night. 🙄
const when = new Date();
const validate = createValidate({...cfg, when});
const findTripBetween = async (stopAId, stopBId, products = {}) => {
const {journeys} = await client.journeys(stopAId, stopBId, {
departure: new Date(when - 10 * minute),
transfers: 0, products,
results: 8, stopovers: false, remarks: false,
});
for (const j of journeys) {
const l = j.legs.find(isU6Leg);
if (!l) {
continue;
}
const t = await client.trip(l.tripId, {
stopovers: true, remarks: false,
});
const pastStopovers = t.stopovers
.filter(st => departureOf(st) < Date.now()); // todo: <= ?
const hasStoppedAtA = pastStopovers
.find(sameStopOrStation({id: stopAId}));
const willStopAtB = t.stopovers
.filter(st => arrivalOf(st) > Date.now()) // todo: >= ?
.find(sameStopOrStation({id: stopBId}));
if (hasStoppedAtA && willStopAtB) {
const prevStopover = maxBy(pastStopovers, departureOf);
return {trip: t, prevStopover};
}
}
return {trip: null, prevStopover: null};
};
// Find a vehicle from U Mehringdamm to U Stadtmitte (to the north) that is currently
// between these two stations.
const {trip, prevStopover} = await findTripBetween(blnMehringdamm, blnStadtmitte, {
regionalExpress: false, regional: false, suburban: false,
});
t.ok(trip, 'precondition failed: trip not found');
t.ok(prevStopover, 'precondition failed: previous stopover missing');
// todo: "Error: Suche aus dem Zug: Vor Abfahrt des Zuges"
const newJourneys = await client.journeysFromTrip(trip.id, prevStopover, blnSpittelmarkt, {
results: 3, stopovers: true, remarks: false,
});
// Validate with fake prices.
const withFakePrice = (j) => {
const clone = Object.assign({}, j);
clone.price = {amount: 123, currency: 'EUR'};
return clone;
};
// todo: there is no such validator!
validate(t, newJourneys.map(withFakePrice), 'journeysFromTrip', 'newJourneys');
for (let i = 0; i < newJourneys.length; i++) {
const j = newJourneys[i];
const n = `newJourneys[${i}]`;
const legOnTrip = j.legs.find(l => l.tripId === trip.id);
t.ok(legOnTrip, n + ': leg with trip ID not found');
t.equal(last(legOnTrip.stopovers).stop.id, blnStadtmitte);
}
});
tap.test('trip details', async (t) => {
const res = await client.journeys(berlinHbf, münchenHbf, {
results: 1, departure: when,
});
const p = res.journeys[0].legs.find(l => !l.walking);
t.ok(p.tripId, 'precondition failed');
t.ok(p.line.name, 'precondition failed');
const tripRes = await client.trip(p.tripId, {when});
const validate = createValidate(cfg, {
trip: (cfg) => {
const validateTrip = createValidateTrip(cfg);
const validateTripWithFakeDirection = (val, trip, name) => {
validateTrip(val, {
...trip,
direction: trip.direction || 'foo', // todo, see #49
}, name);
};
return validateTripWithFakeDirection;
},
});
validate(t, tripRes, 'tripResult', 'tripRes');
t.end();
});
*/
tap.test('departures at Berlin Schwedter Str.', async (t) => {
const res = await client.departures(blnSchwedterStr, {
duration: 5, when,
});
await testDepartures({
test: t,
res,
validate,
id: blnSchwedterStr,
});
t.end();
});
tap.test('departures with station object', async (t) => {
const res = await client.departures({
type: 'station',
id: jungfernheide,
name: 'Berlin Jungfernheide',
location: {
type: 'location',
latitude: 1.23,
longitude: 2.34,
},
}, {when});
validate(t, res, 'departuresResponse', 'res');
t.end();
});
tap.test('arrivals at Berlin Schwedter Str.', async (t) => {
const res = await client.arrivals(blnSchwedterStr, {
duration: 5, when,
});
await testArrivals({
test: t,
res,
validate,
id: blnSchwedterStr,
});
t.end();
});
/*
tap.test('nearby Berlin Jungfernheide', async (t) => {
const nearby = await client.nearby({
type: 'location',
latitude: 52.530273,
longitude: 13.299433,
}, {
results: 2, distance: 400,
});
validate(t, nearby, 'locations', 'nearby');
t.equal(nearby.length, 2);
const s0 = nearby[0];
t.equal(s0.id, jungfernheide);
t.equal(s0.name, 'Berlin Jungfernheide');
t.ok(isRoughlyEqual(0.0005, s0.location.latitude, 52.530408));
t.ok(isRoughlyEqual(0.0005, s0.location.longitude, 13.299424));
t.ok(s0.distance >= 0);
t.ok(s0.distance <= 100);
t.end();
});
tap.test('locations named Jungfernheide', async (t) => {
const locations = await client.locations('Jungfernheide', {
results: 10,
});
validate(t, locations, 'locations', 'locations');
t.ok(locations.length <= 10);
t.ok(locations.some((l) => {
return l.station && l.station.id === jungfernheide || l.id === jungfernheide;
}), 'Jungfernheide not found');
t.end();
});
tap.test('stop', async (t) => {
const s = await client.stop(regensburgHbf);
validate(t, s, ['stop', 'station'], 'stop');
t.equal(s.id, regensburgHbf);
t.end();
});
tap.test('line with additionalName', async (t) => {
const {departures} = await client.departures(potsdamHbf, {
when,
duration: 12 * 60, // 12 minutes
products: {bus: false, suburban: false, tram: false},
});
t.ok(departures.some(d => d.line && d.line.additionalName));
t.end();
});
*/

498
test/e2e/dbweb.js Normal file
View file

@ -0,0 +1,498 @@
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: 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();
});
*/

View file

@ -1,4 +1,4 @@
const dbDepartures = [ const dbwebDepartures = [
{ {
tripId: '2|#VN#1#ST#1738610742#PI#0#ZI#1401845#TA#0#DA#50225#1S#801324#1T#1348#LS#801293#LT#1418#PU#80#RT#2#CA#Bus#ZE#-#ZB#Bus -#PC#5#FR#801324#FT#1348#TO#801293#TT#1418#', tripId: '2|#VN#1#ST#1738610742#PI#0#ZI#1401845#TA#0#DA#50225#1S#801324#1T#1348#LS#801293#LT#1418#PU#80#RT#2#CA#Bus#ZE#-#ZB#Bus -#PC#5#FR#801324#FT#1348#TO#801293#TT#1418#',
stop: { stop: {
@ -4263,5 +4263,5 @@ const dbDepartures = [
]; ];
export { export {
dbDepartures, dbwebDepartures,
}; };

View file

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

View file

@ -1,4 +1,4 @@
const dbTrip = { const dbwebTrip = {
trip: { trip: {
id: 'foo', id: 'foo',
origin: { origin: {
@ -470,5 +470,5 @@ const dbTrip = {
}; };
export { export {
dbTrip, dbwebTrip,
}; };

View file

@ -1,7 +1,7 @@
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../../index.js'; import {createClient} from '../../index.js';
import {profile as rawProfile} from '../../p/db/index.js'; import {profile as rawProfile} from '../../p/dbweb/index.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test'); const client = createClient(rawProfile, 'public-transport/hafas-client:test');
const {profile} = client; const {profile} = client;

View file

@ -1,7 +1,7 @@
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../../index.js'; 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'; import {data as loyaltyCards} from '../../format/loyalty-cards.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false}); 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 ctx = {profile, opt};
const req = profile.formatJourneysReq(ctx, '8098160', '8000284', new Date('2024-12-07T23:50:12+01:00'), true, null); 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(); 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 _opt = {...opt};
const ctx = {profile, opt: _opt}; const ctx = {profile, opt: _opt};