MotorSignal-App/server/waze_relay/test/routing_and_places.test.js

387 lines
12 KiB
JavaScript

const { describe, it } = require('node:test');
const assert = require('node:assert');
const axios = require('axios');
const routingRouter = require('../routes/routing');
const placesRouter = require('../routes/places');
describe('Routing Input Hardening & Validations', () => {
const calcLayer = routingRouter.stack.find((l) => l.route && l.route.path === '/calculate');
assert.ok(calcLayer, '/calculate endpoint must exist');
const calcHandler = calcLayer.route.stack[calcLayer.route.stack.length - 1].handle;
it('rejects less than 2 stops with 400 invalidRequest', async () => {
process.env.STADIA_API_KEY = 'test-key';
try {
const req = { body: { locations: [{ lat: 37.77, lon: -122.42 }] } };
let statusCode = null;
let body = null;
const res = {
status: (c) => {
statusCode = c;
return { json: (b) => { body = b; } };
}
};
await calcHandler(req, res);
assert.strictEqual(statusCode, 400);
assert.strictEqual(body?.error, 'invalidRequest');
assert.match(body?.message, /At least 2 locations/);
} finally {
delete process.env.STADIA_API_KEY;
}
});
it('rejects more than 25 stops with 400 invalidRequest', async () => {
process.env.STADIA_API_KEY = 'test-key';
try {
const stops = Array.from({ length: 26 }, (_, i) => ({
lat: 37.0 + i * 0.01,
lon: -122.0
}));
const req = { body: { locations: stops } };
let statusCode = null;
let body = null;
const res = {
status: (c) => {
statusCode = c;
return { json: (b) => { body = b; } };
}
};
await calcHandler(req, res);
assert.strictEqual(statusCode, 400);
assert.strictEqual(body?.error, 'invalidRequest');
assert.match(body?.message, /Maximum of 25/);
} finally {
delete process.env.STADIA_API_KEY;
}
});
it('accepts exactly 25 stops and passes motorcycle costing to provider', async () => {
process.env.STADIA_API_KEY = 'test-secret-key-123';
const originalPost = axios.post;
try {
let interceptedUrl = null;
let interceptedPayload = null;
let interceptedParams = null;
axios.post = async (url, payload, config) => {
interceptedUrl = url;
interceptedPayload = payload;
interceptedParams = config?.params;
return {
data: {
trip: {
legs: [
{
summary: { length: 15.5, time: 1200 },
shape: 'test_encoded_polyline'
}
],
warnings: []
}
}
};
};
const stops = Array.from({ length: 25 }, (_, i) => ({
lat: 37.0 + i * 0.01,
lon: -122.0
}));
const req = { body: { locations: stops } };
let responseData = null;
const res = {
json: (b) => { responseData = b; }
};
await calcHandler(req, res);
assert.strictEqual(interceptedUrl, 'https://api.stadiamaps.com/route/v1');
assert.strictEqual(interceptedParams?.api_key, 'test-secret-key-123');
assert.strictEqual(interceptedPayload?.costing, 'motorcycle');
assert.strictEqual(interceptedPayload?.locations?.length, 25);
assert.strictEqual(responseData?.distanceMeters, 15500);
assert.strictEqual(responseData?.durationSeconds, 1200);
assert.strictEqual(responseData?.encodedPolyline, 'test_encoded_polyline');
// Verify API key is NOT leaked in client response
assert.strictEqual(JSON.stringify(responseData).includes('test-secret-key-123'), false);
} finally {
axios.post = originalPost;
delete process.env.STADIA_API_KEY;
}
});
it('sanitizes upstream timeout error to 504 providerTimeout without leaking internals', async () => {
process.env.STADIA_API_KEY = 'test-key';
const originalPost = axios.post;
try {
axios.post = async () => {
const err = new Error('connect ECONNABORTED');
err.code = 'ECONNABORTED';
throw err;
};
const req = {
body: {
locations: [
{ lat: 37.7, lon: -122.4 },
{ lat: 37.8, lon: -122.4 }
]
}
};
let statusCode = null;
let body = null;
const res = {
status: (c) => {
statusCode = c;
return { json: (b) => { body = b; } };
}
};
await calcHandler(req, res);
assert.strictEqual(statusCode, 504);
assert.strictEqual(body?.error, 'providerTimeout');
assert.strictEqual(JSON.stringify(body).includes('ECONNABORTED'), false);
} finally {
axios.post = originalPost;
delete process.env.STADIA_API_KEY;
}
});
it('rejects invalid latitude bounds (< -90 or > 90)', async () => {
process.env.STADIA_API_KEY = 'test-key';
try {
const req = {
body: {
locations: [
{ lat: 95.0, lon: -122.4 },
{ lat: 37.7, lon: -122.4 }
]
}
};
let statusCode = null;
let body = null;
const res = {
status: (c) => {
statusCode = c;
return { json: (b) => { body = b; } };
}
};
await calcHandler(req, res);
assert.strictEqual(statusCode, 400);
assert.strictEqual(body?.error, 'invalidRequest');
assert.match(body?.message, /out of geographic bounds/);
} finally {
delete process.env.STADIA_API_KEY;
}
});
it('rejects invalid longitude bounds (< -180 or > 180)', async () => {
process.env.STADIA_API_KEY = 'test-key';
try {
const req = {
body: {
locations: [
{ lat: 37.7, lon: -185.0 },
{ lat: 37.7, lon: -122.4 }
]
}
};
let statusCode = null;
let body = null;
const res = {
status: (c) => {
statusCode = c;
return { json: (b) => { body = b; } };
}
};
await calcHandler(req, res);
assert.strictEqual(statusCode, 400);
assert.strictEqual(body?.error, 'invalidRequest');
assert.match(body?.message, /out of geographic bounds/);
} finally {
delete process.env.STADIA_API_KEY;
}
});
it('rejects non-finite coordinates (NaN/Infinity)', async () => {
process.env.STADIA_API_KEY = 'test-key';
try {
const req = {
body: {
locations: [
{ lat: NaN, lon: -122.4 },
{ lat: 37.7, lon: -122.4 }
]
}
};
let statusCode = null;
let body = null;
const res = {
status: (c) => {
statusCode = c;
return { json: (b) => { body = b; } };
}
};
await calcHandler(req, res);
assert.strictEqual(statusCode, 400);
assert.strictEqual(body?.error, 'invalidRequest');
assert.match(body?.message, /Invalid or non-finite/);
} finally {
delete process.env.STADIA_API_KEY;
}
});
it('fails with 503 providerUnavailable if STADIA_API_KEY is not configured', async () => {
delete process.env.STADIA_API_KEY;
const req = { body: { locations: [{ lat: 37.7, lon: -122.4 }, { lat: 37.8, lon: -122.4 }] } };
let statusCode = null;
let body = null;
const res = {
status: (c) => {
statusCode = c;
return { json: (b) => { body = b; } };
}
};
await calcHandler(req, res);
assert.strictEqual(statusCode, 503);
assert.strictEqual(body?.error, 'providerUnavailable');
});
});
describe('Places Input Hardening & Validations', () => {
const searchLayer = placesRouter.stack.find((l) => l.route && l.route.path === '/search');
assert.ok(searchLayer, '/search endpoint must exist');
const searchHandler = searchLayer.route.stack[searchLayer.route.stack.length - 1].handle;
const resolveLayer = placesRouter.stack.find((l) => l.route && l.route.path === '/resolve');
assert.ok(resolveLayer, '/resolve endpoint must exist');
const resolveHandler = resolveLayer.route.stack[resolveLayer.route.stack.length - 1].handle;
it('search rejects empty or short query with 400', async () => {
process.env.STADIA_API_KEY = 'test-key';
try {
const req = { query: { query: 'a' } };
let statusCode = null;
let body = null;
const res = {
status: (c) => {
statusCode = c;
return { json: (b) => { body = b; } };
}
};
await searchHandler(req, res);
assert.strictEqual(statusCode, 400);
assert.strictEqual(body?.error, 'invalidRequest');
} finally {
delete process.env.STADIA_API_KEY;
}
});
it('search rejects excessive query (>200 chars) with 400', async () => {
process.env.STADIA_API_KEY = 'test-key';
try {
const req = { query: { query: 'A'.repeat(201) } };
let statusCode = null;
let body = null;
const res = {
status: (c) => {
statusCode = c;
return { json: (b) => { body = b; } };
}
};
await searchHandler(req, res);
assert.strictEqual(statusCode, 400);
assert.strictEqual(body?.error, 'invalidRequest');
} finally {
delete process.env.STADIA_API_KEY;
}
});
it('search normalizes Pelias GeoJSON features into place results with coordinates', async () => {
process.env.STADIA_API_KEY = 'test-places-key';
const originalGet = axios.get;
try {
axios.get = async (url, config) => {
assert.strictEqual(url, 'https://api.stadiamaps.com/geocoding/v1/autocomplete');
assert.strictEqual(config?.params?.api_key, 'test-places-key');
return {
data: {
features: [
{
properties: {
id: 'venue_123',
name: 'Skyline Boulevard',
label: 'Skyline Boulevard, Oakland, CA',
region: 'California',
country: 'USA'
},
geometry: {
coordinates: [-122.18, 37.81]
}
}
]
}
};
};
const req = { query: { query: 'Skyline' } };
let responseData = null;
const res = {
json: (d) => { responseData = d; }
};
await searchHandler(req, res);
assert.ok(Array.isArray(responseData?.results));
assert.strictEqual(responseData.results.length, 1);
const place = responseData.results[0];
assert.strictEqual(place.providerId, 'venue_123');
assert.strictEqual(place.primaryText, 'Skyline Boulevard');
assert.strictEqual(place.latitude, 37.81);
assert.strictEqual(place.longitude, -122.18);
} finally {
axios.get = originalGet;
delete process.env.STADIA_API_KEY;
}
});
it('resolve rejects missing or empty placeId with 400', async () => {
process.env.STADIA_API_KEY = 'test-key';
try {
const req = { query: { placeId: ' ' } };
let statusCode = null;
let body = null;
const res = {
status: (c) => {
statusCode = c;
return { json: (b) => { body = b; } };
}
};
await resolveHandler(req, res);
assert.strictEqual(statusCode, 400);
assert.strictEqual(body?.error, 'invalidRequest');
} finally {
delete process.env.STADIA_API_KEY;
}
});
it('resolve rejects excessive placeId (>200 chars) with 400', async () => {
process.env.STADIA_API_KEY = 'test-key';
try {
const req = { query: { placeId: 'x'.repeat(201) } };
let statusCode = null;
let body = null;
const res = {
status: (c) => {
statusCode = c;
return { json: (b) => { body = b; } };
}
};
await resolveHandler(req, res);
assert.strictEqual(statusCode, 400);
assert.strictEqual(body?.error, 'invalidRequest');
} finally {
delete process.env.STADIA_API_KEY;
}
});
});