Keep Kakao radius filters local

Reject keyword radius without a coordinate center before Kakao Local calls so predictable client errors do not spend upstream quota.\n\nConstraint: PR #283 review round 3 requested local radius validation for issue #267.\nRejected: Letting Kakao Local reject radius-only keyword searches | wastes quota and weakens proxy determinism.\nConfidence: high\nScope-risk: narrow\nDirective: Keep coordinate-centered Kakao filters validated before cache lookup or upstream fetch.\nTested: node --test packages/k-skill-proxy/test/server.test.js; npm test --workspace k-skill-proxy; npm run lint --workspace k-skill-proxy; node --test scripts/skill-docs.test.js; bash scripts/validate-skills.sh; manual Fastify inject smoke.\nNot-tested: npm run ci remains blocked in local Python 3.14 pyexpat during pip install beautifulsoup4 after lint/typecheck.
This commit is contained in:
Jeffrey (Dongkyu) Kim 2026-05-23 21:34:38 +09:00
commit 2dbad40078
2 changed files with 19 additions and 0 deletions

View file

@ -105,6 +105,9 @@ function normalizeKakaoKeywordSearchQuery(query) {
const radius = query.radius;
if (radius !== undefined && radius !== null && radius !== "") {
if (!result.x || !result.y) {
throw new Error("Provide both x (lng) and y (lat) when using radius.");
}
result.radius = parseBoundedPositiveInteger(radius, {
defaultValue: undefined,
min: 0,

View file

@ -760,10 +760,18 @@ test("Kakao Map keyword search injects KakaoAK header, forwards x/y/radius/sort,
});
test("Kakao Map keyword search validates coordinate pairing and radius bounds", async (t) => {
const originalFetch = global.fetch;
const calls = [];
global.fetch = async (url, options = {}) => {
calls.push({ url: String(url), headers: options.headers ?? {} });
return jsonResponse({ documents: [], meta: { total_count: 0 } });
};
const app = buildServer({
env: { KAKAO_REST_API_KEY: "server-kakao-key" }
});
t.after(async () => {
global.fetch = originalFetch;
await app.close();
});
@ -785,12 +793,20 @@ test("Kakao Map keyword search validates coordinate pairing and radius bounds",
});
assert.equal(badRadius.statusCode, 400);
const radiusWithoutCoords = await app.inject({
method: "GET",
url: "/v1/kakao-map/search/keyword?q=hi&radius=500"
});
assert.equal(radiusWithoutCoords.statusCode, 400);
assert.match(radiusWithoutCoords.json().message, /radius/i);
const distanceSortWithoutCoords = await app.inject({
method: "GET",
url: "/v1/kakao-map/search/keyword?q=hi&sort=distance"
});
assert.equal(distanceSortWithoutCoords.statusCode, 400);
assert.match(distanceSortWithoutCoords.json().message, /sort=distance/i);
assert.equal(calls.length, 0, "validation failures should not call Kakao upstream");
});
test("Kakao Map category search rejects unsupported category group codes", async (t) => {