import test from 'node:test' import assert from 'node:assert/strict' import { buildGeocodeUrl, normalizeAdcode, parseGeocodeResult } from './geo.js' /** * 回归测试:客户拜访页定位链路的两处确定性缺陷。 * * 1) 天地图 geocoder 的 postStr 未编码 —— 实测同一 key: * 原样 {'lon':..} -> HTTP 400 参数格式错误 * 仅编码引号 {%27lon%27:..} -> HTTP 400 * 花括号/冒号编码(encodeURI) -> HTTP 200 * 全编码(admin 端写法) -> HTTP 200 * 所以必须确保 postStr 经过完整 URL 编码。 * * 2) 行政区划按「中文名」查询在重名区县上必然失败(白云区=广州/贵阳、 * 朝阳区=北京/长春…)。天地图已直接返回 county_code,其去 156 前缀 * 即 sys_area.id(实测:越秀区 156440104→440104 与库中 id 一致), * 可直接精确命中,绕开重名。 */ // ==================== buildGeocodeUrl ==================== test('buildGeocodeUrl:postStr 必须被 URL 编码(不得残留裸花括号,否则接口返回 400)', () => { const url = buildGeocodeUrl(113.2644, 23.1291) const query = url.slice(url.indexOf('?') + 1) const encodedPostStr = query.split('&').find(p => p.startsWith('postStr=')) // 解码后应还原为天地图要求的原始形态 assert.equal( decodeURIComponent(encodedPostStr.slice('postStr='.length)), "{'lon':113.2644,'lat':23.1291,'ver':1}" ) // 关键:裸的 { } 必须被编码。未编码时接口实测返回 HTTP 400「参数格式错误」。 // 注意单引号 ' 按 RFC 3986 属 unreserved 之外的子分隔符,encodeURIComponent // 不会编码它;实测该形态(花括号/冒号已编码、引号保留)接口返回 HTTP 200, // 因此这里只断言花括号与冒号必须被编码。 assert.ok(!/[{}]/.test(encodedPostStr), `花括号未被编码: ${encodedPostStr}`) assert.ok(!encodedPostStr.includes(':'), `冒号未被编码: ${encodedPostStr}`) }) test('buildGeocodeUrl:经纬度与 tk 参数齐全', () => { const url = buildGeocodeUrl(121.4737, 31.2304) assert.match(url, /^https:\/\/api\.tianditu\.gov\.cn\/geocoder\?/) assert.ok(url.includes('type=geocode')) assert.ok(url.includes('tk=')) assert.ok(decodeURIComponent(url).includes("'lon':121.4737")) assert.ok(decodeURIComponent(url).includes("'lat':31.2304")) }) test('buildGeocodeUrl:负数/小数坐标不应破坏参数结构', () => { const url = buildGeocodeUrl(-73.9857, 40.7484) const query = url.slice(url.indexOf('?') + 1) // 恰好 3 个参数,说明坐标里的符号没有把它撑开 assert.equal(query.split('&').length, 3) assert.ok(decodeURIComponent(url).includes("'lon':-73.9857")) }) // ==================== normalizeAdcode ==================== test('normalizeAdcode:去掉天地图的 156(中国)前缀,得到 6 位 adcode', () => { assert.equal(normalizeAdcode('156440104'), '440104') // 广州越秀区 assert.equal(normalizeAdcode('156520113'), '520113') // 贵阳白云区 assert.equal(normalizeAdcode('156310101'), '310101') // 上海黄浦区 }) test('normalizeAdcode:已是 6 位裸码时原样返回', () => { assert.equal(normalizeAdcode('440104'), '440104') assert.equal(normalizeAdcode(440104), '440104') assert.equal(normalizeAdcode(' 440104 '), '440104') }) test('normalizeAdcode:非法值返回空串,绝不产出错误编码', () => { for (const bad of [null, undefined, '', ' ', 'abc', '156', '12345', {}, []]) { assert.equal(normalizeAdcode(bad), '', `应拒绝非法输入: ${JSON.stringify(bad)}`) } }) // ==================== parseGeocodeResult ==================== const successPayload = { status: '0', msg: 'ok', result: { formatted_address: '广东省广州市越秀区北京街道禺山社区北京路374号', addressComponent: { province: '广东省', city: '广州市', county: '越秀区', county_code: '156440104', city_code: '156440100', province_code: '156440000' } } } test('parseGeocodeResult:正常响应解析出省/市/区县/adcode/地址', () => { const geo = parseGeocodeResult(successPayload) assert.equal(geo.province, '广东省') assert.equal(geo.city, '广州市') assert.equal(geo.county, '越秀区') assert.equal(geo.adcode, '440104') assert.equal(geo.address, '广东省广州市越秀区北京街道禺山社区北京路374号') }) test('parseGeocodeResult:直辖市 city 为空时用省级名兜底(与 sys_area 一致)', () => { // 实测天地图对上海/北京/天津/重庆返回 city="" ,而 sys_area 中黄浦区的 city 是「上海市」 const shanghai = { status: '0', result: { formatted_address: '上海市黄浦区人民广场', addressComponent: { province: '上海市', city: '', county: '黄浦区', county_code: '156310101', city_code: '', province_code: '156310000' } } } const geo = parseGeocodeResult(shanghai) assert.equal(geo.city, '上海市', '直辖市应以省名兜底,避免 city_str 为空导致筛选失效') assert.equal(geo.county, '黄浦区') assert.equal(geo.adcode, '310101') }) test('parseGeocodeResult:天地图的各类错误响应一律返回 null(不得当成成功)', () => { const failures = [ { msg: '参数格式错误', status: 400 }, // postStr 未编码 { code: 301012, msg: '权限类型错误' }, // key 类型不符 { code: 302010, msg: '该tk已限流' }, // 限流 { status: '0' }, // 缺 result { result: {} }, // 缺 addressComponent {}, null, undefined ] for (const payload of failures) { assert.equal(parseGeocodeResult(payload), null, `错误响应必须返回 null: ${JSON.stringify(payload)}`) } }) test('parseGeocodeResult:adcode 缺失时不崩,且不产生假编码', () => { const geo = parseGeocodeResult({ status: '0', result: { formatted_address: '某地', addressComponent: { province: '某省', city: '某市', county: '某区' } } }) assert.equal(geo.adcode, '', '缺 county_code 时应为空串,由调用方降级为按名字查') assert.equal(geo.county, '某区') })