mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Wrap longitude differences in annotation proximity checks and nearby-place distances so points across the antimeridian remain close. Original contribution by quanticsoul4772. Co-authored-by: quanticsoul4772 <[email protected]>
564 lines
17 KiB
JavaScript
564 lines
17 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
googlePlacesContextProxy,
|
|
installRouteMiddleware,
|
|
} from '../../server/providers/places.js';
|
|
import {
|
|
ROUTE_UPSTREAM_MIN_INTERVAL_MS,
|
|
ROUTE_UPSTREAM_QUEUE_MAX,
|
|
_resetRouteUpstreamForTest,
|
|
_routeInflightCountForTest,
|
|
_setRouteUpstreamIntervalForTest,
|
|
} from '../../server/providers/places/routes.js';
|
|
import { ROUTE_STEPS_MAX } from '../../src/data/routeSteps.js';
|
|
import {
|
|
projectNearbyPlaces,
|
|
projectTextSearchPlaces,
|
|
} from '../../src/data/placeProviderPayloads.js';
|
|
|
|
function install(register) {
|
|
const routes = new Map();
|
|
register({
|
|
use(route, handler) {
|
|
routes.set(route, handler);
|
|
},
|
|
});
|
|
return async (route, url, method = 'GET', peer = 'fixture') => {
|
|
const res = {
|
|
statusCode: 200,
|
|
headers: {},
|
|
setHeader(name, value) {
|
|
this.headers[name.toLowerCase()] = value;
|
|
},
|
|
writeHead(status, headers) {
|
|
this.statusCode = status;
|
|
for (const [key, value] of Object.entries(headers))
|
|
this.setHeader(key, value);
|
|
},
|
|
end(body) {
|
|
this.body = JSON.parse(body);
|
|
},
|
|
};
|
|
await routes.get(route)(
|
|
{ url, method, socket: { remoteAddress: peer } },
|
|
res,
|
|
);
|
|
return res;
|
|
};
|
|
}
|
|
|
|
test('nearby labels rank landmarks, deduplicate names/addresses and bound the projection', () => {
|
|
const place = (name, types, longitude = 0) => ({
|
|
displayName: { text: name },
|
|
types,
|
|
location: { latitude: 0, longitude },
|
|
formattedAddress: 'Street',
|
|
});
|
|
const result = projectNearbyPlaces(
|
|
{
|
|
places: [
|
|
place('Bathroom', ['public_bathroom']),
|
|
place('Monument', ['monument'], 0.1),
|
|
place('MONUMENT', ['monument']),
|
|
...Array.from({ length: 25 }, (_, i) =>
|
|
place(`POI ${i}`, ['point_of_interest'], i / 100),
|
|
),
|
|
{ id: 'unnamed' },
|
|
],
|
|
},
|
|
0,
|
|
0,
|
|
);
|
|
assert.equal(result.length, 20);
|
|
assert.equal(result[0].name, 'Monument');
|
|
assert.equal(result[0].distanceM, 11132);
|
|
assert.equal(
|
|
result.filter((p) => p.name.toLowerCase() === 'monument').length,
|
|
1,
|
|
);
|
|
assert.ok(result.every((p) => !('contextPriority' in p)));
|
|
assert.deepEqual(projectNearbyPlaces({}, 0, 0), []);
|
|
});
|
|
|
|
test('nearby place distances take the short way round 180°', () => {
|
|
const at = (name, longitude) => ({
|
|
id: name,
|
|
displayName: { text: name },
|
|
location: { latitude: -16.8, longitude },
|
|
types: ['point_of_interest'],
|
|
});
|
|
// From Taveuni at 179.98: 2 km east across 180°, 4 km west on this side.
|
|
const result = projectNearbyPlaces(
|
|
{ places: [at('West', 179.942), at('East', -179.999)] },
|
|
-16.8,
|
|
179.98,
|
|
);
|
|
const byName = Object.fromEntries(result.map((p) => [p.name, p.distanceM]));
|
|
assert.ok(byName.East > 1500 && byName.East < 2500, String(byName.East));
|
|
assert.ok(byName.West > 3500 && byName.West < 4500, String(byName.West));
|
|
});
|
|
|
|
test('text search preserves bounds, rejects malformed bounds and tolerates absent locations', () => {
|
|
const viewport = {
|
|
low: { latitude: 1, longitude: 2 },
|
|
high: { latitude: 3, longitude: 4 },
|
|
};
|
|
const result = projectTextSearchPlaces(
|
|
{
|
|
places: [
|
|
{
|
|
displayName: { text: 'Museum' },
|
|
viewport,
|
|
types: Array(12).fill('museum'),
|
|
},
|
|
{ displayName: { text: 'Park' }, viewport: { low: viewport.low } },
|
|
],
|
|
},
|
|
0,
|
|
0,
|
|
);
|
|
assert.deepEqual(result[0].viewport, viewport);
|
|
assert.equal(result[0].distanceM, Number.MAX_SAFE_INTEGER);
|
|
assert.equal(result[0].latitude, null);
|
|
assert.equal(result[0].types.length, 8);
|
|
assert.equal(result[1].viewport, null);
|
|
});
|
|
|
|
for (const preview of [false, true]) {
|
|
test(`Google middleware resolves credentials per request and preserves search responses (${preview ? 'preview' : 'dev'})`, async (t) => {
|
|
let key = '';
|
|
const plugin = googlePlacesContextProxy({ resolveApiKey: () => key });
|
|
const request = install((middlewares) =>
|
|
plugin[preview ? 'configurePreviewServer' : 'configureServer']({
|
|
middlewares,
|
|
}),
|
|
);
|
|
const calls = [];
|
|
t.mock.method(globalThis, 'fetch', async (url, options) => {
|
|
calls.push({ url, options });
|
|
assert.equal(options.headers['X-Goog-Api-Key'], key);
|
|
return Response.json({
|
|
places: [
|
|
{
|
|
displayName: { text: 'Museum' },
|
|
location: { latitude: 30, longitude: -97 },
|
|
},
|
|
],
|
|
});
|
|
});
|
|
const nearby = '/api/google/nearby-places';
|
|
const search = '/api/google/text-search';
|
|
assert.equal(
|
|
(await request(nearby, '?lat=30&lon=-97')).body.configured,
|
|
false,
|
|
);
|
|
assert.equal(calls.length, 0);
|
|
key = 'fixture-server-key';
|
|
const result = await request(nearby, '?lat=30&lon=-97&radiusM=9000');
|
|
assert.equal(result.statusCode, 200);
|
|
assert.equal(result.body.places[0].name, 'Museum');
|
|
assert.equal(
|
|
JSON.parse(calls[0].options.body).locationRestriction.circle.radius,
|
|
5000,
|
|
);
|
|
assert.equal(result.headers['cache-control'], 'private, max-age=300');
|
|
key = 'rotated-fixture-key';
|
|
assert.equal(
|
|
(await request(search, '?q=museum&lat=30&lon=-97&radiusM=1')).statusCode,
|
|
200,
|
|
);
|
|
assert.equal(
|
|
JSON.parse(calls[1].options.body).locationBias.circle.radius,
|
|
50,
|
|
);
|
|
assert.equal((await request(search, '?lat=30&lon=-97')).statusCode, 400);
|
|
assert.equal((await request(nearby, '?lat=bad&lon=-97')).statusCode, 400);
|
|
assert.equal((await request(nearby, '', 'POST')).statusCode, 405);
|
|
t.mock.method(globalThis, 'fetch', async () =>
|
|
Response.json({ error: { message: 'Denied' } }, { status: 403 }),
|
|
);
|
|
assert.deepEqual((await request(search, '?q=museum&lat=30&lon=-97')).body, {
|
|
places: [],
|
|
error: 'Denied',
|
|
});
|
|
});
|
|
}
|
|
|
|
test('OSRM routing preserves aliases, cache, span guards and upstream failure behavior', async (t) => {
|
|
const request = install(installRouteMiddleware);
|
|
let calls = 0;
|
|
let now = Date.now();
|
|
t.mock.method(Date, 'now', () => now);
|
|
t.mock.method(globalThis, 'fetch', async (url) => {
|
|
calls++;
|
|
assert.match(url, /routed-car\/route\/v1\/driving\//);
|
|
return Response.json({
|
|
code: 'Ok',
|
|
routes: [
|
|
{
|
|
distance: 123.6,
|
|
duration: 80.2,
|
|
geometry: {
|
|
coordinates: [
|
|
[-97, 30],
|
|
[-97.01, 30.01],
|
|
],
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
const route = '/api/route';
|
|
const query = '?profile=driving&coords=-97,30;-97.01,30.01';
|
|
const result = await request(route, query);
|
|
assert.deepEqual(result.body, {
|
|
ok: true,
|
|
profile: 'car',
|
|
distanceM: 124,
|
|
durationS: 80,
|
|
geometry: [
|
|
[-97, 30],
|
|
[-97.01, 30.01],
|
|
],
|
|
});
|
|
assert.deepEqual(
|
|
(await request(route, query.replace('driving', 'car'))).body,
|
|
result.body,
|
|
);
|
|
assert.equal(calls, 1);
|
|
for (const [url, error] of [
|
|
['?profile=plane&coords=0,0;1,1', 'invalid profile'],
|
|
['?coords=0,91;1,1', 'invalid coordinate'],
|
|
['?coords=0,0;100,0', 'route leg too long'],
|
|
['?coords=0,0', 'need 2-12 coordinates'],
|
|
])
|
|
assert.equal((await request(route, url)).body.error, error);
|
|
assert.equal(calls, 1);
|
|
now += 600001;
|
|
t.mock.method(
|
|
globalThis,
|
|
'fetch',
|
|
async () => new Response('offline', { status: 503 }),
|
|
);
|
|
assert.deepEqual((await request(route, query)).body, {
|
|
ok: false,
|
|
error: 'no route found',
|
|
});
|
|
t.mock.method(
|
|
globalThis,
|
|
'fetch',
|
|
async () => new Response('x', { headers: { 'content-type': 'text/html' } }),
|
|
);
|
|
assert.equal((await request(route, query)).body.error, 'no route found');
|
|
});
|
|
|
|
/** One OSRM route with `count` maneuvers, for the step tests. */
|
|
function osrmRouteWithSteps(count) {
|
|
return {
|
|
code: 'Ok',
|
|
routes: [
|
|
{
|
|
distance: 123.6,
|
|
duration: 80.2,
|
|
geometry: {
|
|
coordinates: [
|
|
[-97, 30],
|
|
[-97.01, 30.01],
|
|
],
|
|
},
|
|
legs: [
|
|
{
|
|
steps: Array.from({ length: count }, (_, i) => ({
|
|
name: `Road ${i}`,
|
|
distance: 10,
|
|
duration: 5,
|
|
maneuver: {
|
|
type: i === 0 ? 'depart' : 'turn',
|
|
modifier: 'left',
|
|
location: [-97 + i / 1000, 30],
|
|
},
|
|
})),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
test('steps are opt-in: the upstream is only asked for them when a caller is', async (t) => {
|
|
_resetRouteUpstreamForTest();
|
|
const request = install(installRouteMiddleware);
|
|
const asked = [];
|
|
t.mock.method(globalThis, 'fetch', async (url) => {
|
|
asked.push(String(url));
|
|
return Response.json(osrmRouteWithSteps(3));
|
|
});
|
|
const route = '/api/route';
|
|
const query = '?profile=car&coords=-97,30;-97.01,30.01';
|
|
|
|
// The voice route annotation and fly_route do not read maneuvers. What they
|
|
// get back must be exactly what this endpoint returned before steps existed.
|
|
const plain = await request(route, query);
|
|
assert.deepEqual(plain.body, {
|
|
ok: true,
|
|
profile: 'car',
|
|
distanceM: 124,
|
|
durationS: 80,
|
|
geometry: [
|
|
[-97, 30],
|
|
[-97.01, 30.01],
|
|
],
|
|
});
|
|
assert.equal('steps' in plain.body, false, 'no steps key at all');
|
|
assert.match(asked[0], /steps=false/, 'the upstream payload stays small');
|
|
|
|
// Directions asks, and gets them.
|
|
_resetRouteUpstreamForTest();
|
|
const withSteps = await request(route, `${query}&steps=1`);
|
|
assert.equal(asked.length, 2);
|
|
assert.match(asked[1], /steps=true/);
|
|
assert.equal(withSteps.body.steps.length, 3);
|
|
assert.equal(withSteps.body.steps[0].instruction, 'Head out on Road 0');
|
|
assert.equal(withSteps.body.steps[1].instruction, 'Turn left onto Road 1');
|
|
|
|
// A stepful cache entry still serves a stepless caller its own shape.
|
|
const reuse = await request(route, query);
|
|
assert.equal(asked.length, 2, 'served from cache');
|
|
assert.deepEqual(
|
|
reuse.body,
|
|
plain.body,
|
|
'byte-identical to the no-steps shape',
|
|
);
|
|
});
|
|
|
|
test('identical concurrent route requests make one upstream call', async (t) => {
|
|
_resetRouteUpstreamForTest();
|
|
const request = install(installRouteMiddleware);
|
|
let calls = 0;
|
|
let release;
|
|
const gate = new Promise((resolve) => {
|
|
release = resolve;
|
|
});
|
|
t.mock.method(globalThis, 'fetch', async () => {
|
|
calls += 1;
|
|
await gate;
|
|
return Response.json(osrmRouteWithSteps(4));
|
|
});
|
|
const route = '/api/route';
|
|
const query = '?profile=car&coords=-97,30;-97.01,30.01&steps=1';
|
|
|
|
// Three rapid reroutes of the same A→B, before any of them answers.
|
|
const all = [
|
|
request(route, query),
|
|
request(route, query),
|
|
request(route, query),
|
|
];
|
|
// ...plus a caller that wants no steps; it rides the same call.
|
|
all.push(request(route, '?profile=car&coords=-97,30;-97.01,30.01'));
|
|
await Promise.resolve();
|
|
assert.equal(
|
|
_routeInflightCountForTest(),
|
|
1,
|
|
'one upstream call is in flight',
|
|
);
|
|
release();
|
|
const results = await Promise.all(all);
|
|
assert.equal(calls, 1, 'the FOSSGIS servers saw one request, not four');
|
|
assert.equal(results[0].body.steps.length, 4);
|
|
assert.deepEqual(results[0].body, results[1].body);
|
|
assert.deepEqual(results[0].body, results[2].body);
|
|
assert.equal(
|
|
'steps' in results[3].body,
|
|
false,
|
|
'the stepless caller still gets no steps',
|
|
);
|
|
assert.equal(
|
|
_routeInflightCountForTest(),
|
|
0,
|
|
'the in-flight entry is released',
|
|
);
|
|
});
|
|
|
|
test('a pathological maneuver count is bounded before it reaches the browser', async (t) => {
|
|
_resetRouteUpstreamForTest();
|
|
const request = install(installRouteMiddleware);
|
|
t.mock.method(globalThis, 'fetch', async () =>
|
|
Response.json(osrmRouteWithSteps(5000)),
|
|
);
|
|
const result = await request(
|
|
'/api/route',
|
|
'?profile=car&coords=-97,30;-97.01,30.01&steps=1',
|
|
);
|
|
assert.equal(result.body.steps.length, ROUTE_STEPS_MAX);
|
|
assert.ok(ROUTE_STEPS_MAX <= 200, 'the cap stays a cap');
|
|
});
|
|
|
|
test('outbound route requests are spaced to the rate the routing service asks for', async (t) => {
|
|
_resetRouteUpstreamForTest();
|
|
const request = install(installRouteMiddleware);
|
|
const sentAt = [];
|
|
t.mock.method(globalThis, 'fetch', async (url) => {
|
|
sentAt.push({ at: Date.now(), url: String(url) });
|
|
return Response.json(osrmRouteWithSteps(2));
|
|
});
|
|
// FOSSGIS publish "one request per second max". The per-client limiter alone
|
|
// cannot honour that: a DRIVE and a WALK of the same trip are two different
|
|
// cache keys and used to leave 48 ms apart.
|
|
assert.ok(ROUTE_UPSTREAM_MIN_INTERVAL_MS >= 1000);
|
|
const first = await request(
|
|
'/api/route',
|
|
'?profile=car&coords=-97,30;-97.01,30.01',
|
|
);
|
|
assert.equal(first.body.ok, true);
|
|
assert.equal(sentAt.length, 1);
|
|
|
|
const second = request(
|
|
'/api/route',
|
|
'?profile=foot&coords=-97,30;-97.01,30.01',
|
|
);
|
|
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
assert.equal(sentAt.length, 1, 'the second profile waits for its slot');
|
|
assert.equal((await second).body.ok, true);
|
|
assert.ok(
|
|
sentAt[1].at - sentAt[0].at >= ROUTE_UPSTREAM_MIN_INTERVAL_MS - 20,
|
|
`two outbound calls ${sentAt[1].at - sentAt[0].at} ms apart`,
|
|
);
|
|
// A cached route costs no outbound slot at all.
|
|
const cached = await request(
|
|
'/api/route',
|
|
'?profile=car&coords=-97,30;-97.01,30.01',
|
|
);
|
|
assert.equal(cached.body.ok, true);
|
|
assert.equal(sentAt.length, 2, 'the cache answers without an upstream call');
|
|
});
|
|
|
|
test('past the outbound queue depth the answer is an honest 429, not a growing queue', async (t) => {
|
|
_resetRouteUpstreamForTest();
|
|
// The real gate is one per second (asserted above); this test is about what
|
|
// happens past the queue depth, so it runs the gate fast.
|
|
_setRouteUpstreamIntervalForTest(5);
|
|
t.after(() => _resetRouteUpstreamForTest());
|
|
const request = install(installRouteMiddleware);
|
|
t.mock.method(globalThis, 'fetch', async () =>
|
|
Response.json(osrmRouteWithSteps(2)),
|
|
);
|
|
const pending = [];
|
|
for (let i = 0; i < ROUTE_UPSTREAM_QUEUE_MAX + 3; i += 1) {
|
|
pending.push(
|
|
request(
|
|
'/api/route',
|
|
`?profile=car&coords=-97,${30 + i / 100};-97.01,30.01`,
|
|
),
|
|
);
|
|
}
|
|
const results = await Promise.all(pending);
|
|
const refused = results.filter((result) => result.statusCode === 429);
|
|
assert.ok(refused.length >= 1, 'the excess is refused rather than queued');
|
|
assert.match(refused[0].body.error, /routing busy/);
|
|
assert.ok(
|
|
Number(refused[0].headers['retry-after']) >= 1,
|
|
`Retry-After is ${refused[0].headers['retry-after']}`,
|
|
);
|
|
// Everything that was admitted still got a real answer.
|
|
assert.ok(results.some((result) => result.body.ok === true));
|
|
});
|
|
|
|
test('an upstream rate limit is reported as one, not as "no route found"', async (t) => {
|
|
_resetRouteUpstreamForTest();
|
|
const request = install(installRouteMiddleware);
|
|
t.mock.method(
|
|
globalThis,
|
|
'fetch',
|
|
async () =>
|
|
new Response('slow down', {
|
|
status: 429,
|
|
headers: { 'retry-after': '30', 'content-type': 'text/plain' },
|
|
}),
|
|
);
|
|
const result = await request(
|
|
'/api/route',
|
|
'?profile=car&coords=-97,30;-97.01,30.01',
|
|
);
|
|
assert.equal(result.statusCode, 429);
|
|
assert.match(result.body.error, /routing service is rate limited/);
|
|
assert.equal(
|
|
result.headers['retry-after'],
|
|
'30',
|
|
"the service's own wait is passed on",
|
|
);
|
|
});
|
|
|
|
test('the upstream host is pinned: a redirect is refused, not followed', async (t) => {
|
|
_resetRouteUpstreamForTest();
|
|
const request = install(installRouteMiddleware);
|
|
const seen = [];
|
|
t.mock.method(globalThis, 'fetch', async (url, options) => {
|
|
seen.push(options?.redirect);
|
|
return Response.json(osrmRouteWithSteps(2));
|
|
});
|
|
await request('/api/route', '?profile=car&coords=-97,30;-97.01,30.01');
|
|
assert.deepEqual(seen, ['error'], 'a redirect would escape the pinned host');
|
|
});
|
|
|
|
test('both paid Google routes refuse cross-site requests before calling Google', async (t) => {
|
|
const plugin = googlePlacesContextProxy({ resolveApiKey: () => 'key' });
|
|
const routes = new Map();
|
|
plugin.configureServer({
|
|
middlewares: { use: (route, handler) => routes.set(route, handler) },
|
|
});
|
|
let upstream = 0;
|
|
t.mock.method(globalThis, 'fetch', async () => {
|
|
upstream += 1;
|
|
return Response.json({ places: [] });
|
|
});
|
|
const call = async (route, url, headers) => {
|
|
const res = {
|
|
statusCode: 200,
|
|
headers: {},
|
|
setHeader(name, value) {
|
|
this.headers[name.toLowerCase()] = value;
|
|
},
|
|
writeHead(status) {
|
|
this.statusCode = status;
|
|
},
|
|
end() {},
|
|
};
|
|
await routes.get(route)(
|
|
{
|
|
url,
|
|
method: 'GET',
|
|
headers: { host: 'localhost:4173', ...headers },
|
|
socket: { remoteAddress: '127.0.0.1' },
|
|
},
|
|
res,
|
|
);
|
|
return res.statusCode;
|
|
};
|
|
const crossSite = {
|
|
origin: 'https://attacker.example',
|
|
'sec-fetch-site': 'cross-site',
|
|
};
|
|
for (const [route, url] of [
|
|
['/api/google/nearby-places', '?lat=30&lon=-97'],
|
|
['/api/google/text-search', '?q=museum&lat=30&lon=-97'],
|
|
]) {
|
|
assert.equal(await call(route, url, crossSite), 403, route);
|
|
assert.equal(
|
|
await call(route, url, { 'sec-fetch-site': 'cross-site' }),
|
|
403,
|
|
route,
|
|
);
|
|
}
|
|
assert.equal(upstream, 0, 'a refused request never reaches Google');
|
|
// The app's own page still gets answers.
|
|
assert.equal(
|
|
await call('/api/google/text-search', '?q=museum&lat=30&lon=-97', {
|
|
origin: 'http://localhost:4173',
|
|
'sec-fetch-site': 'same-origin',
|
|
}),
|
|
200,
|
|
);
|
|
assert.equal(upstream, 1);
|
|
});
|