← back to Directory Core
test/match.test.ts
205 lines
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import {
BBOX_CA,
BBOX_US_CONTINENTAL,
CENTROID_LA,
resolveZipAnchor,
haversineKmSql,
withinBoundingBoxSql,
type QueryFn,
type EmptyReason,
} from '../src/match.js';
describe('Constants', () => {
test('CA bbox covers LA + SF + the Oregon/Mexico borders', () => {
// LA
assert.ok(34.05 >= BBOX_CA.latMin && 34.05 <= BBOX_CA.latMax);
assert.ok(-118.24 >= BBOX_CA.lngMin && -118.24 <= BBOX_CA.lngMax);
// SF
assert.ok(37.77 >= BBOX_CA.latMin && 37.77 <= BBOX_CA.latMax);
// CA bounds excludes Oregon (Portland 45.5 > 42.05)
assert.ok(45.5 > BBOX_CA.latMax);
// CA bounds excludes Tijuana (32.5 boundary; TJ at 32.51 is barely in)
assert.equal(BBOX_CA.latMin, 32.5);
});
test('Continental US bbox includes both coasts but excludes AK', () => {
// Seattle
assert.ok(47.6 >= BBOX_US_CONTINENTAL.latMin && 47.6 <= BBOX_US_CONTINENTAL.latMax);
// Miami
assert.ok(25.7 >= BBOX_US_CONTINENTAL.latMin && 25.7 <= BBOX_US_CONTINENTAL.latMax);
// Anchorage AK at 61.2 — should be EXCLUDED
assert.ok(61.2 > BBOX_US_CONTINENTAL.latMax);
});
test('LA centroid matches downtown LA coords', () => {
assert.equal(CENTROID_LA.lat, 34.0522);
assert.equal(CENTROID_LA.lng, -118.2437);
});
});
describe('resolveZipAnchor — input validation', () => {
test('returns null for null/undefined zip', async () => {
const q: QueryFn = async () => ({ rows: [] });
assert.equal(await resolveZipAnchor(q, null, { table: 'organizations' }), null);
assert.equal(await resolveZipAnchor(q, undefined, { table: 'organizations' }), null);
});
test('returns null for non-numeric zip', async () => {
const q: QueryFn = async () => ({ rows: [] });
assert.equal(await resolveZipAnchor(q, 'abcde', { table: 'organizations' }), null);
});
test('returns null for zip too short (< 3 chars)', async () => {
const q: QueryFn = async () => ({ rows: [] });
assert.equal(await resolveZipAnchor(q, '12', { table: 'organizations' }), null);
});
test('returns null for zip too long (> 5 chars)', async () => {
const q: QueryFn = async () => ({ rows: [] });
assert.equal(await resolveZipAnchor(q, '123456', { table: 'organizations' }), null);
});
});
describe('resolveZipAnchor — exact match path', () => {
test('returns centroid from exact ZIP match', async () => {
const calls: Array<{ text: string; params: unknown[] }> = [];
const q: QueryFn = async <T = unknown>(text: string, params: unknown[]) => {
calls.push({ text, params });
return { rows: [{ lat: 34.05, lng: -118.24, n: 7 }] as unknown as T[] };
};
const r = await resolveZipAnchor(q, '90210', { table: 'organizations' });
assert.deepEqual(r, { lat: 34.05, lng: -118.24 });
assert.equal(calls.length, 1, 'should not need fallback when exact match returns rows');
assert.match(calls[0].text, /WHERE zip = \$1/);
assert.equal(calls[0].params[0], '90210');
});
test('falls through to prefix when exact ZIP returns 0 rows', async () => {
const calls: Array<{ text: string; params: unknown[] }> = [];
const q: QueryFn = async <T = unknown>(text: string, params: unknown[]) => {
calls.push({ text, params });
// First call (exact): no rows. Second call (prefix): 3 rows.
const isExact = (params[0] as string).length === 5;
return {
rows: (isExact
? [{ lat: null, lng: null, n: 0 }]
: [{ lat: 34.10, lng: -118.30, n: 3 }]) as unknown as T[],
};
};
const r = await resolveZipAnchor(q, '90210', { table: 'organizations' });
assert.deepEqual(r, { lat: 34.10, lng: -118.30 });
assert.equal(calls.length, 2);
assert.match(calls[1].text, /WHERE zip LIKE \$1/);
assert.equal(calls[1].params[0], '902%');
});
test('returns null when both exact AND prefix yield zero rows', async () => {
const q: QueryFn = async <T = unknown>(_t: string, _p: unknown[]) =>
({ rows: [{ lat: null, lng: null, n: 0 }] as unknown as T[] });
const r = await resolveZipAnchor(q, '99999', { table: 'organizations' });
assert.equal(r, null);
});
});
describe('resolveZipAnchor — bbox + whereExtra', () => {
test('passes bbox bounds as $2..$5 params', async () => {
let captured: unknown[] = [];
const q: QueryFn = async <T = unknown>(_t: string, params: unknown[]) => {
captured = params;
return { rows: [{ lat: 34, lng: -118, n: 1 }] as unknown as T[] };
};
await resolveZipAnchor(q, '90210', {
table: 'organizations',
bbox: { latMin: 30, latMax: 45, lngMin: -125, lngMax: -110 },
});
assert.deepEqual(captured, ['90210', 30, 45, -125, -110]);
});
test('defaults to BBOX_CA when bbox omitted', async () => {
let captured: unknown[] = [];
const q: QueryFn = async <T = unknown>(_t: string, params: unknown[]) => {
captured = params;
return { rows: [{ lat: 34, lng: -118, n: 1 }] as unknown as T[] };
};
await resolveZipAnchor(q, '90210', { table: 'organizations' });
assert.equal(captured[1], BBOX_CA.latMin);
assert.equal(captured[4], BBOX_CA.lngMax);
});
test('appends whereExtra clause to the query', async () => {
let capturedText = '';
const q: QueryFn = async <T = unknown>(text: string, _p: unknown[]) => {
capturedText = text;
return { rows: [{ lat: 34, lng: -118, n: 1 }] as unknown as T[] };
};
await resolveZipAnchor(q, '90210', {
table: 'organizations',
whereExtra: "type='law_firm'",
});
assert.match(capturedText, /AND \(type='law_firm'\)/);
});
test('honors custom table name', async () => {
let capturedText = '';
const q: QueryFn = async <T = unknown>(text: string, _p: unknown[]) => {
capturedText = text;
return { rows: [{ lat: 34, lng: -118, n: 1 }] as unknown as T[] };
};
await resolveZipAnchor(q, '90210', { table: 'animal_businesses' });
assert.match(capturedText, /FROM animal_businesses/);
});
test('3-digit ZIP skips exact-match step', async () => {
const calls: Array<{ params: unknown[] }> = [];
const q: QueryFn = async <T = unknown>(_t: string, params: unknown[]) => {
calls.push({ params });
return { rows: [{ lat: 34, lng: -118, n: 5 }] as unknown as T[] };
};
await resolveZipAnchor(q, '902', { table: 'organizations' });
assert.equal(calls.length, 1, '3-digit input goes straight to prefix path');
assert.equal(calls[0].params[0], '902%');
});
});
describe('haversineKmSql', () => {
test('produces a SELECT expression with all 4 bind params', () => {
const sql = haversineKmSql('o.lat', 'o.lng', '$1', '$2');
assert.match(sql, /6371/, 'uses km earth radius');
assert.match(sql, /ASIN/);
assert.match(sql, /SQRT/);
assert.match(sql, /RADIANS/);
assert.match(sql, /o\.lat/);
assert.match(sql, /o\.lng/);
assert.match(sql, /\$1/);
assert.match(sql, /\$2/);
assert.match(sql, /::float8/, 'casts to numeric');
});
test('honors custom column + param names', () => {
const sql = haversineKmSql('row.latitude', 'row.longitude', '$5', '$6');
assert.match(sql, /row\.latitude/);
assert.match(sql, /row\.longitude/);
assert.match(sql, /\$5/);
assert.match(sql, /\$6/);
assert.doesNotMatch(sql, /\$1/);
});
});
describe('withinBoundingBoxSql', () => {
test('produces a BETWEEN-AND-BETWEEN predicate', () => {
const sql = withinBoundingBoxSql('o.lat', 'o.lng', '$3', '$4', '$5', '$6');
assert.match(sql, /o\.lat BETWEEN \$3 AND \$4/);
assert.match(sql, /o\.lng BETWEEN \$5 AND \$6/);
});
});
describe('EmptyReason type', () => {
test('all four reasons are valid string literals', () => {
const reasons: EmptyReason[] = ['ok', 'no_zip', 'no_anchor', 'no_providers_in_radius'];
assert.equal(reasons.length, 4);
assert.deepEqual(new Set(reasons).size, 4);
});
});