mirror of
https://dev.azure.com/tstanciu94/PhantomMind/_git/Bitip
synced 2025-10-13 01:52:19 +03:00
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:
parent
c7f26a78a2
commit
5a623a4384
@ -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>
|
||||
|
@ -1,9 +0,0 @@
|
||||
// Copyright (c) 2025 Tudor Stanciu
|
||||
|
||||
namespace Bitip.Client.Tests
|
||||
{
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
70
src/clients/dotnet/Bitip.Client.Tests/GetHealthTests.cs
Normal file
70
src/clients/dotnet/Bitip.Client.Tests/GetHealthTests.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
157
src/clients/dotnet/Bitip.Client.Tests/GetIpLocationTests.cs
Normal file
157
src/clients/dotnet/Bitip.Client.Tests/GetIpLocationTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
71
src/clients/dotnet/Bitip.Client.Tests/GetVersionTests.cs
Normal file
71
src/clients/dotnet/Bitip.Client.Tests/GetVersionTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
47
src/clients/dotnet/Bitip.Client.Tests/TestConfiguration.cs
Normal file
47
src/clients/dotnet/Bitip.Client.Tests/TestConfiguration.cs
Normal 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";
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
27
src/clients/dotnet/Bitip.Client/LICENSE
Normal file
27
src/clients/dotnet/Bitip.Client/LICENSE
Normal 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.
|
@ -0,0 +1,5 @@
|
||||
// Copyright (c) 2025 Tudor Stanciu
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Bitip.Client.Tests")]
|
@ -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**
|
||||
|
342
src/clients/dotnet/Bitip.Client/RELEASE.md
Normal file
342
src/clients/dotnet/Bitip.Client/RELEASE.md
Normal 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**
|
Loading…
x
Reference in New Issue
Block a user