Skip to content
Merged
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
125 changes: 125 additions & 0 deletions IPinfo.Tests/IPApiPlusTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using Xunit;

using IPinfo.Models;

namespace IPinfo.Tests
{
public class IPApiPlusTest
{
[Fact]
public void TestGetDetailsIPV4()
{
string ip = "8.8.8.8";
IPinfoClientPlus client = new IPinfoClientPlus.Builder()
.AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN"))
.Build();

IPResponsePlus actual = client.IPApi.GetDetails(ip);

Assert.Equal("8.8.8.8", actual.IP);
Assert.Equal("dns.google", actual.Hostname);
Assert.False(actual.Bogon);

// Geo assertions
Assert.NotNull(actual.Geo);
Assert.NotNull(actual.Geo.City);
Assert.NotNull(actual.Geo.Region);
Assert.NotNull(actual.Geo.RegionCode);
Assert.Equal("US", actual.Geo.CountryCode);
Assert.Equal("United States", actual.Geo.Country);
Assert.Equal("United States", actual.Geo.CountryName);
Assert.False(actual.Geo.IsEU);
Assert.NotNull(actual.Geo.Continent);
Assert.NotNull(actual.Geo.ContinentCode);
Assert.NotEqual(0, actual.Geo.Latitude);
Assert.NotEqual(0, actual.Geo.Longitude);
Assert.NotNull(actual.Geo.Timezone);
Assert.NotNull(actual.Geo.PostalCode);
Assert.Equal("🇺🇸", actual.Geo.CountryFlag.Emoji);
Assert.Equal("U+1F1FA U+1F1F8", actual.Geo.CountryFlag.Unicode);
Assert.Equal("https://cdn.ipinfo.io/static/images/countries-flags/US.svg", actual.Geo.CountryFlagURL);
Assert.Equal("USD", actual.Geo.CountryCurrency.Code);
Assert.Equal("$", actual.Geo.CountryCurrency.Symbol);
Assert.Equal("NA", actual.Geo.ContinentInfo.Code);
Assert.Equal("North America", actual.Geo.ContinentInfo.Name);

// AS assertions
Assert.NotNull(actual.As);
Assert.Equal("AS15169", actual.As.Asn);
Assert.NotNull(actual.As.Name);
Assert.NotNull(actual.As.Domain);
Assert.NotNull(actual.As.Type);

// Network flags
Assert.False(actual.IsAnonymous);
Assert.True(actual.IsAnycast);
Assert.True(actual.IsHosting);
Assert.False(actual.IsMobile);
Assert.False(actual.IsSatellite);

// Plus-specific fields (may be present based on token tier)
// These fields exist in the response structure
}

[Fact]
public void TestGetDetailsIPV6()
{
string ip = "2001:4860:4860::8888";
IPinfoClientPlus client = new IPinfoClientPlus.Builder()
.AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN"))
.Build();

IPResponsePlus actual = client.IPApi.GetDetails(ip);

Assert.Equal("2001:4860:4860::8888", actual.IP);

// Geo assertions
Assert.NotNull(actual.Geo);
Assert.Equal("US", actual.Geo.CountryCode);
Assert.Equal("United States", actual.Geo.Country);
Assert.NotNull(actual.Geo.City);
Assert.NotNull(actual.Geo.Region);

// AS assertions
Assert.NotNull(actual.As);
Assert.NotNull(actual.As.Asn);
Assert.NotNull(actual.As.Name);
Assert.NotNull(actual.As.Domain);

// Network flags
Assert.False(actual.IsAnonymous);
Assert.False(actual.IsMobile);
Assert.False(actual.IsSatellite);
}

[Fact]
public void TestBogonIPV4()
{
string ip = "127.0.0.1";
IPinfoClientPlus client = new IPinfoClientPlus.Builder()
.AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN"))
.Build();

IPResponsePlus actual = client.IPApi.GetDetails(ip);

Assert.Equal("127.0.0.1", actual.IP);
Assert.True(actual.Bogon);
}

[Fact]
public void TestBogonIPV6()
{
string ip = "2001:0:c000:200::0:255:1";
IPinfoClientPlus client = new IPinfoClientPlus.Builder()
.AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN"))
.Build();

IPResponsePlus actual = client.IPApi.GetDetails(ip);

Assert.Equal("2001:0:c000:200::0:255:1", actual.IP);
Assert.True(actual.Bogon);
}
}
}
117 changes: 117 additions & 0 deletions src/IPinfo/Apis/IPApiPlus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System.Net;
using System.Threading.Tasks;
using System.Threading;

using IPinfo.Utilities;
using IPinfo.Http.Client;
using IPinfo.Http.Request;
using IPinfo.Models;
using IPinfo.Http.Response;
using IPinfo.Cache;

namespace IPinfo.Apis
{
/// <summary>
/// IPApiPlus.
/// </summary>
public sealed class IPApiPlus : BaseApi
{
/// <summary>
/// Initializes a new instance of the <see cref="IPApiPlus"/> class.
/// </summary>
/// <param name="httpClient"> httpClient. </param>
/// <param name="token"> token. </param>
/// <param name="cacheHandler"> cacheHandler. </param>
internal IPApiPlus(IHttpClient httpClient, string token, CacheHandler cacheHandler)
: base(httpClient, token, cacheHandler)
{
this.BaseUrl = "https://api.ipinfo.io/lookup/";
}

/// <summary>
/// Retrieves details of an IP address.
/// </summary>
/// <param name="ipAddress">The IP address of the user to retrieve details for.</param>
/// <returns>Returns the Models.IPResponsePlus response from the API call.</returns>
public Models.IPResponsePlus GetDetails(
IPAddress ipAddress)
{
string ipString = ipAddress?.ToString();
return this.GetDetails(ipString);
}

/// <summary>
/// Retrieves details of an IP address.
/// </summary>
/// <param name="ipAddress">The IP address of the user to retrieve details for.</param>
/// <returns>Returns the Models.IPResponsePlus response from the API call.</returns>
public Models.IPResponsePlus GetDetails(
string ipAddress = "")
{
Task<Models.IPResponsePlus> t = this.GetDetailsAsync(ipAddress);
ApiHelper.RunTaskSynchronously(t);
return t.Result;
}

/// <summary>
/// Retrieves details of an IP address.
/// </summary>
/// <param name="ipAddress">The IP address of the user to retrieve details for.</param>
/// <param name="cancellationToken">Cancellation token if the request is cancelled. </param>
/// <returns>Returns the Models.IPResponsePlus response from the API call.</returns>
public Task<Models.IPResponsePlus> GetDetailsAsync(
IPAddress ipAddress,
CancellationToken cancellationToken = default)
{
string ipString = ipAddress?.ToString();
return this.GetDetailsAsync(ipString, cancellationToken);
}

/// <summary>
/// Retrieves details of an IP address.
/// </summary>
/// <param name="ipAddress">The IP address of the user to retrieve details for.</param>
/// <param name="cancellationToken">Cancellation token if the request is cancelled. </param>
/// <returns>Returns the Models.IPResponsePlus response from the API call.</returns>
public async Task<Models.IPResponsePlus> GetDetailsAsync(
string ipAddress = "",
CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(ipAddress))
{
ipAddress = "";
}

// first check the data in the cache if cache is available
IPResponsePlus ipResponse = (IPResponsePlus)GetFromCache(ipAddress);
if (ipResponse != null)
{
return ipResponse;
}

if (BogonHelper.IsBogon(ipAddress))
{
ipResponse = new IPResponsePlus()
{
IP = ipAddress,
Bogon = true
};
return ipResponse;
}

// prepare the API call request to fetch the response.
HttpRequest httpRequest = this.CreateGetRequest(this.BaseUrl + ipAddress);
// invoke request and get response.
HttpStringResponse response = await this.GetClientInstance().ExecuteAsStringAsync(httpRequest, cancellationToken).ConfigureAwait(false);
HttpContext context = new HttpContext(httpRequest, response);

// handle errors defined at the API level.
this.ValidateResponse(context);

var responseModel = JsonHelper.ParseIPResponsePlus(response.Body);

SetInCache(ipAddress, responseModel);
return responseModel;
}
}
}
120 changes: 120 additions & 0 deletions src/IPinfo/IPinfoClientPlus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using System;

using IPinfo.Http.Client;
using IPinfo.Apis;
using IPinfo.Cache;
using IPinfo.Utilities;

namespace IPinfo
{
/// <summary>
/// The gateway for IPinfo Plus SDK. This class holds the configuration of the SDK.
/// </summary>
public sealed class IPinfoClientPlus
{
private readonly IHttpClient _httpClient;
private readonly CacheHandler _cacheHandler;
private readonly Lazy<IPApiPlus> _ipApi;

private IPinfoClientPlus(
string accessToken,
IHttpClient httpClient,
CacheHandler cacheHandler,
IHttpClientConfiguration httpClientConfiguration)
{
this._httpClient = httpClient;
this._cacheHandler = cacheHandler;
this.HttpClientConfiguration = httpClientConfiguration;

this._ipApi = new Lazy<IPApiPlus>(
() => new IPApiPlus(this._httpClient, accessToken, cacheHandler));
}

/// <summary>
/// Gets IPApiPlus.
/// </summary>
public IPApiPlus IPApi => this._ipApi.Value;

/// <summary>
/// Gets the configuration of the Http Client associated with this client.
/// </summary>
public IHttpClientConfiguration HttpClientConfiguration { get; }

/// <summary>
/// Gets the configuration of the Http Client associated with this client.
/// </summary>
public ICache Cache { get => _cacheHandler?.Cache; }

/// <summary>
/// Builder class.
/// </summary>
public class Builder
{
private string _accessToken = "";
private HttpClientConfiguration.Builder _httpClientConfig = new HttpClientConfiguration.Builder();
private IHttpClient _httpClient;
private CacheHandler _cacheHandler = new CacheHandler();

/// <summary>
/// Sets credentials for BearerAuth.
/// </summary>
/// <param name="accessToken">AccessToken.</param>
/// <returns>Builder.</returns>
public Builder AccessToken(string accessToken)
{
this._accessToken = accessToken;
return this;
}

/// <summary>
/// Sets HttpClientConfig.
/// </summary>
/// <param name="action"> Action. </param>
/// <returns>Builder.</returns>
public Builder HttpClientConfig(Action<HttpClientConfiguration.Builder> action)
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}

action(this._httpClientConfig);
return this;
}

/// <summary>
/// Sets the ICache implementation for the Builder.
/// </summary>
/// <param name="cache"> ICache implementation. Pass null to disable the cache.</param>
/// <returns>Builder.</returns>
public Builder Cache(ICache cache)
{
// Null is allowed here, which is being used to indicate that user do not want the cache.
if(cache == null)
{
this._cacheHandler = null;
}
else
{
this._cacheHandler = new CacheHandler(cache);
}
return this;
}

/// <summary>
/// Creates an object of the IPinfoClientPlus using the values provided for the builder.
/// </summary>
/// <returns>IPinfoClientPlus.</returns>
public IPinfoClientPlus Build()
{
this._httpClient = new HttpClientWrapper(this._httpClientConfig.Build());

return new IPinfoClientPlus(
_accessToken,
_httpClient,
_cacheHandler,
_httpClientConfig.Build());
}
}
}
}
Loading
Loading