文件
OmniCard/backend/test/modules/crm/geoip.test.ts
wikiboss 62e0ae4435 feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 18:01:47 +08:00

214 行
7.5 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

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<typeof axios>;
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');
});
});
});