补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
214 行
7.5 KiB
TypeScript
214 行
7.5 KiB
TypeScript
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');
|
||
});
|
||
});
|
||
});
|