Add unit and integration tests for Bitip.Client

- Implement GetHealthTests to validate health check responses and error handling.
- Implement GetIpLocationTests for IP geolocation lookups, including valid and invalid cases.
- Implement GetVersionTests to check version retrieval and error handling.
- Create BitipClientTestFixture for mocking HTTP responses in tests.
- Add RealApiIntegrationTests for testing against a live Bitip API instance.
- Introduce TestConfiguration for managing test settings and API keys.
- Update Bitip.Client.csproj with package metadata and licensing information.
- Add LICENSE file for MIT License compliance.
- Create README.md with detailed usage instructions and examples.
- Establish RELEASE.md for publishing guidelines and version management.
- Add AssemblyInfo.cs for internal visibility to tests.
This commit is contained in:
Tudor Stanciu 2025-10-09 00:54:12 +03:00
parent c7f26a78a2
commit 5a623a4384
14 changed files with 1518 additions and 19 deletions

View File

@ -3,6 +3,27 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Bitip.Client\Bitip.Client.csproj" />
</ItemGroup>
</Project>

View File

@ -1,9 +0,0 @@
// Copyright (c) 2025 Tudor Stanciu
namespace Bitip.Client.Tests
{
public class Class1
{
}
}

View File

@ -0,0 +1,250 @@
// Copyright (c) 2025 Tudor Stanciu
using Bitip.Client.Models;
using Bitip.Client.Tests.Helpers;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Bitip.Client.Tests
{
/// <summary>
/// Unit tests for IBitipClient.GetDetailedIpLocation method (detailed lookup).
/// </summary>
public class GetDetailedIpLocationTests
{
[Fact]
public async Task GetDetailedIpLocation_WithValidIp_ReturnsDetailedLocation()
{
// Arrange
var expectedLocation = new DetailedIpLocation
{
Ip = "8.8.8.8",
Location = new GeoIpLocation
{
Country = new CountryInfo
{
IsoCode = "US",
Names = new Dictionary<string, string>
{
{ "en", "United States" },
{ "de", "Vereinigte Staaten" }
},
IsInEuropeanUnion = false
},
City = new CityInfo
{
Names = new Dictionary<string, string>
{
{ "en", "Mountain View" }
}
},
Subdivisions = new List<Subdivision>
{
new Subdivision
{
IsoCode = "CA",
Names = new Dictionary<string, string>
{
{ "en", "California" }
}
}
},
Location = new LocationInfo
{
Latitude = 37.4056,
Longitude = -122.0775,
TimeZone = "America/Los_Angeles"
},
Postal = new PostalInfo
{
Code = "94043"
},
Continent = new ContinentInfo
{
Code = "NA",
Names = new Dictionary<string, string>
{
{ "en", "North America" }
}
},
Traits = new TraitsInfo
{
IsAnonymousProxy = false,
IsSatelliteProvider = false
}
},
Asn = new AsnInfo
{
AutonomousSystemNumber = 15169,
AutonomousSystemOrganization = "Google LLC",
IpAddress = "8.8.8.8",
Network = "8.8.8.0/24"
}
};
var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedLocation, "lookup/detailed?ip=8.8.8.8");
// Act
var result = await client.GetDetailedIpLocation("8.8.8.8");
// Assert
Assert.NotNull(result);
Assert.Equal("8.8.8.8", result.Ip);
Assert.NotNull(result.Location);
Assert.NotNull(result.Location.Country);
Assert.Equal("US", result.Location.Country.IsoCode);
Assert.Equal("United States", result.Location.Country.Names?["en"]);
Assert.Equal("Mountain View", result.Location.City?.Names?["en"]);
Assert.Equal(37.4056, result.Location.Location?.Latitude);
Assert.Equal(-122.0775, result.Location.Location?.Longitude);
Assert.NotNull(result.Asn);
Assert.Equal((long)15169, result.Asn.AutonomousSystemNumber);
Assert.Equal("Google LLC", result.Asn.AutonomousSystemOrganization);
}
[Fact]
public async Task GetDetailedIpLocation_WithIpV6_ReturnsDetailedLocation()
{
// Arrange
var expectedLocation = new DetailedIpLocation
{
Ip = "2001:4860:4860::8888",
Location = new GeoIpLocation
{
Country = new CountryInfo
{
IsoCode = "US",
Names = new Dictionary<string, string> { { "en", "United States" } }
},
City = new CityInfo
{
Names = new Dictionary<string, string> { { "en", "Mountain View" } }
}
},
Asn = new AsnInfo
{
AutonomousSystemNumber = 15169,
AutonomousSystemOrganization = "Google LLC"
}
};
var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedLocation, "lookup/detailed");
// Act
var result = await client.GetDetailedIpLocation("2001:4860:4860::8888");
// Assert
Assert.NotNull(result);
Assert.Equal("2001:4860:4860::8888", result.Ip);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public async Task GetDetailedIpLocation_WithNullOrEmptyIp_ThrowsArgumentException(string? ip)
{
// Arrange
var client = BitipClientTestFixture.CreateMockedClient(mock => { });
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(async () =>
await client.GetDetailedIpLocation(ip!));
}
[Fact]
public async Task GetDetailedIpLocation_WithInvalidIpFormat_ThrowsArgumentException()
{
// Arrange
var client = BitipClientTestFixture.CreateMockedClient(mock => { });
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(async () =>
await client.GetDetailedIpLocation("not-an-ip"));
}
[Fact]
public async Task GetDetailedIpLocation_WithNotFoundIp_ThrowsHttpRequestException()
{
// Arrange
var client = BitipClientTestFixture.CreateClientWithError(
HttpStatusCode.NotFound,
"Not Found",
"IP address not found in database",
"1.2.3.4");
// Act & Assert
var exception = await Assert.ThrowsAsync<HttpRequestException>(async () =>
await client.GetDetailedIpLocation("1.2.3.4"));
Assert.Contains("Not Found", exception.Message);
}
[Fact]
public async Task GetDetailedIpLocation_WithCancellation_ThrowsTaskCanceledException()
{
// Arrange
var client = BitipClientTestFixture.CreateCancelledClient();
var cts = new CancellationTokenSource();
cts.Cancel();
// Act & Assert
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
await client.GetDetailedIpLocation("8.8.8.8", cts.Token));
}
[Fact]
public async Task GetDetailedIpLocation_WithServiceUnavailable_ThrowsHttpRequestException()
{
// Arrange
var client = BitipClientTestFixture.CreateClientWithError(
HttpStatusCode.ServiceUnavailable,
"Service Unavailable",
"Under maintenance, try again later");
// Act & Assert
var exception = await Assert.ThrowsAsync<HttpRequestException>(async () =>
await client.GetDetailedIpLocation("8.8.8.8"));
Assert.Contains("Service Unavailable", exception.Message);
}
[Fact]
public async Task GetDetailedIpLocation_VerifyAsnInformation_ContainsExpectedFields()
{
// Arrange
var expectedLocation = new DetailedIpLocation
{
Ip = "1.1.1.1",
Location = new GeoIpLocation
{
Country = new CountryInfo { IsoCode = "AU" }
},
Asn = new AsnInfo
{
AutonomousSystemNumber = 13335,
AutonomousSystemOrganization = "Cloudflare, Inc.",
IpAddress = "1.1.1.1",
Network = "1.1.1.0/24"
}
};
var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedLocation, "lookup/detailed");
// Act
var result = await client.GetDetailedIpLocation("1.1.1.1");
// Assert
Assert.NotNull(result.Asn);
Assert.Equal((long)13335, result.Asn.AutonomousSystemNumber);
Assert.Equal("Cloudflare, Inc.", result.Asn.AutonomousSystemOrganization);
Assert.Equal("1.1.1.1", result.Asn.IpAddress);
Assert.Equal("1.1.1.0/24", result.Asn.Network);
}
}
}

View File

@ -0,0 +1,70 @@
// Copyright (c) 2025 Tudor Stanciu
using Bitip.Client.Models;
using Bitip.Client.Tests.Helpers;
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Bitip.Client.Tests
{
/// <summary>
/// Unit tests for IBitipClient.GetHealth method.
/// </summary>
public class GetHealthTests
{
[Fact]
public async Task GetHealth_WithMockedResponse_ReturnsHealthInfo()
{
// Arrange
var expectedHealth = new HealthInfo
{
Status = "healthy",
Service = "Bitip GeoIP Service",
Timestamp = DateTime.UtcNow,
Error = null
};
var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedHealth, "health");
// Act
var result = await client.GetHealth();
// Assert
Assert.NotNull(result);
Assert.Equal("healthy", result.Status);
Assert.Equal("Bitip GeoIP Service", result.Service);
Assert.Null(result.Error);
}
[Fact]
public async Task GetHealth_WithCancellation_ThrowsTaskCanceledException()
{
// Arrange
var client = BitipClientTestFixture.CreateCancelledClient();
var cts = new CancellationTokenSource();
cts.Cancel();
// Act & Assert
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
await client.GetHealth(cts.Token));
}
[Fact]
public async Task GetHealth_WithServerError_ThrowsHttpRequestException()
{
// Arrange
var client = BitipClientTestFixture.CreateClientWithError(
HttpStatusCode.ServiceUnavailable,
"Service Unavailable",
"Under maintenance");
// Act & Assert
await Assert.ThrowsAsync<HttpRequestException>(async () =>
await client.GetHealth());
}
}
}

View File

@ -0,0 +1,157 @@
// Copyright (c) 2025 Tudor Stanciu
using Bitip.Client.Models;
using Bitip.Client.Tests.Helpers;
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Bitip.Client.Tests
{
/// <summary>
/// Unit tests for IBitipClient.GetIpLocation method (simple lookup).
/// </summary>
public class GetIpLocationTests
{
[Fact]
public async Task GetIpLocation_WithValidIpV4_ReturnsLocation()
{
// Arrange
var expectedLocation = new IpLocation
{
Ip = "8.8.8.8",
Country = "United States",
CountryCode = "US",
IsInEuropeanUnion = false,
Region = "California",
RegionCode = "CA",
City = "Mountain View",
Latitude = 37.4056,
Longitude = -122.0775,
Timezone = "America/Los_Angeles",
PostalCode = "94043",
ContinentCode = "NA",
ContinentName = "North America",
Organization = "Google LLC"
};
var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedLocation, "lookup?ip=8.8.8.8");
// Act
var result = await client.GetIpLocation("8.8.8.8");
// Assert
Assert.NotNull(result);
Assert.Equal("8.8.8.8", result.Ip);
Assert.Equal("United States", result.Country);
Assert.Equal("US", result.CountryCode);
Assert.False(result.IsInEuropeanUnion);
Assert.Equal("California", result.Region);
Assert.Equal("Mountain View", result.City);
Assert.Equal(37.4056, result.Latitude);
Assert.Equal(-122.0775, result.Longitude);
}
[Fact]
public async Task GetIpLocation_WithValidIpV6_ReturnsLocation()
{
// Arrange
var expectedLocation = new IpLocation
{
Ip = "2001:4860:4860::8888",
Country = "United States",
CountryCode = "US",
IsInEuropeanUnion = false,
Region = "California",
City = "Mountain View",
Latitude = 37.4056,
Longitude = -122.0775,
Timezone = "America/Los_Angeles"
};
var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedLocation, "lookup?ip=2001:4860:4860::8888");
// Act
var result = await client.GetIpLocation("2001:4860:4860::8888");
// Assert
Assert.NotNull(result);
Assert.Equal("2001:4860:4860::8888", result.Ip);
Assert.Equal("United States", result.Country);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public async Task GetIpLocation_WithNullOrEmptyIp_ThrowsArgumentException(string? ip)
{
// Arrange
var client = BitipClientTestFixture.CreateMockedClient(mock => { });
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(async () =>
await client.GetIpLocation(ip!));
}
[Fact]
public async Task GetIpLocation_WithInvalidIpFormat_ThrowsArgumentException()
{
// Arrange
var client = BitipClientTestFixture.CreateMockedClient(mock => { });
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(async () =>
await client.GetIpLocation("invalid-ip-address"));
}
[Fact]
public async Task GetIpLocation_WithNotFoundIp_ThrowsHttpRequestException()
{
// Arrange
var client = BitipClientTestFixture.CreateClientWithError(
HttpStatusCode.NotFound,
"Not Found",
"IP address not found in database",
"1.2.3.4");
// Act & Assert
var exception = await Assert.ThrowsAsync<HttpRequestException>(async () =>
await client.GetIpLocation("1.2.3.4"));
Assert.Contains("Not Found", exception.Message);
}
[Fact]
public async Task GetIpLocation_WithCancellation_ThrowsTaskCanceledException()
{
// Arrange
var client = BitipClientTestFixture.CreateCancelledClient();
var cts = new CancellationTokenSource();
cts.Cancel();
// Act & Assert
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
await client.GetIpLocation("8.8.8.8", cts.Token));
}
[Fact]
public async Task GetIpLocation_WithRateLimit_ThrowsHttpRequestException()
{
// Arrange
var client = BitipClientTestFixture.CreateClientWithError(
(HttpStatusCode)429,
"Too Many Requests",
"Rate limit exceeded");
// Act & Assert
var exception = await Assert.ThrowsAsync<HttpRequestException>(async () =>
await client.GetIpLocation("8.8.8.8"));
Assert.Contains("Too Many Requests", exception.Message);
}
}
}

View File

@ -0,0 +1,71 @@
// Copyright (c) 2025 Tudor Stanciu
using Bitip.Client.Models;
using Bitip.Client.Tests.Helpers;
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Bitip.Client.Tests
{
/// <summary>
/// Unit tests for IBitipClient.GetVersion method.
/// </summary>
public class GetVersionTests
{
[Fact]
public async Task GetVersion_WithMockedResponse_ReturnsVersionInfo()
{
// Arrange
var expectedVersion = new VersionInfo
{
Version = "1.0.0",
CommitHash = "abc123def456",
BuildDate = new DateTime(2025, 10, 8)
};
var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedVersion, "version");
// Act
var result = await client.GetVersion();
// Assert
Assert.NotNull(result);
Assert.Equal("1.0.0", result.Version);
Assert.Equal("abc123def456", result.CommitHash);
Assert.Equal(new DateTime(2025, 10, 8), result.BuildDate);
}
[Fact]
public async Task GetVersion_WithCancellation_ThrowsTaskCanceledException()
{
// Arrange
var client = BitipClientTestFixture.CreateCancelledClient();
var cts = new CancellationTokenSource();
cts.Cancel();
// Act & Assert
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
await client.GetVersion(cts.Token));
}
[Fact]
public async Task GetVersion_WithUnauthorized_ThrowsHttpRequestException()
{
// Arrange
var client = BitipClientTestFixture.CreateClientWithError(
HttpStatusCode.Unauthorized,
"Unauthorized",
"Invalid API key");
// Act & Assert
var exception = await Assert.ThrowsAsync<HttpRequestException>(async () =>
await client.GetVersion());
Assert.Contains("Unauthorized", exception.Message);
}
}
}

View File

@ -0,0 +1,130 @@
// Copyright (c) 2025 Tudor Stanciu
using Bitip.Client.Models;
using Bitip.Client.Services;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Moq.Protected;
using System;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Bitip.Client.Tests.Helpers
{
/// <summary>
/// Helper class for creating mocked BitipClient instances in tests.
/// </summary>
public static class BitipClientTestFixture
{
/// <summary>
/// Creates a mocked BitipClient with a custom HttpMessageHandler setup.
/// </summary>
public static IBitipClient CreateMockedClient(Action<Mock<HttpMessageHandler>> setupHandler)
{
var mockHandler = new Mock<HttpMessageHandler>();
setupHandler(mockHandler);
var httpClient = new HttpClient(mockHandler.Object)
{
BaseAddress = new Uri(TestConfiguration.MockApiBaseUrl)
};
return new BitipClient(
httpClient,
Microsoft.Extensions.Options.Options.Create(new BitipOptions
{
BaseUrl = TestConfiguration.MockApiBaseUrl,
ApiKey = TestConfiguration.MockApiKey
}));
}
/// <summary>
/// Creates a mocked client that returns a successful response with the provided data.
/// </summary>
public static IBitipClient CreateMockedClientWithResponse<T>(T responseData, string urlContains = "")
{
return CreateMockedClient(mockHandler =>
{
mockHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
string.IsNullOrEmpty(urlContains)
? ItExpr.IsAny<HttpRequestMessage>()
: ItExpr.Is<HttpRequestMessage>(req =>
req.Method == HttpMethod.Get &&
req.RequestUri!.ToString().Contains(urlContains)),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(JsonSerializer.Serialize(responseData))
});
});
}
/// <summary>
/// Creates a mocked client that throws TaskCanceledException.
/// </summary>
public static IBitipClient CreateCancelledClient()
{
return CreateMockedClient(mockHandler =>
{
mockHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ThrowsAsync(new TaskCanceledException());
});
}
/// <summary>
/// Creates a mocked client that returns an HTTP error with specified status code.
/// </summary>
public static IBitipClient CreateClientWithError(HttpStatusCode statusCode, string error, string message, string? ip = null)
{
return CreateMockedClient(mockHandler =>
{
mockHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = statusCode,
Content = new StringContent(JsonSerializer.Serialize(new ErrorResponse
{
Error = error,
Message = message,
Ip = ip
}))
});
});
}
/// <summary>
/// Creates a real service provider for integration tests (if configured).
/// Returns null if TestConfiguration.UseRealApi is false.
/// </summary>
#pragma warning disable CS0162 // Unreachable code detected (by design - UseRealApi is const)
public static IServiceProvider? CreateRealServiceProvider()
{
if (!TestConfiguration.UseRealApi)
{
return null;
}
var services = new ServiceCollection();
services.UseBitipClient(TestConfiguration.RealApiBaseUrl, TestConfiguration.RealApiKey);
return services.BuildServiceProvider();
}
#pragma warning restore CS0162
}
}

View File

@ -0,0 +1,106 @@
// Copyright (c) 2025 Tudor Stanciu
using Bitip.Client.Services;
using Bitip.Client.Tests.Helpers;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;
using Xunit;
namespace Bitip.Client.Tests.Integration
{
/// <summary>
/// Integration tests that make real API calls to a live Bitip instance.
/// These tests are DISABLED by default (TestConfiguration.UseRealApi = false).
/// </summary>
public class RealApiIntegrationTests
{
private readonly IServiceProvider? _realServiceProvider;
public RealApiIntegrationTests()
{
_realServiceProvider = BitipClientTestFixture.CreateRealServiceProvider();
}
[Fact]
public async Task GetHealth_ReturnsHealthInfo()
{
// Skip if not configured for real API
if (!TestConfiguration.UseRealApi || _realServiceProvider == null)
return;
// Arrange
var client = _realServiceProvider.GetRequiredService<IBitipClient>();
// Act
var result = await client.GetHealth();
// Assert
Assert.NotNull(result);
Assert.NotNull(result.Status);
Assert.NotNull(result.Service);
Assert.True(result.Timestamp > DateTime.MinValue);
}
[Fact]
public async Task GetVersion_ReturnsVersionInfo()
{
// Skip if not configured for real API
if (!TestConfiguration.UseRealApi || _realServiceProvider == null)
return;
// Arrange
var client = _realServiceProvider.GetRequiredService<IBitipClient>();
// Act
var result = await client.GetVersion();
// Assert
Assert.NotNull(result);
Assert.NotNull(result.Version);
Assert.NotNull(result.CommitHash);
Assert.True(result.BuildDate > DateTime.MinValue);
}
[Fact]
public async Task GetIpLocation_ReturnsLocation()
{
// Skip if not configured for real API
if (!TestConfiguration.UseRealApi || _realServiceProvider == null)
return;
// Arrange
var client = _realServiceProvider.GetRequiredService<IBitipClient>();
// Act
var result = await client.GetIpLocation(TestConfiguration.ValidIpV4);
// Assert
Assert.NotNull(result);
Assert.Equal(TestConfiguration.ValidIpV4, result.Ip);
Assert.NotNull(result.Country);
Assert.NotNull(result.CountryCode);
Assert.NotNull(result.City);
}
[Fact]
public async Task GetDetailedIpLocation_ReturnsDetailedLocation()
{
// Skip if not configured for real API
if (!TestConfiguration.UseRealApi || _realServiceProvider == null)
return;
// Arrange
var client = _realServiceProvider.GetRequiredService<IBitipClient>();
// Act
var result = await client.GetDetailedIpLocation(TestConfiguration.ValidIpV4);
// Assert
Assert.NotNull(result);
Assert.Equal(TestConfiguration.ValidIpV4, result.Ip);
Assert.NotNull(result.Location);
Assert.NotNull(result.Asn);
}
}
}

View File

@ -0,0 +1,47 @@
// Copyright (c) 2025 Tudor Stanciu
namespace Bitip.Client.Tests
{
/// <summary>
/// Test configuration for Bitip.Client tests.
/// </summary>
public static class TestConfiguration
{
/// <summary>
/// Set this to true to run tests against a real Bitip API instance.
/// </summary>
public const bool UseRealApi = false;
/// <summary>
/// Real Bitip API base URL for integration tests.
/// Update this with your actual Bitip instance URL.
/// Example: "https://lab.code-rove.com/bitip/api/"
/// </summary>
public const string RealApiBaseUrl = "https://lab.code-rove.com/bitip/api/";
/// <summary>
/// Real API key for integration tests.
/// Update this with your actual API key when running integration tests locally.
/// WARNING: Never commit your real API key to source control!
/// </summary>
public const string RealApiKey = "your-api-key-here";
/// <summary>
/// Mock base URL used for unit tests
/// </summary>
public const string MockApiBaseUrl = "https://mock-bitip-api.test/api/";
/// <summary>
/// Mock API key used for unit tests
/// </summary>
public const string MockApiKey = "mock-api-key-12345";
/// <summary>
/// Test IP addresses
/// </summary>
public const string ValidIpV4 = "8.8.8.8";
public const string ValidIpV6 = "2001:4860:4860::8888";
public const string InvalidIp = "invalid-ip";
public const string PrivateIp = "192.168.1.1";
}
}

View File

@ -3,12 +3,35 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<!-- Package Information -->
<PackageId>Bitip.Client</PackageId>
<Version>1.0.0</Version>
<Authors>Tudor Stanciu</Authors>
<Company>Code Rove</Company>
<Product>Bitip.Client</Product>
<Description>A .NET client library for integrating with the Bitip GeoIP Service. Provides IP geolocation lookup, health checks, and version information retrieval with full async/await support.</Description>
<Copyright>Copyright © 2025 Tudor Stanciu</Copyright>
<!-- Package URLs -->
<PackageProjectUrl>https://lab.code-rove.com/bitip/</PackageProjectUrl>
<RepositoryUrl>https://lab.code-rove.com/gitea/tudor.stanciu/bitip</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<!-- Package Assets -->
<PackageIcon>logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://lab.code-rove.com/gitea/tudor.stanciu/bitip/src/branch/main/src/clients/dotnet</RepositoryUrl>
<PackageTags>Bitip Toodle</PackageTags>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<!-- Package Metadata -->
<PackageTags>bitip;geoip;geolocation;ip-lookup;ip-geolocation;asn;maxmind;dotnet</PackageTags>
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))</PackageReleaseNotes>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<!-- Build Configuration -->
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
@ -25,6 +48,10 @@
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Update="LICENSE" Condition="Exists('LICENSE')">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,27 @@
MIT License
Copyright (c) 2025 Tudor Stanciu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
Note: This license applies to the Bitip.Client library only. Access to the
Bitip GeoIP Service API requires a separate API key and is subject to the
service's own terms and conditions.

View File

@ -0,0 +1,5 @@
// Copyright (c) 2025 Tudor Stanciu
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Bitip.Client.Tests")]

View File

@ -1,20 +1,275 @@
# Bitip.Client
A .NET client library for integrating with the [Bitip GeoIP Service](https://lab.code-rove.com/bitip/). This library provides a simple and efficient way to perform IP geolocation lookups, retrieve health status, and get version information from your Bitip API instance.
[Bitip](https://lab.code-rove.com/gitea/tudor.stanciu/bitip#readme) is a high-performance GeoIP lookup service designed to provide accurate geolocation data for IP addresses.
-------- TO DO --------
- You can see Bitip live here: https://lab.code-rove.com/bitip/
## Features
[Bitip.Client](https://lab.code-rove.com/gitea/tudor.stanciu/bitip/src/branch/main/src/clients/dotnet) is a simple .net client for Bitip. SDK is available as NuGet package on [NuGet](https://lab.code-rove.com/public-nuget-server/packages/bitip.client). Detail this section.
- ✅ Simple IP geolocation lookup
- ✅ Detailed IP geolocation with ASN information
- ✅ Health check endpoint
- ✅ Version information retrieval
- ✅ Built-in IP address validation
- ✅ Async/await support with CancellationToken
- ✅ Dependency injection ready
- ✅ Type-safe models with modern C# records
## Installation
You can install the Bitip.Client package via NuGet Package Manager Console:
```bash
Install the package via NuGet Package Manager:
```bash
dotnet add package Bitip.Client
```
Or via Package Manager Console:
```powershell
Install-Package Bitip.Client
```
Or via .NET CLI:
```bash
--- TO DO ---
The package is available on the [public NuGet server](https://lab.code-rove.com/public-nuget-server/packages/bitip.client).
## Quick Start
### 1. Register the Service
In your `Program.cs` or `Startup.cs`, register the Bitip client with dependency injection:
```csharp
using Bitip.Client;
// Register Bitip client
services.UseBitipClient(
baseUrl: "https://your-bitip-instance.com/api/",
apiKey: "your-api-key-here"
);
```
### 2. Inject and Use
Inject `IBitipClient` into your services or controllers:
```csharp
using Bitip.Client.Services;
using Bitip.Client.Models;
public class MyService
{
private readonly IBitipClient _bitipClient;
public MyService(IBitipClient bitipClient)
{
_bitipClient = bitipClient;
}
public async Task<IpLocation> GetLocationAsync(string ipAddress)
{
return await _bitipClient.GetIpLocation(ipAddress);
}
}
```
## Usage Examples
### Simple IP Lookup
Get basic geolocation information for an IP address:
```csharp
var location = await _bitipClient.GetIpLocation("8.8.8.8");
Console.WriteLine($"IP: {location.Ip}");
Console.WriteLine($"Country: {location.Country} ({location.CountryCode})");
Console.WriteLine($"City: {location.City}");
Console.WriteLine($"Region: {location.Region}");
Console.WriteLine($"Coordinates: {location.Latitude}, {location.Longitude}");
Console.WriteLine($"Timezone: {location.Timezone}");
Console.WriteLine($"EU Member: {location.IsInEuropeanUnion}");
```
**Response Model (`IpLocation`):**
- `Ip` - The queried IP address
- `Country` - Full country name
- `CountryCode` - ISO country code (e.g., "US")
- `IsInEuropeanUnion` - Boolean flag
- `Region` - Region/state name
- `RegionCode` - Region code (optional)
- `City` - City name
- `Latitude` / `Longitude` - Geographic coordinates (optional)
- `Timezone` - IANA timezone (optional)
- `PostalCode` - Postal/ZIP code (optional)
- `ContinentCode` / `ContinentName` - Continent information (optional)
- `Organization` - ISP/organization name (optional)
### Detailed IP Lookup
Get comprehensive geolocation data including ASN information:
```csharp
var detailedLocation = await _bitipClient.GetDetailedIpLocation("1.1.1.1");
Console.WriteLine($"IP: {detailedLocation.Ip}");
Console.WriteLine($"Country: {detailedLocation.Location.Country?.Names?["en"]}");
Console.WriteLine($"City: {detailedLocation.Location.City?.Names?["en"]}");
Console.WriteLine($"Latitude: {detailedLocation.Location.Location?.Latitude}");
Console.WriteLine($"Longitude: {detailedLocation.Location.Location?.Longitude}");
Console.WriteLine($"Timezone: {detailedLocation.Location.Location?.TimeZone}");
Console.WriteLine($"ASN: {detailedLocation.Asn.AutonomousSystemNumber}");
Console.WriteLine($"ISP: {detailedLocation.Asn.AutonomousSystemOrganization}");
```
**Response Model (`DetailedIpLocation`):**
- `Ip` - The queried IP address
- `Location` - Nested geolocation data
- `Country` - Country info with ISO code and localized names
- `City` - City with localized names
- `Subdivisions` - State/province information
- `Location` - Latitude, longitude, timezone
- `Postal` - Postal code
- `Continent` - Continent code and names
- `RegisteredCountry` - Registered country info
- `Traits` - Proxy and satellite provider flags
- `Asn` - Autonomous System Number information
- `AutonomousSystemNumber` - ASN number
- `AutonomousSystemOrganization` - ISP name
- `IpAddress` - IP address
- `Network` - Network range
### Health Check
Check the health status of the Bitip API:
```csharp
var health = await _bitipClient.GetHealth();
Console.WriteLine($"Status: {health.Status}");
Console.WriteLine($"Service: {health.Service}");
Console.WriteLine($"Timestamp: {health.Timestamp}");
if (!string.IsNullOrEmpty(health.Error))
{
Console.WriteLine($"Error: {health.Error}");
}
```
### Version Information
Retrieve the API version and build details:
```csharp
var version = await _bitipClient.GetVersion();
Console.WriteLine($"Version: {version.Version}");
Console.WriteLine($"Commit: {version.CommitHash}");
Console.WriteLine($"Build Date: {version.BuildDate}");
```
## Error Handling
The client throws standard exceptions that you should handle appropriately:
```csharp
try
{
var location = await _bitipClient.GetIpLocation("invalid-ip");
}
catch (ArgumentException ex)
{
// Invalid IP format
Console.WriteLine($"Invalid IP: {ex.Message}");
}
catch (HttpRequestException ex)
{
// API error (404 not found, 503 service unavailable, etc.)
Console.WriteLine($"API Error: {ex.Message}");
}
catch (TaskCanceledException ex)
{
// Request timeout
Console.WriteLine($"Timeout: {ex.Message}");
}
```
### Common API Error Responses
- **400 Bad Request** - Invalid IP address format or private IP
- **404 Not Found** - IP address not found in database
- **429 Too Many Requests** - Rate limit exceeded
- **503 Service Unavailable** - Service under maintenance
## Cancellation Support
All methods support `CancellationToken` for request cancellation:
```csharp
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
var location = await _bitipClient.GetIpLocation("8.8.8.8", cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Request cancelled or timed out");
}
```
## Configuration
### Base URL Format
The base URL should point to your Bitip API instance. The library automatically ensures proper URL formatting:
```csharp
// All of these work correctly:
services.UseBitipClient("https://bitip.example.com/api", "your-api-key");
services.UseBitipClient("https://bitip.example.com/api/", "your-api-key");
```
### API Key
The API key is sent via the `X-API-Key` header in all requests. Make sure your Bitip API is configured to accept your key.
## Requirements
- **.NET 9.0** or higher
- **Microsoft.Extensions.DependencyInjection.Abstractions** (9.0.9+)
- **Microsoft.Extensions.Http** (9.0.9+)
## API Endpoints Used
This client interacts with the following Bitip API endpoints:
| Method | Endpoint | Description |
| --------------------------- | ------------------------------ | --------------- |
| `GetHealth()` | `GET /health` | Health check |
| `GetVersion()` | `GET /version` | Version info |
| `GetIpLocation(ip)` | `GET /lookup?ip={ip}` | Simple lookup |
| `GetDetailedIpLocation(ip)` | `GET /lookup/detailed?ip={ip}` | Detailed lookup |
## Best Practices
1. **Reuse IBitipClient instances** - They are registered as scoped services by default
2. **Use cancellation tokens** for requests that may take time
3. **Handle exceptions appropriately** - Network calls can fail
4. **Validate IPs before calling** (optional, the library validates internally)
5. **Consider caching results** - IP geolocation data doesn't change frequently
## License
This project is licensed under the terms specified in the LICENSE file.
## Support
For issues, questions, or contributions, please visit:
- **Repository**: https://lab.code-rove.com/gitea/tudor.stanciu/bitip
- **Package**: https://lab.code-rove.com/public-nuget-server/packages/bitip.client
---
**Copyright © 2025 Tudor Stanciu**

View File

@ -0,0 +1,342 @@
# Bitip.Client Release Guide
This guide explains how to publish the Bitip.Client NuGet package to your private NuGet server.
## Prerequisites
Before publishing, ensure you have:
1. **.NET 9.0 SDK** installed
2. **NuGet CLI** or use `dotnet` CLI (recommended)
3. **API Key** for your NuGet server (https://lab.code-rove.com/public-nuget-server)
4. **Git** installed and repository up to date
## Version Management
### Semantic Versioning
Bitip.Client follows [Semantic Versioning 2.0.0](https://semver.org/):
```
MAJOR.MINOR.PATCH (e.g., 1.0.0)
```
- **MAJOR**: Breaking changes to the API
- **MINOR**: New features, backward compatible
- **PATCH**: Bug fixes, backward compatible
### When to Increment
| Change Type | Version Update | Example |
| -------------------- | ---------------- | ----------------- |
| Breaking API changes | MAJOR | `1.0.0``2.0.0` |
| New methods added | MINOR | `1.0.0``1.1.0` |
| Bug fixes | PATCH | `1.0.0``1.0.1` |
| Documentation only | PATCH (optional) | `1.0.0``1.0.1` |
### How to Update Version
1. **Edit `Bitip.Client.csproj`**:
```xml
<Version>1.0.0</Version>
```
2. **Update `ReleaseNotes.txt`** with changes:
```
Version 1.0.0 (2025-10-08)
- Initial release
- IP geolocation lookup (simple and detailed)
- Health check endpoint
- Version information retrieval
```
3. **Commit the changes**:
```powershell
git add Bitip.Client.csproj ReleaseNotes.txt
git commit -m "Bump version to 1.0.0"
git push
```
## Building the Package
### 1. Clean Previous Builds
```powershell
cd d:\Git\Home\Bitip\src\clients\dotnet\Bitip.Client
dotnet clean
```
### 2. Build in Release Mode
```powershell
dotnet build -c Release
```
### 3. Create NuGet Package
```powershell
dotnet pack -c Release -o .\nupkg
```
This creates two files in the `nupkg` folder:
- `Bitip.Client.{version}.nupkg` - The main package
- `Bitip.Client.{version}.snupkg` - Symbol package (for debugging)
### 4. Verify Package Contents
Before publishing, inspect the package:
```powershell
# List package contents
nuget.exe list -Source .\nupkg
# Or extract and inspect manually
Expand-Archive .\nupkg\Bitip.Client.1.0.0.nupkg -DestinationPath .\temp
```
Ensure the package contains:
- `lib/net9.0/Bitip.Client.dll`
- `logo.png`
- `README.md`
- `LICENSE` (if available)
- XML documentation file
## Publishing to NuGet Server
### Option 1: Using dotnet CLI (Recommended)
#### First-time Setup
Configure your NuGet source (one-time setup):
```powershell
dotnet nuget add source https://lab.code-rove.com/public-nuget-server/v3/index.json `
--name CodeRoveNuGet `
--username your-username `
--password your-api-key `
--store-password-in-clear-text
```
#### Publish Package
```powershell
cd d:\Git\Home\Bitip\src\clients\dotnet\Bitip.Client
# Push both main and symbol packages
dotnet nuget push .\nupkg\Bitip.Client.1.0.0.nupkg `
--source CodeRoveNuGet `
--api-key your-api-key
```
### Option 2: Using NuGet CLI
```powershell
nuget.exe push .\nupkg\Bitip.Client.1.0.0.nupkg `
-Source https://lab.code-rove.com/public-nuget-server `
-ApiKey your-api-key
```
### Verify Publication
After publishing, verify the package is available:
1. Visit: https://lab.code-rove.com/public-nuget-server/packages/bitip.client
2. Or search via CLI:
```powershell
dotnet nuget search Bitip.Client --source CodeRoveNuGet
```
## Complete Release Workflow
Here's the full workflow from start to finish:
```powershell
# 1. Navigate to project directory
cd d:\Git\Home\Bitip\src\clients\dotnet\Bitip.Client
# 2. Update version in .csproj (manually edit)
# 3. Update ReleaseNotes.txt (manually edit)
# 4. Commit version changes
git add Bitip.Client.csproj ReleaseNotes.txt
git commit -m "Release version 1.0.0"
# 5. Create git tag
git tag -a v1.0.0 -m "Release version 1.0.0"
# 6. Clean and build
dotnet clean
dotnet build -c Release
# 7. Run tests (if any)
cd ..\Bitip.Client.Tests
dotnet test -c Release
# 8. Create package
cd ..\Bitip.Client
dotnet pack -c Release -o .\nupkg
# 9. Publish to NuGet server
dotnet nuget push .\nupkg\Bitip.Client.1.0.0.nupkg `
--source CodeRoveNuGet `
--api-key your-api-key
# 10. Push git changes and tags
git push
git push --tags
```
## Troubleshooting
### Package Already Exists
If you get "Package already exists" error:
- **Solution 1**: Increment the version number (most common)
- **Solution 2**: Delete the old package from the server (if you have permissions)
- **Solution 3**: Unlisting the old version (if supported by your server)
### Authentication Failed
```
error: Response status code does not indicate success: 401 (Unauthorized)
```
**Solutions**:
- Verify your API key is correct
- Check if the API key has expired
- Ensure you have publish permissions on the NuGet server
### Missing Dependencies
If package references are not resolved:
- Ensure all `PackageReference` versions are correct
- Run `dotnet restore` before building
- Check that your NuGet sources are configured correctly
### Symbol Package Upload Failed
Symbol packages (`.snupkg`) may fail if the server doesn't support them:
- This is optional; the main package will still work
- You can skip symbols with: `dotnet pack --no-symbols`
## Best Practices
### Before Every Release
1. ✅ **Update ReleaseNotes.txt** with all changes
2. ✅ **Run all tests** to ensure nothing is broken
3. ✅ **Update README.md** if API changed
4. ✅ **Review breaking changes** carefully
5. ✅ **Create git tag** for the release
6. ✅ **Test package locally** before publishing
### Testing Locally
Before publishing, test the package in a separate project:
```powershell
# In a test project
dotnet add package Bitip.Client --source d:\Git\Home\Bitip\src\clients\dotnet\Bitip.Client\nupkg
```
### Version Checklist
- [ ] Version number updated in `.csproj`
- [ ] Release notes updated in `ReleaseNotes.txt`
- [ ] All tests passing
- [ ] Package built successfully
- [ ] Package contents verified
- [ ] Git changes committed
- [ ] Git tag created
- [ ] Package published to NuGet server
- [ ] Changes pushed to Git repository
## Managing API Keys
### Storing API Key Securely
**Option 1: Environment Variable**
```powershell
$env:NUGET_API_KEY = "your-api-key"
dotnet nuget push .\nupkg\Bitip.Client.1.0.0.nupkg `
--source CodeRoveNuGet `
--api-key $env:NUGET_API_KEY
```
**Option 2: NuGet.Config** (store credentials)
```xml
<!-- In NuGet.Config -->
<packageSources>
<add key="CodeRoveNuGet" value="https://lab.code-rove.com/public-nuget-server/v3/index.json" />
</packageSources>
<packageSourceCredentials>
<CodeRoveNuGet>
<add key="Username" value="your-username" />
<add key="ClearTextPassword" value="your-api-key" />
</CodeRoveNuGet>
</packageSourceCredentials>
```
**Option 3: Credential Provider** (most secure)
- Use a credential provider for automatic authentication
- Recommended for CI/CD pipelines
## Automated Release (Future Enhancement)
Consider setting up automated releases using GitHub Actions or similar:
```yaml
# .github/workflows/publish-nuget.yml
name: Publish NuGet Package
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 9.0.x
- name: Build
run: dotnet build -c Release
- name: Test
run: dotnet test -c Release
- name: Pack
run: dotnet pack -c Release -o ./nupkg
- name: Publish
run: dotnet nuget push ./nupkg/*.nupkg --source CodeRoveNuGet --api-key ${{ secrets.NUGET_API_KEY }}
```
## Quick Reference
| Command | Description |
| ----------------------------------- | ----------------------- |
| `dotnet clean` | Clean build artifacts |
| `dotnet build -c Release` | Build in release mode |
| `dotnet pack -c Release -o .\nupkg` | Create NuGet package |
| `dotnet nuget push` | Publish to NuGet server |
| `git tag -a v1.0.0 -m "Release"` | Create git tag |
| `git push --tags` | Push tags to remote |
---
**For questions or issues, contact the maintainer or visit the repository.**
**Copyright © 2025 Tudor Stanciu**