diff --git a/src/clients/dotnet/Bitip.Client.Tests/Bitip.Client.Tests.csproj b/src/clients/dotnet/Bitip.Client.Tests/Bitip.Client.Tests.csproj index 0e0aa05..c720486 100644 --- a/src/clients/dotnet/Bitip.Client.Tests/Bitip.Client.Tests.csproj +++ b/src/clients/dotnet/Bitip.Client.Tests/Bitip.Client.Tests.csproj @@ -3,6 +3,27 @@ net9.0 enable + false + true + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + diff --git a/src/clients/dotnet/Bitip.Client.Tests/Class1.cs b/src/clients/dotnet/Bitip.Client.Tests/Class1.cs deleted file mode 100644 index 2c0fa9a..0000000 --- a/src/clients/dotnet/Bitip.Client.Tests/Class1.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) 2025 Tudor Stanciu - -namespace Bitip.Client.Tests -{ - public class Class1 - { - - } -} diff --git a/src/clients/dotnet/Bitip.Client.Tests/GetDetailedIpLocationTests.cs b/src/clients/dotnet/Bitip.Client.Tests/GetDetailedIpLocationTests.cs new file mode 100644 index 0000000..9e0c312 --- /dev/null +++ b/src/clients/dotnet/Bitip.Client.Tests/GetDetailedIpLocationTests.cs @@ -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 +{ + /// + /// Unit tests for IBitipClient.GetDetailedIpLocation method (detailed lookup). + /// + 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 + { + { "en", "United States" }, + { "de", "Vereinigte Staaten" } + }, + IsInEuropeanUnion = false + }, + City = new CityInfo + { + Names = new Dictionary + { + { "en", "Mountain View" } + } + }, + Subdivisions = new List + { + new Subdivision + { + IsoCode = "CA", + Names = new Dictionary + { + { "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 + { + { "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 { { "en", "United States" } } + }, + City = new CityInfo + { + Names = new Dictionary { { "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(async () => + await client.GetDetailedIpLocation(ip!)); + } + + [Fact] + public async Task GetDetailedIpLocation_WithInvalidIpFormat_ThrowsArgumentException() + { + // Arrange + var client = BitipClientTestFixture.CreateMockedClient(mock => { }); + + // Act & Assert + await Assert.ThrowsAsync(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(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(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(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); + } + } +} diff --git a/src/clients/dotnet/Bitip.Client.Tests/GetHealthTests.cs b/src/clients/dotnet/Bitip.Client.Tests/GetHealthTests.cs new file mode 100644 index 0000000..fe7026c --- /dev/null +++ b/src/clients/dotnet/Bitip.Client.Tests/GetHealthTests.cs @@ -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 +{ + /// + /// Unit tests for IBitipClient.GetHealth method. + /// + 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(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(async () => + await client.GetHealth()); + } + } +} diff --git a/src/clients/dotnet/Bitip.Client.Tests/GetIpLocationTests.cs b/src/clients/dotnet/Bitip.Client.Tests/GetIpLocationTests.cs new file mode 100644 index 0000000..db1be16 --- /dev/null +++ b/src/clients/dotnet/Bitip.Client.Tests/GetIpLocationTests.cs @@ -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 +{ + /// + /// Unit tests for IBitipClient.GetIpLocation method (simple lookup). + /// + 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(async () => + await client.GetIpLocation(ip!)); + } + + [Fact] + public async Task GetIpLocation_WithInvalidIpFormat_ThrowsArgumentException() + { + // Arrange + var client = BitipClientTestFixture.CreateMockedClient(mock => { }); + + // Act & Assert + await Assert.ThrowsAsync(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(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(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(async () => + await client.GetIpLocation("8.8.8.8")); + + Assert.Contains("Too Many Requests", exception.Message); + } + } +} diff --git a/src/clients/dotnet/Bitip.Client.Tests/GetVersionTests.cs b/src/clients/dotnet/Bitip.Client.Tests/GetVersionTests.cs new file mode 100644 index 0000000..41839fd --- /dev/null +++ b/src/clients/dotnet/Bitip.Client.Tests/GetVersionTests.cs @@ -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 +{ + /// + /// Unit tests for IBitipClient.GetVersion method. + /// + 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(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(async () => + await client.GetVersion()); + + Assert.Contains("Unauthorized", exception.Message); + } + } +} diff --git a/src/clients/dotnet/Bitip.Client.Tests/Helpers/BitipClientTestFixture.cs b/src/clients/dotnet/Bitip.Client.Tests/Helpers/BitipClientTestFixture.cs new file mode 100644 index 0000000..071a31e --- /dev/null +++ b/src/clients/dotnet/Bitip.Client.Tests/Helpers/BitipClientTestFixture.cs @@ -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 +{ + /// + /// Helper class for creating mocked BitipClient instances in tests. + /// + public static class BitipClientTestFixture + { + /// + /// Creates a mocked BitipClient with a custom HttpMessageHandler setup. + /// + public static IBitipClient CreateMockedClient(Action> setupHandler) + { + var mockHandler = new Mock(); + 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 + })); + } + + /// + /// Creates a mocked client that returns a successful response with the provided data. + /// + public static IBitipClient CreateMockedClientWithResponse(T responseData, string urlContains = "") + { + return CreateMockedClient(mockHandler => + { + mockHandler + .Protected() + .Setup>( + "SendAsync", + string.IsNullOrEmpty(urlContains) + ? ItExpr.IsAny() + : ItExpr.Is(req => + req.Method == HttpMethod.Get && + req.RequestUri!.ToString().Contains(urlContains)), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonSerializer.Serialize(responseData)) + }); + }); + } + + /// + /// Creates a mocked client that throws TaskCanceledException. + /// + public static IBitipClient CreateCancelledClient() + { + return CreateMockedClient(mockHandler => + { + mockHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ThrowsAsync(new TaskCanceledException()); + }); + } + + /// + /// Creates a mocked client that returns an HTTP error with specified status code. + /// + public static IBitipClient CreateClientWithError(HttpStatusCode statusCode, string error, string message, string? ip = null) + { + return CreateMockedClient(mockHandler => + { + mockHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = statusCode, + Content = new StringContent(JsonSerializer.Serialize(new ErrorResponse + { + Error = error, + Message = message, + Ip = ip + })) + }); + }); + } + + /// + /// Creates a real service provider for integration tests (if configured). + /// Returns null if TestConfiguration.UseRealApi is false. + /// +#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 + } +} diff --git a/src/clients/dotnet/Bitip.Client.Tests/Integration/RealApiIntegrationTests.cs b/src/clients/dotnet/Bitip.Client.Tests/Integration/RealApiIntegrationTests.cs new file mode 100644 index 0000000..4f6cefd --- /dev/null +++ b/src/clients/dotnet/Bitip.Client.Tests/Integration/RealApiIntegrationTests.cs @@ -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 +{ + /// + /// Integration tests that make real API calls to a live Bitip instance. + /// These tests are DISABLED by default (TestConfiguration.UseRealApi = false). + /// + 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(); + + // 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(); + + // 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(); + + // 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(); + + // 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); + } + } +} diff --git a/src/clients/dotnet/Bitip.Client.Tests/TestConfiguration.cs b/src/clients/dotnet/Bitip.Client.Tests/TestConfiguration.cs new file mode 100644 index 0000000..fff9f63 --- /dev/null +++ b/src/clients/dotnet/Bitip.Client.Tests/TestConfiguration.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2025 Tudor Stanciu + +namespace Bitip.Client.Tests +{ + /// + /// Test configuration for Bitip.Client tests. + /// + public static class TestConfiguration + { + /// + /// Set this to true to run tests against a real Bitip API instance. + /// + public const bool UseRealApi = false; + + /// + /// Real Bitip API base URL for integration tests. + /// Update this with your actual Bitip instance URL. + /// Example: "https://lab.code-rove.com/bitip/api/" + /// + public const string RealApiBaseUrl = "https://lab.code-rove.com/bitip/api/"; + + /// + /// 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! + /// + public const string RealApiKey = "your-api-key-here"; + + /// + /// Mock base URL used for unit tests + /// + public const string MockApiBaseUrl = "https://mock-bitip-api.test/api/"; + + /// + /// Mock API key used for unit tests + /// + public const string MockApiKey = "mock-api-key-12345"; + + /// + /// Test IP addresses + /// + 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"; + } +} diff --git a/src/clients/dotnet/Bitip.Client/Bitip.Client.csproj b/src/clients/dotnet/Bitip.Client/Bitip.Client.csproj index ab79365..0dc0341 100644 --- a/src/clients/dotnet/Bitip.Client/Bitip.Client.csproj +++ b/src/clients/dotnet/Bitip.Client/Bitip.Client.csproj @@ -3,12 +3,35 @@ net9.0 enable + + + Bitip.Client + 1.0.0 + Tudor Stanciu + Code Rove + Bitip.Client + 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. + Copyright © 2025 Tudor Stanciu + + https://lab.code-rove.com/bitip/ + https://lab.code-rove.com/gitea/tudor.stanciu/bitip + git + + logo.png README.md - https://lab.code-rove.com/gitea/tudor.stanciu/bitip/src/branch/main/src/clients/dotnet - Bitip Toodle + LICENSE + + + bitip;geoip;geolocation;ip-lookup;ip-geolocation;asn;maxmind;dotnet $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt")) + false + + + false + true + snupkg @@ -25,6 +48,10 @@ True \ + + True + \ + diff --git a/src/clients/dotnet/Bitip.Client/LICENSE b/src/clients/dotnet/Bitip.Client/LICENSE new file mode 100644 index 0000000..688daa3 --- /dev/null +++ b/src/clients/dotnet/Bitip.Client/LICENSE @@ -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. diff --git a/src/clients/dotnet/Bitip.Client/Properties/AssemblyInfo.cs b/src/clients/dotnet/Bitip.Client/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1902204 --- /dev/null +++ b/src/clients/dotnet/Bitip.Client/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +// Copyright (c) 2025 Tudor Stanciu + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Bitip.Client.Tests")] diff --git a/src/clients/dotnet/Bitip.Client/README.md b/src/clients/dotnet/Bitip.Client/README.md index 2225f60..0604145 100644 --- a/src/clients/dotnet/Bitip.Client/README.md +++ b/src/clients/dotnet/Bitip.Client/README.md @@ -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 --- \ No newline at end of file +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 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** diff --git a/src/clients/dotnet/Bitip.Client/RELEASE.md b/src/clients/dotnet/Bitip.Client/RELEASE.md new file mode 100644 index 0000000..f8ff389 --- /dev/null +++ b/src/clients/dotnet/Bitip.Client/RELEASE.md @@ -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 + 1.0.0 + ``` + +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 + + + + + + + + + + +``` + +**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**