← back to Directory Core
test/geo.test.ts
193 lines
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import {
haversineMi,
haversineKm,
geocodeZip,
makePgZipCache,
type LatLng,
type ZipCacheAdapter,
} from '../src/geo.js';
describe('haversineMi', () => {
test('returns 0 for identical points', () => {
const p: LatLng = { lat: 34.0522, lng: -118.2437 };
assert.equal(haversineMi(p, p), 0);
});
test('LA to NYC is ~2445 mi (within 1%)', () => {
const la: LatLng = { lat: 34.0522, lng: -118.2437 };
const nyc: LatLng = { lat: 40.7128, lng: -74.006 };
const d = haversineMi(la, nyc);
assert.ok(d > 2420 && d < 2470, `expected ~2445 mi, got ${d}`);
});
test('symmetry: d(a,b) == d(b,a)', () => {
const a: LatLng = { lat: 34.05, lng: -118.24 };
const b: LatLng = { lat: 40.71, lng: -74.0 };
assert.equal(haversineMi(a, b), haversineMi(b, a));
});
test('handles antipodal points (max distance ~12,438 mi)', () => {
const a: LatLng = { lat: 0, lng: 0 };
const b: LatLng = { lat: 0, lng: 180 };
const d = haversineMi(a, b);
// half earth circumference at equator
assert.ok(d > 12400 && d < 12500, `antipodal expected ~12438 mi, got ${d}`);
});
});
describe('haversineKm', () => {
test('returns 0 for identical points', () => {
const p: LatLng = { lat: 51.5074, lng: -0.1278 };
assert.equal(haversineKm(p, p), 0);
});
test('London to Paris is ~344 km (within 2%)', () => {
const london: LatLng = { lat: 51.5074, lng: -0.1278 };
const paris: LatLng = { lat: 48.8566, lng: 2.3522 };
const d = haversineKm(london, paris);
assert.ok(d > 337 && d < 351, `expected ~344 km, got ${d}`);
});
test('km result ≈ 1.609 × mi result for the same pair', () => {
const a: LatLng = { lat: 34.05, lng: -118.24 };
const b: LatLng = { lat: 40.71, lng: -74.0 };
const mi = haversineMi(a, b);
const km = haversineKm(a, b);
const ratio = km / mi;
assert.ok(ratio > 1.605 && ratio < 1.615, `mi-to-km ratio off: ${ratio}`);
});
});
describe('geocodeZip — input validation', () => {
test('rejects null/undefined', async () => {
assert.equal(await geocodeZip(null), null);
assert.equal(await geocodeZip(undefined), null);
});
test('rejects non-5-digit strings', async () => {
assert.equal(await geocodeZip(''), null);
assert.equal(await geocodeZip('1234'), null);
assert.equal(await geocodeZip('abcde'), null);
assert.equal(await geocodeZip('123456'), null);
});
test('accepts numeric ZIP', async () => {
// numeric input that's only 5 digits should pass validation; we mock fetch
// to confirm it gets through to the URL construction.
let calledUrl = '';
const fetchImpl = (async (url: string) => {
calledUrl = url;
return new Response(JSON.stringify([{ lat: '34.09', lon: '-118.4' }]), { status: 200 });
}) as typeof fetch;
const r = await geocodeZip(90210, { fetchImpl });
assert.ok(calledUrl.includes('90210'), `url should include 90210, got ${calledUrl}`);
assert.deepEqual(r, { lat: 34.09, lng: -118.4, source: 'nominatim' });
});
});
describe('geocodeZip — cache path', () => {
test('cache hit short-circuits Nominatim', async () => {
let fetchCalled = false;
const fetchImpl = (async () => {
fetchCalled = true;
return new Response('[]', { status: 200 });
}) as typeof fetch;
const cache: ZipCacheAdapter = {
async get() { return { lat: 34.0, lng: -118.0 }; },
async put() { /* not called */ },
};
const r = await geocodeZip('90210', { cache, fetchImpl });
assert.equal(fetchCalled, false, 'fetch should not be called on cache hit');
assert.deepEqual(r, { lat: 34.0, lng: -118.0, source: 'cache' });
});
test('cache miss → fetch → put', async () => {
const puts: Array<[string, LatLng, string]> = [];
const fetchImpl = (async () =>
new Response(JSON.stringify([{ lat: '40.7128', lon: '-74.006' }]), { status: 200 })) as typeof fetch;
const cache: ZipCacheAdapter = {
async get() { return null; },
async put(zip, coords, source) { puts.push([zip, coords, source]); },
};
const r = await geocodeZip('10001', { cache, fetchImpl });
assert.equal(r?.lat, 40.7128);
assert.equal(r?.lng, -74.006);
assert.equal(r?.source, 'nominatim');
assert.equal(puts.length, 1);
assert.equal(puts[0][0], '10001');
assert.equal(puts[0][2], 'nominatim');
});
});
describe('geocodeZip — failure modes', () => {
test('returns null on Nominatim non-200', async () => {
const fetchImpl = (async () => new Response('rate limited', { status: 429 })) as typeof fetch;
assert.equal(await geocodeZip('90210', { fetchImpl }), null);
});
test('returns null on empty result array', async () => {
const fetchImpl = (async () => new Response('[]', { status: 200 })) as typeof fetch;
assert.equal(await geocodeZip('90210', { fetchImpl }), null);
});
test('returns null on fetch throw', async () => {
const fetchImpl = (async () => { throw new Error('network down'); }) as typeof fetch;
assert.equal(await geocodeZip('90210', { fetchImpl }), null);
});
test('cache.put failure does not break the geocode result', async () => {
const fetchImpl = (async () =>
new Response(JSON.stringify([{ lat: '34', lon: '-118' }]), { status: 200 })) as typeof fetch;
const cache: ZipCacheAdapter = {
async get() { return null; },
async put() { throw new Error('db down'); },
};
const r = await geocodeZip('90210', { cache, fetchImpl });
assert.equal(r?.lat, 34);
});
});
describe('makePgZipCache', () => {
test('get translates row to LatLng', async () => {
const queryFn = async <T = unknown>(_text: string, _params: unknown[]) =>
({ rows: [{ lat: 34.05, lng: -118.24 }] as unknown as T[] });
const cache = makePgZipCache(queryFn);
const r = await cache.get('90210');
assert.deepEqual(r, { lat: 34.05, lng: -118.24 });
});
test('get returns null when no rows', async () => {
const queryFn = async <T = unknown>(_t: string, _p: unknown[]) => ({ rows: [] as T[] });
const cache = makePgZipCache(queryFn);
assert.equal(await cache.get('00000'), null);
});
test('put issues an INSERT and returns', async () => {
let calledText = '';
let calledParams: unknown[] = [];
const queryFn = async <T = unknown>(text: string, params: unknown[]) => {
calledText = text;
calledParams = params;
return { rows: [] as T[] };
};
const cache = makePgZipCache(queryFn);
await cache.put('90210', { lat: 34, lng: -118 }, 'nominatim');
assert.match(calledText, /INSERT INTO zip_centroids/);
assert.match(calledText, /ON CONFLICT \(zip\) DO NOTHING/);
assert.deepEqual(calledParams, ['90210', 34, -118, 'nominatim']);
});
test('put honors custom table name', async () => {
let calledText = '';
const queryFn = async <T = unknown>(text: string, _p: unknown[]) => {
calledText = text;
return { rows: [] as T[] };
};
const cache = makePgZipCache(queryFn, 'my_custom_zips');
await cache.put('90210', { lat: 34, lng: -118 }, 'nominatim');
assert.match(calledText, /INSERT INTO my_custom_zips/);
});
});