Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions lib/geocoder/ip2geogeocoder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
var util = require('util'),
AbstractGeocoder = require('./abstractgeocoder');

/**
* Constructor
* @param <object> httpAdapter Http Adapter
* @param <object> options Options ({ apiKey: 'xxx' })
*/
var Ip2geoGeocoder = function Ip2geoGeocoder(httpAdapter, options) {
this.supportIPv4 = true;
this.supportIPv6 = true;
this.supportAddress = false;

Ip2geoGeocoder.super_.call(this, httpAdapter, options);

if (!options || !options.apiKey || options.apiKey === 'undefined') {
throw new Error(this.constructor.name + ' needs an apiKey');
}

// Inject X-Api-Key header into the http adapter for authentication
if (httpAdapter.options) {
httpAdapter.options.headers = Object.assign(
{},
httpAdapter.options.headers || {},
{ 'X-Api-Key': options.apiKey }
);
}
};

util.inherits(Ip2geoGeocoder, AbstractGeocoder);

// API endpoint
Ip2geoGeocoder.prototype._endpoint = 'https://api.ip2geo.dev/convert';

/**
* Geocode
* @param <string> value Value to geocode (IP address)
* @param <function> callback Callback method
*/
Ip2geoGeocoder.prototype._geocode = function(value, callback) {
var params = {
ip: value
};

this.httpAdapter.get(this._endpoint, params, function(err, result) {
if (err) {
return callback(err);
}

if (!result || !result.success || !result.data) {
return callback(new Error('ip2geo: Invalid response or request failed'));
}

var data = result.data;
var results = [];

var continent = data.continent || {};
var country = (continent && continent.country) || {};
var city = (country && country.city) || {};
var subdiv = (country && country.subdivision) || {};
var timezone = (city && city.timezone) || {};
var currency = (country && country.currency) || {};
var flag = (country && country.flag) || {};
var asn = data.asn || {};
var regCountry = data.registered_country || {};

results.push({
'ip' : data.ip,
'ipType' : data.type,
'isEu' : data.is_eu,
'latitude' : city.latitude,
'longitude' : city.longitude,
'accuracyRadius' : city.accuracy_radius,
'geonameId' : city.geoname_id,
'metroCode' : city.metro_code,
'continent' : continent.name,
'continentCode' : continent.code,
'continentGeonameId' : continent.geoname_id,
'country' : country.name,
'countryCode' : country.code,
'countryGeonameId' : country.geoname_id,
'phoneCode' : country.phone_code,
'capital' : country.capital,
'tld' : country.tld,
'flagEmoji' : flag.emoji,
'flagEmojiUnicode' : flag.emoji_unicode,
'flagImg' : flag.img,
'city' : city.name,
'state' : subdiv.name,
'stateCode' : subdiv.code,
'zipcode' : city.postal_code,
'timeZone' : timezone.name,
'timeNow' : timezone.time_now,
'currencyName' : currency.name,
'currencyCode' : currency.code,
'currencySymbol' : currency.symbol,
'asnNumber' : asn.number,
'asnName' : asn.name,
'registeredCountry' : regCountry.name,
'registeredCountryCode' : regCountry.code,
'registeredCountryGeonameId' : regCountry.geoname_id
});

results.raw = result;
callback(false, results);
});
};

module.exports = Ip2geoGeocoder;
6 changes: 6 additions & 0 deletions lib/geocoderfactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const TeleportGeocoder = require('./geocoder/teleportgeocoder.js');
const OpendataFranceGeocoder = require('./geocoder/opendatafrancegeocoder.js');
const MapBoxGeocoder = require('./geocoder/mapboxgeocoder.js');
const APlaceGeocoder = require('./geocoder/aplacegeocoder.js');
const Ip2geoGeocoder = require('./geocoder/ip2geogeocoder.js');

/**
* Geocoder Facotry
Expand Down Expand Up @@ -152,6 +153,11 @@ const GeocoderFactory = {
if (geocoderName === 'mapbox') {
return new MapBoxGeocoder(adapter, extra);
}
if (geocoderName === 'ip2geo') {
return new Ip2geoGeocoder(adapter, {
apiKey: extra.apiKey
});
}
if (geocoderName === 'aplace') {
return new APlaceGeocoder(adapter, extra);
}
Expand Down
113 changes: 113 additions & 0 deletions test/geocoder/ip2geogeocoder.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
(function() {
var chai = require('chai'),
should = chai.should(),
expect = chai.expect,
sinon = require('sinon');

var Ip2geoGeocoder = require('../../lib/geocoder/ip2geogeocoder.js');

var mockedHttpAdapter = {
get: function() {}
};

describe('Ip2geoGeocoder', () => {

describe('#constructor', () => {
test('an http adapter must be set', () => {
expect(function() {new Ip2geoGeocoder();}).to.throw(Error, 'Ip2geoGeocoder need an httpAdapter');
});

test('an apiKey must be set', () => {
expect(function() {new Ip2geoGeocoder(mockedHttpAdapter);}).to.throw(Error, 'Ip2geoGeocoder needs an apiKey');
});

test('Should be an instance of Ip2geoGeocoder', () => {
var geocoder = new Ip2geoGeocoder(mockedHttpAdapter, {apiKey: 'test'});
geocoder.should.be.instanceof(Ip2geoGeocoder);
});
});

describe('#geocode', () => {
test('Should not accept address', () => {
var geocoder = new Ip2geoGeocoder(mockedHttpAdapter, {apiKey: 'test'});
expect(function() {geocoder.geocode('1 rue test');})
.to.throw(Error, 'Ip2geoGeocoder does not support geocoding address');
});

test('Should call httpAdapter get method', () => {
var mock = sinon.mock(mockedHttpAdapter);
mock.expects('get').once().returns({then: function() {}});

var geocoder = new Ip2geoGeocoder(mockedHttpAdapter, {apiKey: 'test'});
geocoder.geocode('8.8.8.8');
mock.verify();
});

test('Should return a geocoded address', done => {
var mock = sinon.mock(mockedHttpAdapter);
mock.expects('get').once().callsArgWith(2, false, {
success: true,
data: {
ip: '8.8.8.8',
type: 'IPv4',
is_eu: false,
continent: {
name: 'North America',
code: 'NA',
geoname_id: 6255149,
country: {
name: 'United States',
code: 'US',
geoname_id: 6252001,
phone_code: '+1',
capital: 'Washington D.C.',
tld: '.us',
flag: { emoji: '', emoji_unicode: 'U+1F1FA U+1F1F8', img: 'https://flagcdn.com/us.svg' },
currency: { name: 'United States Dollar', code: 'USD', symbol: '$' },
subdivision: { name: 'California', code: 'CA' },
city: {
name: 'Mountain View',
latitude: 37.386,
longitude: -122.0838,
postal_code: '94035',
geoname_id: 5375480,
metro_code: 807,
accuracy_radius: 1000,
timezone: { name: 'America/Los_Angeles', time_now: '2026-04-05T10:30:00-07:00' }
}
}
},
asn: { number: 15169, name: 'Google LLC' },
registered_country: { name: 'United States', code: 'US', geoname_id: 6252001 }
}
});

var geocoder = new Ip2geoGeocoder(mockedHttpAdapter, {apiKey: 'test'});
geocoder.geocode('8.8.8.8', function(err, results) {
err.should.to.equal(false);
results[0].ip.should.to.equal('8.8.8.8');
results[0].country.should.to.equal('United States');
results[0].countryCode.should.to.equal('US');
results[0].city.should.to.equal('Mountain View');
results[0].state.should.to.equal('California');
results[0].latitude.should.to.equal(37.386);
results[0].longitude.should.to.equal(-122.0838);
results[0].zipcode.should.to.equal('94035');
results[0].timeZone.should.to.equal('America/Los_Angeles');
results[0].asnNumber.should.to.equal(15169);
results[0].asnName.should.to.equal('Google LLC');
mock.verify();
done();
});
});
});

describe('#reverse', () => {
test('Should throw an error', () => {
var geocoder = new Ip2geoGeocoder(mockedHttpAdapter, {apiKey: 'test'});
expect(function() {geocoder.reverse(10.0235, -2.3662);})
.to.throw(Error, 'Ip2geoGeocoder no support reverse geocoding');
});
});
});
})();