import axios from 'axios'; import { lookupGeoIp, resolveGeoIp, resolveLanguageFromCountry, } from '../../../src/comm/geoip/lookup'; import { CrmRadarService } from '../../../src/modules/crm/service/radar'; jest.mock('axios'); const mockedAxios = axios as jest.Mocked; describe('T34 / P29: GeoIP 可替换解析', () => { const originalBaseUrl = process.env.GEOIP_BASE_URL; afterEach(() => { if (originalBaseUrl === undefined) { delete process.env.GEOIP_BASE_URL; } else { process.env.GEOIP_BASE_URL = originalBaseUrl; } jest.clearAllMocks(); }); describe('lookupGeoIp(本地前缀库)', () => { it('应将 8.8.8.8 解析为 US / Mountain View', () => { expect(lookupGeoIp('8.8.8.8')).toEqual({ country: 'US', city: 'Mountain View', }); }); it('应将 114.114.114.114 解析为 CN / Nanjing', () => { expect(lookupGeoIp('114.114.114.114')).toEqual({ country: 'CN', city: 'Nanjing', }); }); it('回环与私网 IP 应返回 null,交由雷达归 Local', () => { expect(lookupGeoIp('127.0.0.1')).toBeNull(); expect(lookupGeoIp('::1')).toBeNull(); expect(lookupGeoIp('localhost')).toBeNull(); expect(lookupGeoIp('192.168.1.100')).toBeNull(); expect(lookupGeoIp('10.0.8.1')).toBeNull(); expect(lookupGeoIp('172.16.0.8')).toBeNull(); }); it('应覆盖其余内置公网短前缀', () => { expect(lookupGeoIp('8.8.4.4')).toEqual({ country: 'US', city: 'Mountain View', }); expect(lookupGeoIp('1.1.1.1')).toEqual({ country: 'AU', city: 'Sydney', }); expect(lookupGeoIp('9.9.9.9')).toEqual({ country: 'US', city: 'Berkeley', }); }); it('未知公网 IP 不得瞎填国家/城市(含迪拜)', () => { expect(lookupGeoIp('203.0.113.50')).toBeNull(); expect(lookupGeoIp('45.33.32.156')).toBeNull(); expect(lookupGeoIp('185.199.108.153')).toBeNull(); }); }); describe('resolveGeoIp(可替换)', () => { it('无 GEOIP_BASE_URL 时本地库命中返回国家城市与映射语言', async () => { delete process.env.GEOIP_BASE_URL; await expect(resolveGeoIp('8.8.8.8')).resolves.toEqual({ country: 'US', city: 'Mountain View', language: 'en', }); expect(mockedAxios.get).not.toHaveBeenCalled(); }); it('无 GEOIP_BASE_URL 且本地未命中时返回空地理 + reason,不假装迪拜,不猜语言', async () => { delete process.env.GEOIP_BASE_URL; const result = await resolveGeoIp('45.33.32.156'); expect(result.country).toBe(''); expect(result.city).toBe(''); expect(result.reason).toBe('not_found'); expect(['', 'unknown']).toContain(result.language); expect(result.language).not.toBe('ar'); expect(result.city.toLowerCase()).not.toContain('dubai'); expect(result.country).not.toBe('AE'); expect(mockedAxios.get).not.toHaveBeenCalled(); }); it('有 GEOIP_BASE_URL 时用 axios 查询,超时 3000ms', async () => { process.env.GEOIP_BASE_URL = 'https://geo.example.com/lookup'; mockedAxios.get.mockResolvedValueOnce({ data: { country: 'DE', city: 'Berlin' }, }); const result = await resolveGeoIp('1.2.3.4'); expect(result).toEqual({ country: 'DE', city: 'Berlin', language: 'de' }); expect(mockedAxios.get).toHaveBeenCalledWith( 'https://geo.example.com/lookup', expect.objectContaining({ params: { ip: '1.2.3.4' }, timeout: 3000, }) ); }); it('远程失败时返回空地理 + reason,不抛错、不填假城市、不猜语言', async () => { process.env.GEOIP_BASE_URL = 'https://geo.example.com/lookup'; mockedAxios.get.mockRejectedValueOnce(new Error('timeout of 3000ms exceeded')); const result = await resolveGeoIp('1.2.3.4'); expect(result.country).toBe(''); expect(result.city).toBe(''); expect(result.reason).toContain('timeout'); expect(['', 'unknown']).toContain(result.language); expect(result.language).not.toBe('ar'); expect(result.city.toLowerCase()).not.toContain('dubai'); }); }); describe('R46: resolveLanguageFromCountry(无国家不猜语言)', () => { it('无国家时语言为空或 unknown,不得默认 ar', () => { for (const country of ['', ' ', null, undefined]) { const language = resolveLanguageFromCountry(country as string | null | undefined); expect(['', 'unknown']).toContain(language); expect(language).not.toBe('ar'); expect(language).not.toBe('en'); expect(language).not.toBe('zh'); } }); it('有国家且映射表命中才返回对应语言', () => { expect(resolveLanguageFromCountry('US')).toBe('en'); expect(resolveLanguageFromCountry('CN')).toBe('zh'); expect(resolveLanguageFromCountry('AE')).toBe('ar'); expect(resolveLanguageFromCountry('DE')).toBe('de'); expect(resolveLanguageFromCountry('AU')).toBe('en'); }); it('有国家但不在映射表时语言为空或 unknown,不得瞎猜', () => { const language = resolveLanguageFromCountry('ZZ'); expect(['', 'unknown']).toContain(language); expect(language).not.toBe('ar'); }); it('本地未命中 / 私网解析结果的 language 不得为 ar', async () => { delete process.env.GEOIP_BASE_URL; const unknown = await resolveGeoIp('185.199.108.153'); expect(unknown.country).toBe(''); expect(['', 'unknown']).toContain(unknown.language); expect(unknown.language).not.toBe('ar'); const privateIp = await resolveGeoIp('192.168.1.100'); expect(privateIp.country).toBe(''); expect(['', 'unknown']).toContain(privateIp.language); expect(privateIp.language).not.toBe('ar'); }); it('远程仅有城市无国家时 language 为空或 unknown,不得默认 ar', async () => { process.env.GEOIP_BASE_URL = 'https://geo.example.com/lookup'; mockedAxios.get.mockResolvedValueOnce({ data: { city: 'Somewhere' }, }); const result = await resolveGeoIp('1.2.3.4'); expect(result.country).toBe(''); expect(result.city).toBe('Somewhere'); expect(['', 'unknown']).toContain(result.language); expect(result.language).not.toBe('ar'); expect(result.city.toLowerCase()).not.toContain('dubai'); }); }); describe('CrmRadarService.parseIpLocation', () => { let radarService: CrmRadarService; beforeEach(() => { radarService = new CrmRadarService(); }); it('公网 8.8.8.8 应命中 GeoIP 为 US,不再 Unknown', () => { const loc = radarService.parseIpLocation('8.8.8.8'); expect(loc.country).toBe('US'); expect(loc.city).toBe('Mountain View'); }); it('公网 114.114.114.114 应命中 GeoIP 为 CN', () => { const loc = radarService.parseIpLocation('114.114.114.114'); expect(loc.country).toBe('CN'); expect(loc.city).toBe('Nanjing'); }); it('127.0.0.1 在 lookup 为 null 时 parseIpLocation 仍归 Local', () => { expect(lookupGeoIp('127.0.0.1')).toBeNull(); expect(radarService.parseIpLocation('127.0.0.1')).toEqual({ country: 'Local', city: 'Local', }); }); it('cf-ipcountry=AE 应优先于公网 IP 的 GeoIP 结果', () => { const loc = radarService.parseIpLocation('8.8.8.8', { 'cf-ipcountry': 'AE', }); expect(loc.country).toBe('AE'); expect(loc.city).toBe('Unknown'); }); }); });