mirror of
https://dev.azure.com/tstanciu94/PhantomMind/_git/Bitip
synced 2025-10-13 01:52:19 +03:00
Compare commits
2 Commits
c9bda7769c
...
45c6898461
Author | SHA1 | Date | |
---|---|---|---|
|
45c6898461 | ||
|
ad348edf0b |
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"title": "Bitip - Professional GeoIP Lookup Service",
|
"title": "Bitip - Professional GeoIP Lookup Service",
|
||||||
"subtitle": "Modern GeoIP lookup service with REST API and interactive web interface",
|
"subtitle": "Modern GeoIP lookup service with REST API and interactive web interface",
|
||||||
"lastUpdated": "2025-10-09T12:00:00Z",
|
"lastUpdated": "2025-10-09T16:00:00Z",
|
||||||
"sections": [
|
"sections": [
|
||||||
{
|
{
|
||||||
"title": "Overview",
|
"title": "Overview",
|
||||||
@ -10,7 +10,7 @@
|
|||||||
{
|
{
|
||||||
"title": "Client Libraries",
|
"title": "Client Libraries",
|
||||||
"items": [
|
"items": [
|
||||||
"**.NET / C# - Bitip.Client** - Official NuGet package with strongly-typed models, async/await support, and dependency injection integration. Includes comprehensive IntelliSense documentation and 29 unit/integration tests for reliability.",
|
"**.NET / C# - Bitip.Client** - Official NuGet package with strongly-typed models, async/await support, and dependency injection integration. Includes comprehensive IntelliSense documentation and 40 unit/integration tests for reliability.",
|
||||||
"**Node.js / JavaScript** - Client library in development, coming soon",
|
"**Node.js / JavaScript** - Client library in development, coming soon",
|
||||||
"**REST API** - Direct HTTP access available for any programming language with full OpenAPI/Swagger documentation"
|
"**REST API** - Direct HTTP access available for any programming language with full OpenAPI/Swagger documentation"
|
||||||
]
|
]
|
||||||
@ -85,14 +85,25 @@
|
|||||||
"Returns detailed location hierarchy with ISO codes and network information"
|
"Returns detailed location hierarchy with ISO codes and network information"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"subtitle": "Detailed IP Lookup",
|
||||||
|
"items": [
|
||||||
|
"Endpoint: `GET /api/lookup/detailed?ip=:ip`",
|
||||||
|
"Returns structured geolocation data with separate location and ASN sections",
|
||||||
|
"Response format: `{ip, location: {...}, asn: {...}}`",
|
||||||
|
"Location section includes all geographic data (country, region, city, coordinates, timezone, postal code)",
|
||||||
|
"ASN section includes organization and autonomous system number",
|
||||||
|
"Useful for applications requiring clear separation of geographic and network information"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"subtitle": "Batch IP Lookup",
|
"subtitle": "Batch IP Lookup",
|
||||||
"items": [
|
"items": [
|
||||||
"Endpoint: `POST /api/lookup/batch`",
|
"Endpoint: `POST /api/lookup/batch`",
|
||||||
"Process up to 100 IP addresses in a single request",
|
"Process up to 100 IP addresses in a single request",
|
||||||
"Returns array of results matching input order",
|
"Returns `{succeeded: IpLocation[], failed: Array<{ip, error}>}` with separated success and failure arrays",
|
||||||
"Efficient bulk processing for large-scale operations",
|
"Efficient bulk processing for large-scale operations",
|
||||||
"Individual error handling for each IP address"
|
"Individual error handling for each IP address with clear error messages"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -242,7 +253,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "License",
|
"title": "License",
|
||||||
"content": "Bitip is **proprietary software** licensed under a custom license. The software is not open source and all rights are reserved by Tudor Stanciu. A 30-day evaluation period is provided for testing purposes. Commercial use, redistribution, and modifications require explicit written approval. See the LICENSE file for complete terms and conditions, or contact tudor.stanciu94@gmail.com for licensing inquiries."
|
"content": "Bitip is **source-available software** with a proprietary license. The source code is publicly available for inspection and analysis, but commercial use, redistribution, and modifications require explicit written approval from Tudor Stanciu. A 30-day evaluation period is provided for testing and evaluation purposes. All rights are reserved by Tudor Stanciu. See the LICENSE file for complete terms and conditions, or contact tudor.stanciu94@gmail.com for licensing inquiries."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,83 @@
|
|||||||
{
|
{
|
||||||
"releases": [
|
"releases": [
|
||||||
|
{
|
||||||
|
"version": "1.1.2",
|
||||||
|
"date": "2025-10-09T16:00:00Z",
|
||||||
|
"title": "Batch API Refactoring & .NET Client Completion",
|
||||||
|
"summary": "Refactored batch lookup API to use a cleaner succeeded/failed response structure instead of union types. Completed .NET client library (Bitip.Client v1.0.0) with full batch lookup support, comprehensive testing, and updated documentation.",
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"title": "Overview",
|
||||||
|
"content": "Version 1.1.2 introduces a breaking change to the batch lookup API for improved clarity and type safety. The response structure now separates successful and failed lookups into distinct arrays, making it easier to process results and handle errors. The .NET client library has been completed with full batch lookup support and is ready for v1.0.0 release."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "⚠️ Breaking Changes - Batch API",
|
||||||
|
"items": [
|
||||||
|
"**New Response Structure** - `/api/lookup/batch` now returns `{succeeded: IpLocation[], failed: Array<{ip, error}>}` instead of `{results: Array<IpLocation | {ip, error}>}`",
|
||||||
|
"**Separated Collections** - Successful lookups are in the `succeeded` array, failures in the `failed` array",
|
||||||
|
"**Type Safety** - Eliminates union types for cleaner, more predictable response handling",
|
||||||
|
"**Consistent Error Format** - All errors (validation, API failures) use the same `{ip, error}` structure",
|
||||||
|
"**Better Iteration** - Process successful and failed lookups independently without type checks"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Backend Improvements",
|
||||||
|
"items": [
|
||||||
|
"**TypeScript Type Refactoring** - Updated `BatchGeoIPResponse` interface with `succeeded` and `failed` properties",
|
||||||
|
"**Service Layer Updates** - Modified `lookupBatch()` in `geoip.ts` to return separated results",
|
||||||
|
"**Route Handler Changes** - Updated batch endpoint to combine API failures and validation errors in `failed` array",
|
||||||
|
"**Private IP Handling** - Private/reserved IPs rejected with clear error messages in `failed` array",
|
||||||
|
"**Promise.allSettled** - Used for parallel lookups with proper error handling"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": ".NET Client Completion (v1.0.0)",
|
||||||
|
"items": [
|
||||||
|
"**Batch Lookup Implementation** - Added `GetBatchIpLocation()` method to `IBitipClient` interface",
|
||||||
|
"**Clean Model Design** - `BatchIpLookupResponse` with `IEnumerable<IpLocation> Succeeded` and `IEnumerable<BatchIpLookupError> Failed`",
|
||||||
|
"**No Model Pollution** - Kept `IpLocation` strongly-typed without nullable properties for batch use",
|
||||||
|
"**BatchIpLookupError Record** - Dedicated record type with `string Ip` and `string Error` properties",
|
||||||
|
"**Client-Side Validation** - Added `IpValidator.IsValid()` helper to validate IPs before API call",
|
||||||
|
"**Error Aggregation** - Combines client-side validation errors with API errors in unified response"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Testing & Quality",
|
||||||
|
"items": [
|
||||||
|
"**40 Tests Passing** - Complete test suite with unit and integration tests",
|
||||||
|
"**GetBatchIpLocationTests** - 10 comprehensive tests covering all batch scenarios",
|
||||||
|
"**Mixed Success/Failure Handling** - Tests verify both succeeded and failed collections populate correctly",
|
||||||
|
"**Invalid IP Handling** - Tests confirm client-side validation catches malformed IPs",
|
||||||
|
"**Cancellation Token Support** - Tests verify proper async cancellation behavior",
|
||||||
|
"**Integration Tests** - Real API tests confirm end-to-end batch functionality"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Documentation Updates",
|
||||||
|
"items": [
|
||||||
|
"**Bitip.Client README** - Updated with new batch API structure and code examples",
|
||||||
|
"**Separate Loop Examples** - Shows iteration over `Succeeded` and `Failed` collections independently",
|
||||||
|
"**Clear Error Messages** - Documents that invalid IPs are validated client-side with descriptive errors",
|
||||||
|
"**Migration Guide** - Inline comments explain the API structure change for existing users",
|
||||||
|
"**Type Safety Benefits** - Documentation highlights improved type safety with separated arrays"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Developer Experience",
|
||||||
|
"items": [
|
||||||
|
"**Clearer API Contract** - No more union types or type guards needed when processing results",
|
||||||
|
"**Predictable Responses** - Always get both `succeeded` and `failed` arrays (may be empty)",
|
||||||
|
"**Better IntelliSense** - IDEs provide better autocomplete with distinct types",
|
||||||
|
"**Easier Error Handling** - Loop through `failed` array without type checking each item",
|
||||||
|
"**Maintainability** - Code is more readable and easier to test with explicit separation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Migration Notes",
|
||||||
|
"content": "If you are using the batch lookup API, update your code to access `response.succeeded` instead of filtering `response.results`. Failed lookups are now in `response.failed` array. For .NET developers using Bitip.Client, update to v1.0.0 and access `Succeeded` and `Failed` properties on the `BatchIpLookupResponse` object."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"date": "2025-10-09T12:00:00Z",
|
"date": "2025-10-09T12:00:00Z",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bitip",
|
"name": "bitip",
|
||||||
"version": "1.1.1",
|
"version": "1.1.2",
|
||||||
"description": "Bitip - GeoIP Lookup Service with REST API and Web Interface",
|
"description": "Bitip - GeoIP Lookup Service with REST API and Web Interface",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/backend/index.js",
|
"main": "dist/backend/index.js",
|
||||||
|
@ -220,22 +220,23 @@ router.post('/lookup/batch', async (req: Request, res: Response) => {
|
|||||||
const validIPs = ips.filter(ip => !geoIPService.isPrivateIP(ip));
|
const validIPs = ips.filter(ip => !geoIPService.isPrivateIP(ip));
|
||||||
const privateIPs = ips.filter(ip => geoIPService.isPrivateIP(ip));
|
const privateIPs = ips.filter(ip => geoIPService.isPrivateIP(ip));
|
||||||
|
|
||||||
const results = await geoIPService.lookupBatch(validIPs);
|
const batchResults = await geoIPService.lookupBatch(validIPs);
|
||||||
|
|
||||||
// Add errors for private IPs
|
// Add private IP errors to failed results
|
||||||
const privateIPErrors = privateIPs.map(ip => ({
|
const privateIPErrors = privateIPs.map(ip => ({
|
||||||
ip,
|
ip,
|
||||||
error: 'Private IP addresses are not supported',
|
error: 'Private IP addresses are not supported',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const response: BatchGeoIPResponse = {
|
const response: BatchGeoIPResponse = {
|
||||||
results: [...results, ...privateIPErrors],
|
succeeded: batchResults.succeeded,
|
||||||
|
failed: [...batchResults.failed, ...privateIPErrors],
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.info('Batch IP lookup completed', {
|
logger.info('Batch IP lookup completed', {
|
||||||
totalIPs: ips.length,
|
totalIPs: ips.length,
|
||||||
validIPs: validIPs.length,
|
succeeded: batchResults.succeeded.length,
|
||||||
privateIPs: privateIPs.length,
|
failed: batchResults.failed.length + privateIPErrors.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(response);
|
res.json(response);
|
||||||
@ -256,7 +257,7 @@ router.get('/version', (_req: Request, res: Response): void => {
|
|||||||
try {
|
try {
|
||||||
res.json({
|
res.json({
|
||||||
version: process.env.APP_VERSION || '1.0.0',
|
version: process.env.APP_VERSION || '1.0.0',
|
||||||
buildDate: process.env.CREATED_AT || 'unknown',
|
buildDate: process.env.CREATED_AT || new Date(0).toISOString(),
|
||||||
commitHash: process.env.GIT_REVISION || 'unknown',
|
commitHash: process.env.GIT_REVISION || 'unknown',
|
||||||
service: 'Bitip GeoIP Service',
|
service: 'Bitip GeoIP Service',
|
||||||
});
|
});
|
||||||
|
@ -6,6 +6,8 @@ import {
|
|||||||
GeoIPLocation,
|
GeoIPLocation,
|
||||||
SimplifiedGeoIPResponse,
|
SimplifiedGeoIPResponse,
|
||||||
DetailedGeoIPResponse,
|
DetailedGeoIPResponse,
|
||||||
|
BatchFailedLookup,
|
||||||
|
BatchGeoIPResponse,
|
||||||
} from '../types/index';
|
} from '../types/index';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
import logger from './logger';
|
import logger from './logger';
|
||||||
@ -128,23 +130,26 @@ class GeoIPService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async lookupBatch(
|
async lookupBatch(ips: string[]): Promise<BatchGeoIPResponse> {
|
||||||
ips: string[]
|
|
||||||
): Promise<Array<SimplifiedGeoIPResponse | { ip: string; error: string }>> {
|
|
||||||
const results = await Promise.allSettled(
|
const results = await Promise.allSettled(
|
||||||
ips.map(async ip => this.lookupSimple(ip))
|
ips.map(async ip => this.lookupSimple(ip))
|
||||||
);
|
);
|
||||||
|
|
||||||
return results.map((result, index) => {
|
const succeeded: SimplifiedGeoIPResponse[] = [];
|
||||||
|
const failed: Array<BatchFailedLookup> = [];
|
||||||
|
|
||||||
|
results.forEach((result, index) => {
|
||||||
if (result.status === 'fulfilled') {
|
if (result.status === 'fulfilled') {
|
||||||
return result.value;
|
succeeded.push(result.value);
|
||||||
} else {
|
} else {
|
||||||
return {
|
failed.push({
|
||||||
ip: ips[index] || 'unknown',
|
ip: ips[index] || 'unknown',
|
||||||
error: result.reason?.message || 'Lookup failed',
|
error: result.reason?.message || 'Lookup failed',
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return { succeeded, failed };
|
||||||
}
|
}
|
||||||
|
|
||||||
isValidIP(ip: string): boolean {
|
isValidIP(ip: string): boolean {
|
||||||
|
@ -72,8 +72,14 @@ export interface BatchGeoIPRequest {
|
|||||||
ips: string[];
|
ips: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BatchFailedLookup {
|
||||||
|
ip: string;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BatchGeoIPResponse {
|
export interface BatchGeoIPResponse {
|
||||||
results: Array<SimplifiedGeoIPResponse | { ip: string; error: string }>;
|
succeeded: SimplifiedGeoIPResponse[];
|
||||||
|
failed: Array<BatchFailedLookup>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorResponse {
|
export interface ErrorResponse {
|
||||||
|
316
src/clients/dotnet/Bitip.Client.Tests/GetBatchIpLocationTests.cs
Normal file
316
src/clients/dotnet/Bitip.Client.Tests/GetBatchIpLocationTests.cs
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
// Copyright (c) 2025 Tudor Stanciu
|
||||||
|
|
||||||
|
using Bitip.Client.Models;
|
||||||
|
using Bitip.Client.Tests.Helpers;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
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.GetBatchIpLocation method (batch lookup).
|
||||||
|
/// </summary>
|
||||||
|
public class GetBatchIpLocationTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBatchIpLocation_WithValidIps_ReturnsSucceeded()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ips = new List<string> { "8.8.8.8", "1.1.1.1" };
|
||||||
|
var expectedResponse = new BatchIpLookupResponse
|
||||||
|
{
|
||||||
|
Succeeded = new List<IpLocation>
|
||||||
|
{
|
||||||
|
new IpLocation
|
||||||
|
{
|
||||||
|
Ip = "8.8.8.8",
|
||||||
|
Country = "United States",
|
||||||
|
CountryCode = "US",
|
||||||
|
IsInEuropeanUnion = false,
|
||||||
|
Region = "California",
|
||||||
|
City = "Mountain View",
|
||||||
|
Latitude = 37.4056,
|
||||||
|
Longitude = -122.0775,
|
||||||
|
Timezone = "America/Los_Angeles"
|
||||||
|
},
|
||||||
|
new IpLocation
|
||||||
|
{
|
||||||
|
Ip = "1.1.1.1",
|
||||||
|
Country = "Australia",
|
||||||
|
CountryCode = "AU",
|
||||||
|
IsInEuropeanUnion = false,
|
||||||
|
Region = "Queensland",
|
||||||
|
City = "Brisbane",
|
||||||
|
Latitude = -27.4679,
|
||||||
|
Longitude = 153.0281,
|
||||||
|
Timezone = "Australia/Brisbane"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Failed = new List<BatchIpLookupError>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedResponse, "lookup/batch", HttpMethod.Post);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await client.GetBatchIpLocation(ips);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
var succeededList = result.Succeeded.ToList();
|
||||||
|
var failedList = result.Failed.ToList();
|
||||||
|
|
||||||
|
Assert.Equal(2, succeededList.Count);
|
||||||
|
Assert.Empty(failedList);
|
||||||
|
|
||||||
|
Assert.Equal("8.8.8.8", succeededList[0].Ip);
|
||||||
|
Assert.Equal("United States", succeededList[0].Country);
|
||||||
|
Assert.Equal("1.1.1.1", succeededList[1].Ip);
|
||||||
|
Assert.Equal("Australia", succeededList[1].Country);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBatchIpLocation_WithMixedSuccessAndErrors_ReturnsBoth()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ips = new List<string> { "8.8.8.8", "192.168.1.1" };
|
||||||
|
var expectedResponse = new BatchIpLookupResponse
|
||||||
|
{
|
||||||
|
Succeeded = new List<IpLocation>
|
||||||
|
{
|
||||||
|
new IpLocation
|
||||||
|
{
|
||||||
|
Ip = "8.8.8.8",
|
||||||
|
Country = "United States",
|
||||||
|
CountryCode = "US",
|
||||||
|
IsInEuropeanUnion = false,
|
||||||
|
Region = "California",
|
||||||
|
City = "Mountain View"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Failed = new List<BatchIpLookupError>
|
||||||
|
{
|
||||||
|
new BatchIpLookupError
|
||||||
|
{
|
||||||
|
Ip = "192.168.1.1",
|
||||||
|
Error = "Private IP addresses are not supported"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedResponse, "lookup/batch", HttpMethod.Post);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await client.GetBatchIpLocation(ips);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
var succeededList = result.Succeeded.ToList();
|
||||||
|
var failedList = result.Failed.ToList();
|
||||||
|
|
||||||
|
Assert.Single(succeededList);
|
||||||
|
Assert.Single(failedList);
|
||||||
|
|
||||||
|
Assert.Equal("United States", succeededList[0].Country);
|
||||||
|
Assert.Contains("Private IP", failedList[0].Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBatchIpLocation_WithNullList_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = BitipClientTestFixture.CreateMockedClient(mock => { });
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await Assert.ThrowsAsync<ArgumentNullException>(async () =>
|
||||||
|
await client.GetBatchIpLocation(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBatchIpLocation_WithEmptyList_ThrowsArgumentException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = BitipClientTestFixture.CreateMockedClient(mock => { });
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await Assert.ThrowsAsync<ArgumentException>(async () =>
|
||||||
|
await client.GetBatchIpLocation(new List<string>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBatchIpLocation_WithInvalidIp_ReturnsInFailed()
|
||||||
|
{
|
||||||
|
// Arrange - Mix of valid and invalid IPs
|
||||||
|
var ips = new List<string> { "8.8.8.8", "invalid-ip", "1.1.1.1" };
|
||||||
|
|
||||||
|
// Mock response for the valid IPs only (invalid ones handled client-side)
|
||||||
|
var expectedResponse = new BatchIpLookupResponse
|
||||||
|
{
|
||||||
|
Succeeded = new List<IpLocation>
|
||||||
|
{
|
||||||
|
new IpLocation
|
||||||
|
{
|
||||||
|
Ip = "8.8.8.8",
|
||||||
|
Country = "United States",
|
||||||
|
CountryCode = "US",
|
||||||
|
IsInEuropeanUnion = false,
|
||||||
|
Region = "California",
|
||||||
|
City = "Mountain View"
|
||||||
|
},
|
||||||
|
new IpLocation
|
||||||
|
{
|
||||||
|
Ip = "1.1.1.1",
|
||||||
|
Country = "Australia",
|
||||||
|
CountryCode = "AU",
|
||||||
|
IsInEuropeanUnion = false,
|
||||||
|
Region = "Queensland",
|
||||||
|
City = "Brisbane"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Failed = new List<BatchIpLookupError>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedResponse, "lookup/batch", HttpMethod.Post);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await client.GetBatchIpLocation(ips);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
var succeededList = result.Succeeded.ToList();
|
||||||
|
var failedList = result.Failed.ToList();
|
||||||
|
|
||||||
|
Assert.Equal(2, succeededList.Count); // Valid IPs
|
||||||
|
Assert.Single(failedList); // Invalid IP
|
||||||
|
|
||||||
|
// Check valid IPs succeeded
|
||||||
|
Assert.Equal("8.8.8.8", succeededList[0].Ip);
|
||||||
|
Assert.Equal("1.1.1.1", succeededList[1].Ip);
|
||||||
|
|
||||||
|
// Check invalid IP has error
|
||||||
|
Assert.Equal("invalid-ip", failedList[0].Ip);
|
||||||
|
Assert.Contains("Invalid IP", failedList[0].Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBatchIpLocation_WithCancellation_ThrowsTaskCanceledException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ips = new List<string> { "8.8.8.8", "1.1.1.1" };
|
||||||
|
var client = BitipClientTestFixture.CreateCancelledClient();
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
cts.Cancel();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
|
||||||
|
await client.GetBatchIpLocation(ips, cts.Token));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBatchIpLocation_WithServerError_ThrowsHttpRequestException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ips = new List<string> { "8.8.8.8" };
|
||||||
|
var client = BitipClientTestFixture.CreateClientWithError(
|
||||||
|
HttpStatusCode.InternalServerError,
|
||||||
|
"Internal Server Error",
|
||||||
|
"Failed to process batch lookup");
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await Assert.ThrowsAsync<HttpRequestException>(async () =>
|
||||||
|
await client.GetBatchIpLocation(ips));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBatchIpLocation_WithSingleIp_ReturnsResult()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ips = new List<string> { "8.8.8.8" };
|
||||||
|
var expectedResponse = new BatchIpLookupResponse
|
||||||
|
{
|
||||||
|
Succeeded = new List<IpLocation>
|
||||||
|
{
|
||||||
|
new IpLocation
|
||||||
|
{
|
||||||
|
Ip = "8.8.8.8",
|
||||||
|
Country = "United States",
|
||||||
|
CountryCode = "US",
|
||||||
|
IsInEuropeanUnion = false,
|
||||||
|
Region = "California",
|
||||||
|
City = "Mountain View"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Failed = new List<BatchIpLookupError>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedResponse, "lookup/batch", HttpMethod.Post);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await client.GetBatchIpLocation(ips);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
var succeededList = result.Succeeded.ToList();
|
||||||
|
Assert.Single(succeededList);
|
||||||
|
Assert.Empty(result.Failed);
|
||||||
|
Assert.Equal("8.8.8.8", succeededList[0].Ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBatchIpLocation_WithMultipleIps_ReturnsAllResults()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ips = new List<string> { "8.8.8.8", "1.1.1.1", "9.9.9.9" };
|
||||||
|
var expectedResponse = new BatchIpLookupResponse
|
||||||
|
{
|
||||||
|
Succeeded = new List<IpLocation>
|
||||||
|
{
|
||||||
|
new IpLocation { Ip = "8.8.8.8", Country = "United States", CountryCode = "US", IsInEuropeanUnion = false, Region = "California", City = "Mountain View" },
|
||||||
|
new IpLocation { Ip = "1.1.1.1", Country = "Australia", CountryCode = "AU", IsInEuropeanUnion = false, Region = "Queensland", City = "Brisbane" },
|
||||||
|
new IpLocation { Ip = "9.9.9.9", Country = "United States", CountryCode = "US", IsInEuropeanUnion = false, Region = "California", City = "Oakland" }
|
||||||
|
},
|
||||||
|
Failed = new List<BatchIpLookupError>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedResponse, "lookup/batch", HttpMethod.Post);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await client.GetBatchIpLocation(ips);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
var succeededList = result.Succeeded.ToList();
|
||||||
|
Assert.Equal(3, succeededList.Count);
|
||||||
|
Assert.Empty(result.Failed);
|
||||||
|
Assert.All(succeededList, r => Assert.NotNull(r.Ip));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBatchIpLocation_WithOnlyInvalidIps_ReturnsAllInFailed()
|
||||||
|
{
|
||||||
|
// Arrange - Only invalid IPs (no API call should be made)
|
||||||
|
var ips = new List<string> { "invalid-ip", "not-an-ip", "xyz" };
|
||||||
|
var client = BitipClientTestFixture.CreateMockedClient(mock => { });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await client.GetBatchIpLocation(ips);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Empty(result.Succeeded);
|
||||||
|
|
||||||
|
var failedList = result.Failed.ToList();
|
||||||
|
Assert.Equal(3, failedList.Count);
|
||||||
|
Assert.All(failedList, r =>
|
||||||
|
{
|
||||||
|
Assert.NotNull(r.Error);
|
||||||
|
Assert.Contains("Invalid IP", r.Error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -44,7 +44,7 @@ namespace Bitip.Client.Tests.Helpers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a mocked client that returns a successful response with the provided data.
|
/// Creates a mocked client that returns a successful response with the provided data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IBitipClient CreateMockedClientWithResponse<T>(T responseData, string urlContains = "")
|
public static IBitipClient CreateMockedClientWithResponse<T>(T responseData, string urlContains = "", HttpMethod? method = null)
|
||||||
{
|
{
|
||||||
return CreateMockedClient(mockHandler =>
|
return CreateMockedClient(mockHandler =>
|
||||||
{
|
{
|
||||||
@ -52,11 +52,11 @@ namespace Bitip.Client.Tests.Helpers
|
|||||||
.Protected()
|
.Protected()
|
||||||
.Setup<Task<HttpResponseMessage>>(
|
.Setup<Task<HttpResponseMessage>>(
|
||||||
"SendAsync",
|
"SendAsync",
|
||||||
string.IsNullOrEmpty(urlContains)
|
string.IsNullOrEmpty(urlContains) && method == null
|
||||||
? ItExpr.IsAny<HttpRequestMessage>()
|
? ItExpr.IsAny<HttpRequestMessage>()
|
||||||
: ItExpr.Is<HttpRequestMessage>(req =>
|
: ItExpr.Is<HttpRequestMessage>(req =>
|
||||||
req.Method == HttpMethod.Get &&
|
(method == null || req.Method == method) &&
|
||||||
req.RequestUri!.ToString().Contains(urlContains)),
|
(string.IsNullOrEmpty(urlContains) || req.RequestUri!.ToString().Contains(urlContains))),
|
||||||
ItExpr.IsAny<CancellationToken>())
|
ItExpr.IsAny<CancellationToken>())
|
||||||
.ReturnsAsync(new HttpResponseMessage
|
.ReturnsAsync(new HttpResponseMessage
|
||||||
{
|
{
|
||||||
|
@ -4,6 +4,8 @@ using Bitip.Client.Services;
|
|||||||
using Bitip.Client.Tests.Helpers;
|
using Bitip.Client.Tests.Helpers;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@ -102,5 +104,29 @@ namespace Bitip.Client.Tests.Integration
|
|||||||
Assert.NotNull(result.Location);
|
Assert.NotNull(result.Location);
|
||||||
Assert.NotNull(result.Asn);
|
Assert.NotNull(result.Asn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBatchIpLocation_ReturnsBatchResults()
|
||||||
|
{
|
||||||
|
// Skip if not configured for real API
|
||||||
|
if (!TestConfiguration.UseRealApi || _realServiceProvider == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
var client = _realServiceProvider.GetRequiredService<IBitipClient>();
|
||||||
|
var ips = new List<string> { TestConfiguration.ValidIpV4, TestConfiguration.ValidIpV6, TestConfiguration.InvalidIp };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await client.GetBatchIpLocation(ips);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(2, result.Succeeded.Count());
|
||||||
|
Assert.Single(result.Failed);
|
||||||
|
Assert.All(result.Succeeded, r => Assert.NotNull(r.Ip));
|
||||||
|
Assert.All(result.Succeeded, r => Assert.NotNull(r.Country));
|
||||||
|
Assert.All(result.Failed, r => Assert.NotNull(r.Ip));
|
||||||
|
Assert.All(result.Failed, r => Assert.NotNull(r.Error));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,17 +14,15 @@ namespace Bitip.Client.Tests
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Real Bitip API base URL for integration tests.
|
/// 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>
|
/// </summary>
|
||||||
public const string RealApiBaseUrl = "https://lab.code-rove.com/bitip/api/";
|
public const string RealApiBaseUrl = "http://localhost:5172/api/";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Real API key for integration tests.
|
/// Real API key for integration tests.
|
||||||
/// Update this with your actual API key when running integration tests locally.
|
/// Update this with your actual API key when running integration tests locally.
|
||||||
/// WARNING: Never commit your real API key to source control!
|
/// WARNING: Never commit your real API key to source control!
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string RealApiKey = "your-api-key-here";
|
public const string RealApiKey = "external-dev-key-1";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mock base URL used for unit tests
|
/// Mock base URL used for unit tests
|
||||||
|
@ -13,7 +13,8 @@ namespace Bitip.Client.Constants
|
|||||||
Health = "health",
|
Health = "health",
|
||||||
Version = "version",
|
Version = "version",
|
||||||
Lookup = "lookup?ip={ip}",
|
Lookup = "lookup?ip={ip}",
|
||||||
DetailedLookup = "lookup/detailed?ip={ip}";
|
DetailedLookup = "lookup/detailed?ip={ip}",
|
||||||
|
BatchLookup = "lookup/batch";
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ValueKeys
|
public struct ValueKeys
|
||||||
|
@ -15,5 +15,12 @@ namespace Bitip.Client.Helpers
|
|||||||
throw new ArgumentException("The provided IP address is not valid.", nameof(ip));
|
throw new ArgumentException("The provided IP address is not valid.", nameof(ip));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsValid(string ip)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(ip))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return System.Net.IPAddress.TryParse(ip, out _);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
32
src/clients/dotnet/Bitip.Client/Models/BatchIpLocation.cs
Normal file
32
src/clients/dotnet/Bitip.Client/Models/BatchIpLocation.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (c) 2025 Tudor Stanciu
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Bitip.Client.Models
|
||||||
|
{
|
||||||
|
public record BatchIpLookupRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("ips")]
|
||||||
|
public required IEnumerable<string> Ips { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record BatchIpLookupResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("succeeded")]
|
||||||
|
public required IEnumerable<IpLocation> Succeeded { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("failed")]
|
||||||
|
public required IEnumerable<BatchIpLookupError> Failed { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record BatchIpLookupError
|
||||||
|
{
|
||||||
|
[JsonPropertyName("ip")]
|
||||||
|
public required string Ip { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("error")]
|
||||||
|
public required string Error { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ A .NET client library for integrating with the [Bitip GeoIP Service](https://lab
|
|||||||
|
|
||||||
- ✅ Simple IP geolocation lookup
|
- ✅ Simple IP geolocation lookup
|
||||||
- ✅ Detailed IP geolocation with ASN information
|
- ✅ Detailed IP geolocation with ASN information
|
||||||
|
- ✅ Batch IP geolocation for multiple addresses
|
||||||
- ✅ Health check endpoint
|
- ✅ Health check endpoint
|
||||||
- ✅ Version information retrieval
|
- ✅ Version information retrieval
|
||||||
- ✅ Built-in IP address validation
|
- ✅ Built-in IP address validation
|
||||||
@ -139,6 +140,39 @@ Console.WriteLine($"ISP: {detailedLocation.Asn.AutonomousSystemOrganization}");
|
|||||||
- `IpAddress` - IP address
|
- `IpAddress` - IP address
|
||||||
- `Network` - Network range
|
- `Network` - Network range
|
||||||
|
|
||||||
|
### Batch IP Lookup
|
||||||
|
|
||||||
|
Query multiple IP addresses in a single request. Results are separated into succeeded and failed:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var ips = new List<string> { "8.8.8.8", "invalid-ip", "1.1.1.1" };
|
||||||
|
var batchResult = await _bitipClient.GetBatchIpLocation(ips);
|
||||||
|
|
||||||
|
// Process successful lookups
|
||||||
|
foreach (var location in batchResult.Succeeded)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{location.Ip}: {location.Country} ({location.CountryCode}), {location.City}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle failures
|
||||||
|
foreach (var error in batchResult.Failed)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error for {error.Ip}: {error.Error}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Model (`BatchIpLookupResponse`):**
|
||||||
|
|
||||||
|
- `Succeeded` - Collection of `IpLocation` objects for successful lookups
|
||||||
|
- Same structure as single IP lookup response
|
||||||
|
- All location data available
|
||||||
|
|
||||||
|
- `Failed` - Collection of `BatchIpLookupError` objects for failed lookups
|
||||||
|
- `Ip` - The IP address that failed
|
||||||
|
- `Error` - Error message (e.g., "Invalid IP address format", "Private IP addresses are not supported")
|
||||||
|
|
||||||
|
**Note:** Invalid IPs are validated client-side before sending to the API. The API may return additional errors for private IPs or IPs not found in the database.
|
||||||
|
|
||||||
### Health Check
|
### Health Check
|
||||||
|
|
||||||
Check the health status of the Bitip API:
|
Check the health status of the Bitip API:
|
||||||
@ -179,7 +213,7 @@ try
|
|||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
catch (ArgumentException ex)
|
||||||
{
|
{
|
||||||
// Invalid IP format
|
// Invalid IP format (single lookup methods only)
|
||||||
Console.WriteLine($"Invalid IP: {ex.Message}");
|
Console.WriteLine($"Invalid IP: {ex.Message}");
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
@ -194,9 +228,11 @@ catch (TaskCanceledException ex)
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note:** `GetBatchIpLocation` does not throw exceptions for invalid IPs. Instead, it returns error results for each invalid IP in the response.
|
||||||
|
|
||||||
### Common API Error Responses
|
### Common API Error Responses
|
||||||
|
|
||||||
- **400 Bad Request** - Invalid IP address format or private IP
|
- **400 Bad Request** - Invalid IP address format or private IP (single lookups)
|
||||||
- **404 Not Found** - IP address not found in database
|
- **404 Not Found** - IP address not found in database
|
||||||
- **429 Too Many Requests** - Rate limit exceeded
|
- **429 Too Many Requests** - Rate limit exceeded
|
||||||
- **503 Service Unavailable** - Service under maintenance
|
- **503 Service Unavailable** - Service under maintenance
|
||||||
@ -250,6 +286,7 @@ This client interacts with the following Bitip API endpoints:
|
|||||||
| `GetVersion()` | `GET /version` | Version info |
|
| `GetVersion()` | `GET /version` | Version info |
|
||||||
| `GetIpLocation(ip)` | `GET /lookup?ip={ip}` | Simple lookup |
|
| `GetIpLocation(ip)` | `GET /lookup?ip={ip}` | Simple lookup |
|
||||||
| `GetDetailedIpLocation(ip)` | `GET /lookup/detailed?ip={ip}` | Detailed lookup |
|
| `GetDetailedIpLocation(ip)` | `GET /lookup/detailed?ip={ip}` | Detailed lookup |
|
||||||
|
| `GetBatchIpLocation(ips)` | `POST /lookup/batch` | Batch lookup |
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
|
@ -5,8 +5,12 @@ using Bitip.Client.Extensions;
|
|||||||
using Bitip.Client.Helpers;
|
using Bitip.Client.Helpers;
|
||||||
using Bitip.Client.Models;
|
using Bitip.Client.Models;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.Http.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -52,5 +56,60 @@ namespace Bitip.Client.Services
|
|||||||
var task = await _httpClient.GetAndReadJsonAsync<DetailedIpLocation>(route, cancellationToken);
|
var task = await _httpClient.GetAndReadJsonAsync<DetailedIpLocation>(route, cancellationToken);
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<BatchIpLookupResponse> GetBatchIpLocation(IEnumerable<string> ips, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (ips == null)
|
||||||
|
throw new ArgumentNullException(nameof(ips));
|
||||||
|
|
||||||
|
if (!ips.Any())
|
||||||
|
{
|
||||||
|
throw new ArgumentException("IP list cannot be empty", nameof(ips));
|
||||||
|
}
|
||||||
|
|
||||||
|
var validIps = new List<string>();
|
||||||
|
var clientErrors = new List<BatchIpLookupError>();
|
||||||
|
|
||||||
|
foreach (var ip in ips)
|
||||||
|
{
|
||||||
|
if (IpValidator.IsValid(ip))
|
||||||
|
validIps.Add(ip);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
clientErrors.Add(new BatchIpLookupError
|
||||||
|
{
|
||||||
|
Ip = ip ?? string.Empty,
|
||||||
|
Error = "Invalid IP address format"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validIps.Count <= 0)
|
||||||
|
{
|
||||||
|
// All IPs were invalid - return only errors
|
||||||
|
return new BatchIpLookupResponse
|
||||||
|
{
|
||||||
|
Succeeded = Enumerable.Empty<IpLocation>(),
|
||||||
|
Failed = clientErrors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have valid IPs, send them to the API
|
||||||
|
var request = new BatchIpLookupRequest { Ips = validIps };
|
||||||
|
var response = await _httpClient.PostAsJsonAsync(ApiRoutes.BatchLookup, request, cancellationToken);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var apiResponse = await response.Content.ReadFromJsonAsync<BatchIpLookupResponse>(cancellationToken);
|
||||||
|
|
||||||
|
if (apiResponse == null)
|
||||||
|
throw new HttpRequestException("Failed to deserialize batch lookup response");
|
||||||
|
|
||||||
|
// Combine API results with client-side validation errors
|
||||||
|
return new BatchIpLookupResponse
|
||||||
|
{
|
||||||
|
Succeeded = apiResponse.Succeeded,
|
||||||
|
Failed = apiResponse.Failed.Concat(clientErrors)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) 2025 Tudor Stanciu
|
// Copyright (c) 2025 Tudor Stanciu
|
||||||
|
|
||||||
using Bitip.Client.Models;
|
using Bitip.Client.Models;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -12,5 +13,6 @@ namespace Bitip.Client.Services
|
|||||||
Task<VersionInfo> GetVersion(CancellationToken cancellationToken = default);
|
Task<VersionInfo> GetVersion(CancellationToken cancellationToken = default);
|
||||||
Task<IpLocation> GetIpLocation(string ip, CancellationToken cancellationToken = default);
|
Task<IpLocation> GetIpLocation(string ip, CancellationToken cancellationToken = default);
|
||||||
Task<DetailedIpLocation> GetDetailedIpLocation(string ip, CancellationToken cancellationToken = default);
|
Task<DetailedIpLocation> GetDetailedIpLocation(string ip, CancellationToken cancellationToken = default);
|
||||||
|
Task<BatchIpLookupResponse> GetBatchIpLocation(IEnumerable<string> ips, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user