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>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
</PropertyGroup>
|
</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>
|
</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>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<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>
|
<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>
|
<PackageIcon>logo.png</PackageIcon>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<RepositoryUrl>https://lab.code-rove.com/gitea/tudor.stanciu/bitip/src/branch/main/src/clients/dotnet</RepositoryUrl>
|
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||||
<PackageTags>Bitip Toodle</PackageTags>
|
|
||||||
|
<!-- Package Metadata -->
|
||||||
|
<PackageTags>bitip;geoip;geolocation;ip-lookup;ip-geolocation;asn;maxmind;dotnet</PackageTags>
|
||||||
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))</PackageReleaseNotes>
|
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))</PackageReleaseNotes>
|
||||||
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
|
|
||||||
|
<!-- Build Configuration -->
|
||||||
|
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||||
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -25,6 +48,10 @@
|
|||||||
<Pack>True</Pack>
|
<Pack>True</Pack>
|
||||||
<PackagePath>\</PackagePath>
|
<PackagePath>\</PackagePath>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="LICENSE" Condition="Exists('LICENSE')">
|
||||||
|
<Pack>True</Pack>
|
||||||
|
<PackagePath>\</PackagePath>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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
|
# 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.
|
[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 --------
|
## Features
|
||||||
- You can see Bitip live here: https://lab.code-rove.com/bitip/
|
|
||||||
|
|
||||||
[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
|
## Installation
|
||||||
|
|
||||||
You can install the Bitip.Client package via NuGet Package Manager Console:
|
Install the package via NuGet Package Manager:
|
||||||
```bash
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet add package Bitip.Client
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via Package Manager Console:
|
||||||
|
|
||||||
|
```powershell
|
||||||
Install-Package Bitip.Client
|
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