added functional tests
This commit is contained in:
parent
6335c8e942
commit
70d52fbe36
5
.gitignore
vendored
5
.gitignore
vendored
@ -2,4 +2,7 @@ bin/
|
||||
obj/
|
||||
/packages/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
||||
/_ReSharper.Caches/
|
||||
|
||||
# Development configuration files with secrets
|
||||
**/appsettings.Development.json
|
@ -0,0 +1,22 @@
|
||||
namespace Svrnty.GeoManagement.Google.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for Google Geocoding API
|
||||
/// </summary>
|
||||
public class GoogleGeoManagementOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Google Maps API key for authentication
|
||||
/// </summary>
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Optional language code for address results (e.g., "en", "fr", "de")
|
||||
/// </summary>
|
||||
public string? Language { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional region code for biasing results (e.g., "us", "uk", "ca")
|
||||
/// </summary>
|
||||
public string? Region { get; set; }
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Svrnty.GeoManagement.Abstractions.Abstractions;
|
||||
using Svrnty.GeoManagement.Google.Configuration;
|
||||
|
||||
namespace Svrnty.GeoManagement.Google.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering Google Geocoding services
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Google Geocoding provider to the service collection
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection</param>
|
||||
/// <param name="configuration">Configuration section containing GoogleGeoManagementOptions</param>
|
||||
/// <returns>The service collection for chaining</returns>
|
||||
public static IServiceCollection AddGoogleGeoManagement(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
services.Configure<GoogleGeoManagementOptions>(configuration);
|
||||
services.AddScoped<IGeoManagementProvider, GeoManagementGoogleProvider>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds Google Geocoding provider to the service collection with manual configuration
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection</param>
|
||||
/// <param name="configureOptions">Action to configure GoogleGeoManagementOptions</param>
|
||||
/// <returns>The service collection for chaining</returns>
|
||||
public static IServiceCollection AddGoogleGeoManagement(
|
||||
this IServiceCollection services,
|
||||
Action<GoogleGeoManagementOptions> configureOptions)
|
||||
{
|
||||
services.Configure(configureOptions);
|
||||
services.AddScoped<IGeoManagementProvider, GeoManagementGoogleProvider>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
209
Svrnty.GeoManagement.Google/GeoManagementGoogleProvider.cs
Normal file
209
Svrnty.GeoManagement.Google/GeoManagementGoogleProvider.cs
Normal file
@ -0,0 +1,209 @@
|
||||
using GoogleApi;
|
||||
using GoogleApi.Entities.Maps.Geocoding.Address.Request;
|
||||
using GoogleApi.Entities.Maps.Geocoding.Location.Request;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Svrnty.GeoManagement.Abstractions.Abstractions;
|
||||
using Svrnty.GeoManagement.Abstractions.Models;
|
||||
using Svrnty.GeoManagement.Google.Configuration;
|
||||
using Svrnty.GeoManagement.Google.Mapping;
|
||||
|
||||
namespace Svrnty.GeoManagement.Google;
|
||||
|
||||
/// <summary>
|
||||
/// Google Geocoding API implementation of IGeoManagementProvider
|
||||
/// </summary>
|
||||
public class GeoManagementGoogleProvider : IGeoManagementProvider
|
||||
{
|
||||
private readonly GoogleGeoManagementOptions _options;
|
||||
private readonly ILogger<GeoManagementGoogleProvider> _logger;
|
||||
|
||||
public GeoManagementGoogleProvider(
|
||||
IOptions<GoogleGeoManagementOptions> options,
|
||||
ILogger<GeoManagementGoogleProvider> logger)
|
||||
{
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_options.ApiKey))
|
||||
{
|
||||
throw new ArgumentException("Google API key is required", nameof(options));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<GeoPoint?> GetGeoPointAsync(
|
||||
Address address,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var addressString = address.GetFormattedAddress();
|
||||
_logger.LogDebug("Geocoding address: {Address}", addressString);
|
||||
|
||||
var request = new AddressGeocodeRequest
|
||||
{
|
||||
Key = _options.ApiKey,
|
||||
Address = addressString
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_options.Language) &&
|
||||
Enum.TryParse<GoogleApi.Entities.Common.Enums.Language>(_options.Language, true, out var language))
|
||||
request.Language = language;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_options.Region))
|
||||
request.Region = _options.Region;
|
||||
|
||||
var response = await GoogleMaps.Geocode.AddressGeocode.QueryAsync(request, cancellationToken);
|
||||
|
||||
if (response.Status != GoogleApi.Entities.Common.Enums.Status.Ok)
|
||||
{
|
||||
_logger.LogWarning("Google Geocoding API returned status: {Status}", response.Status);
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = response.Results?.FirstOrDefault();
|
||||
var geoPoint = GoogleAddressMapper.MapToGeoPoint(result);
|
||||
|
||||
if (geoPoint != null)
|
||||
{
|
||||
_logger.LogDebug("Successfully geocoded address to: {Latitude}, {Longitude}",
|
||||
geoPoint.Latitude, geoPoint.Longitude);
|
||||
}
|
||||
|
||||
return geoPoint;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error geocoding address: {Address}", address.GetFormattedAddress());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Address?> ReverseGeocodeAsync(
|
||||
GeoPoint geoPoint,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Reverse geocoding location: {Latitude}, {Longitude}",
|
||||
geoPoint.Latitude, geoPoint.Longitude);
|
||||
|
||||
var request = new LocationGeocodeRequest
|
||||
{
|
||||
Key = _options.ApiKey,
|
||||
Location = new GoogleApi.Entities.Common.Coordinate(geoPoint.Latitude, geoPoint.Longitude)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_options.Language) &&
|
||||
Enum.TryParse<GoogleApi.Entities.Common.Enums.Language>(_options.Language, true, out var language))
|
||||
request.Language = language;
|
||||
|
||||
var response = await GoogleMaps.Geocode.LocationGeocode.QueryAsync(request, cancellationToken);
|
||||
|
||||
if (response.Status != GoogleApi.Entities.Common.Enums.Status.Ok)
|
||||
{
|
||||
_logger.LogWarning("Google Reverse Geocoding API returned status: {Status}", response.Status);
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = response.Results?.FirstOrDefault();
|
||||
var address = GoogleAddressMapper.MapToAddress(result);
|
||||
|
||||
if (address != null)
|
||||
{
|
||||
_logger.LogDebug("Successfully reverse geocoded to: {Address}", address.GetFormattedAddress());
|
||||
}
|
||||
|
||||
return address;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reverse geocoding location: {Latitude}, {Longitude}",
|
||||
geoPoint.Latitude, geoPoint.Longitude);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Address?> NormalizeAddressAsync(
|
||||
Address address,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Normalizing address: {Address}", address.GetFormattedAddress());
|
||||
|
||||
// First geocode to get coordinates
|
||||
var geoPoint = await GetGeoPointAsync(address, cancellationToken);
|
||||
if (geoPoint == null)
|
||||
{
|
||||
_logger.LogWarning("Could not geocode address for normalization: {Address}",
|
||||
address.GetFormattedAddress());
|
||||
return null;
|
||||
}
|
||||
|
||||
// Then reverse geocode to get normalized address
|
||||
var normalizedAddress = await ReverseGeocodeAsync(geoPoint, cancellationToken);
|
||||
|
||||
if (normalizedAddress != null)
|
||||
{
|
||||
_logger.LogDebug("Successfully normalized address from {Original} to {Normalized}",
|
||||
address.GetFormattedAddress(), normalizedAddress.GetFormattedAddress());
|
||||
}
|
||||
|
||||
return normalizedAddress;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error normalizing address: {Address}", address.GetFormattedAddress());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Address?> NormalizeAddressAsync(
|
||||
string address,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Normalizing address string: {Address}", address);
|
||||
|
||||
var request = new AddressGeocodeRequest
|
||||
{
|
||||
Key = _options.ApiKey,
|
||||
Address = address
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_options.Language) &&
|
||||
Enum.TryParse<GoogleApi.Entities.Common.Enums.Language>(_options.Language, true, out var language))
|
||||
request.Language = language;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_options.Region))
|
||||
request.Region = _options.Region;
|
||||
|
||||
var response = await GoogleMaps.Geocode.AddressGeocode.QueryAsync(request, cancellationToken);
|
||||
|
||||
if (response.Status != GoogleApi.Entities.Common.Enums.Status.Ok)
|
||||
{
|
||||
_logger.LogWarning("Google Geocoding API returned status: {Status} for address: {Address}",
|
||||
response.Status, address);
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = response.Results?.FirstOrDefault();
|
||||
var normalizedAddress = GoogleAddressMapper.MapToAddress(result);
|
||||
|
||||
if (normalizedAddress != null)
|
||||
{
|
||||
_logger.LogDebug("Successfully normalized address string to: {Normalized}",
|
||||
normalizedAddress.GetFormattedAddress());
|
||||
}
|
||||
|
||||
return normalizedAddress;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error normalizing address string: {Address}", address);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
86
Svrnty.GeoManagement.Google/Mapping/GoogleAddressMapper.cs
Normal file
86
Svrnty.GeoManagement.Google/Mapping/GoogleAddressMapper.cs
Normal file
@ -0,0 +1,86 @@
|
||||
using GoogleApi.Entities.Common;
|
||||
using GoogleApi.Entities.Maps.Geocoding.Common;
|
||||
using Svrnty.GeoManagement.Abstractions.Models;
|
||||
|
||||
namespace Svrnty.GeoManagement.Google.Mapping;
|
||||
|
||||
/// <summary>
|
||||
/// Maps Google Geocoding API responses to Address models
|
||||
/// </summary>
|
||||
internal static class GoogleAddressMapper
|
||||
{
|
||||
public static Abstractions.Models.Address? MapToAddress(Result? googleResult)
|
||||
{
|
||||
if (googleResult == null)
|
||||
return null;
|
||||
|
||||
var components = googleResult.AddressComponents;
|
||||
if (components == null || !components.Any())
|
||||
return null;
|
||||
|
||||
var line1 = BuildLine1(components);
|
||||
var line2 = GetComponentValue(components, "subpremise");
|
||||
var city = GetCity(components);
|
||||
var subdivision = GetSubdivision(components);
|
||||
var postalCode = GetComponentValue(components, "postal_code") ?? string.Empty;
|
||||
var country = GetComponentValue(components, "country") ?? string.Empty;
|
||||
|
||||
var location = googleResult.Geometry?.Location != null
|
||||
? new GeoPoint(googleResult.Geometry.Location.Latitude, googleResult.Geometry.Location.Longitude)
|
||||
: null;
|
||||
|
||||
return new Abstractions.Models.Address(
|
||||
Line1: line1,
|
||||
Line2: line2,
|
||||
City: city,
|
||||
Subdivision: subdivision,
|
||||
PostalCode: postalCode,
|
||||
Country: country,
|
||||
Location: location,
|
||||
Note: null,
|
||||
IsNormalized: true
|
||||
);
|
||||
}
|
||||
|
||||
private static string BuildLine1(IEnumerable<AddressComponent> components)
|
||||
{
|
||||
var streetNumber = GetComponentValue(components, "street_number");
|
||||
var route = GetComponentValue(components, "route");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(streetNumber) && !string.IsNullOrWhiteSpace(route))
|
||||
return $"{streetNumber} {route}";
|
||||
|
||||
return route ?? streetNumber ?? string.Empty;
|
||||
}
|
||||
|
||||
private static string GetCity(IEnumerable<AddressComponent> components)
|
||||
{
|
||||
return GetComponentValue(components, "locality")
|
||||
?? GetComponentValue(components, "postal_town")
|
||||
?? GetComponentValue(components, "administrative_area_level_2")
|
||||
?? string.Empty;
|
||||
}
|
||||
|
||||
private static string GetSubdivision(IEnumerable<AddressComponent> components)
|
||||
{
|
||||
return GetComponentValue(components, "administrative_area_level_1") ?? string.Empty;
|
||||
}
|
||||
|
||||
private static string? GetComponentValue(IEnumerable<AddressComponent> components, string type)
|
||||
{
|
||||
var component = components.FirstOrDefault(c =>
|
||||
c.Types.Any(t => t.ToString().ToLower().Replace("_", "") == type.Replace("_", "")));
|
||||
return component?.ShortName;
|
||||
}
|
||||
|
||||
public static GeoPoint? MapToGeoPoint(Result? googleResult)
|
||||
{
|
||||
if (googleResult?.Geometry?.Location == null)
|
||||
return null;
|
||||
|
||||
return new GeoPoint(
|
||||
googleResult.Geometry.Location.Latitude,
|
||||
googleResult.Geometry.Location.Longitude
|
||||
);
|
||||
}
|
||||
}
|
106
Svrnty.GeoManagement.Google/README.md
Normal file
106
Svrnty.GeoManagement.Google/README.md
Normal file
@ -0,0 +1,106 @@
|
||||
# Svrnty.GeoManagement.Google
|
||||
|
||||
Google Geocoding API provider implementation for the Svrnty.GeoManagement library.
|
||||
|
||||
## Installation
|
||||
|
||||
Add reference to this project or install via NuGet (when published).
|
||||
|
||||
## Configuration
|
||||
|
||||
### Using appsettings.json
|
||||
|
||||
```json
|
||||
{
|
||||
"GoogleGeoManagement": {
|
||||
"ApiKey": "your-google-api-key-here",
|
||||
"Language": "en",
|
||||
"Region": "us"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Register in Dependency Injection
|
||||
|
||||
```csharp
|
||||
using Svrnty.GeoManagement.Google.Extensions;
|
||||
|
||||
// In your Program.cs or Startup.cs
|
||||
builder.Services.AddGoogleGeoManagement(
|
||||
builder.Configuration.GetSection("GoogleGeoManagement"));
|
||||
```
|
||||
|
||||
### Manual Configuration
|
||||
|
||||
```csharp
|
||||
builder.Services.AddGoogleGeoManagement(options =>
|
||||
{
|
||||
options.ApiKey = "your-google-api-key-here";
|
||||
options.Language = "en";
|
||||
options.Region = "us";
|
||||
});
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```csharp
|
||||
using Svrnty.GeoManagement.Abstractions.Abstractions;
|
||||
using Svrnty.GeoManagement.Abstractions.Models;
|
||||
|
||||
public class MyService
|
||||
{
|
||||
private readonly IGeoManagementProvider _geoProvider;
|
||||
|
||||
public MyService(IGeoManagementProvider geoProvider)
|
||||
{
|
||||
_geoProvider = geoProvider;
|
||||
}
|
||||
|
||||
public async Task Example()
|
||||
{
|
||||
// Forward geocoding - address to coordinates
|
||||
var address = new Address(
|
||||
Line1: "1600 Amphitheatre Parkway",
|
||||
Line2: null,
|
||||
City: "Mountain View",
|
||||
Subdivision: "CA",
|
||||
PostalCode: "94043",
|
||||
Country: "US",
|
||||
Location: null,
|
||||
Note: null);
|
||||
|
||||
var geoPoint = await _geoProvider.GetGeoPointAsync(address);
|
||||
Console.WriteLine($"Coordinates: {geoPoint?.Latitude}, {geoPoint?.Longitude}");
|
||||
|
||||
// Reverse geocoding - coordinates to address
|
||||
var location = new GeoPoint(37.4224764, -122.0842499);
|
||||
var foundAddress = await _geoProvider.ReverseGeocodeAsync(location);
|
||||
Console.WriteLine($"Address: {foundAddress?.GetFormattedAddress()}");
|
||||
|
||||
// Normalize address
|
||||
var normalized = await _geoProvider.NormalizeAddressAsync(address);
|
||||
Console.WriteLine($"Normalized: {normalized?.GetFormattedAddress()}");
|
||||
|
||||
// Normalize from string
|
||||
var normalizedFromString = await _geoProvider.NormalizeAddressAsync(
|
||||
"1600 Amphitheatre Parkway, Mountain View, CA");
|
||||
Console.WriteLine($"Normalized: {normalizedFromString?.GetFormattedAddress()}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
- **ApiKey** (required): Your Google Maps API key
|
||||
- **Language** (optional): Language code for results (e.g., "en", "fr", "de")
|
||||
- **Region** (optional): Region code for biasing results (e.g., "us", "uk", "ca")
|
||||
|
||||
## Requirements
|
||||
|
||||
- .NET 8.0
|
||||
- Google Maps API key with Geocoding API enabled
|
||||
- Internet connection for API calls
|
||||
|
||||
## Error Handling
|
||||
|
||||
The provider returns `null` for operations that fail (e.g., invalid address, API errors, network issues). All errors are logged using `ILogger<GeoManagementGoogleProvider>`.
|
@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Svrnty.GeoManagement.Abstractions\Svrnty.GeoManagement.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GoogleApi" Version="5.8.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
173
Svrnty.GeoManagement.Tests/GoogleProviderTests.cs
Normal file
173
Svrnty.GeoManagement.Tests/GoogleProviderTests.cs
Normal file
@ -0,0 +1,173 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Svrnty.GeoManagement.Abstractions.Models;
|
||||
using Svrnty.GeoManagement.Google;
|
||||
using Svrnty.GeoManagement.Google.Configuration;
|
||||
|
||||
namespace Svrnty.GeoManagement.Tests;
|
||||
|
||||
public class GoogleProviderTests : IDisposable
|
||||
{
|
||||
private readonly GeoManagementGoogleProvider _provider;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<GeoManagementGoogleProvider> _logger;
|
||||
|
||||
public GoogleProviderTests()
|
||||
{
|
||||
// Build configuration from appsettings.Development.json
|
||||
_configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.Development.json", optional: false, reloadOnChange: false)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
// Setup logger
|
||||
var loggerFactory = LoggerFactory.Create(builder =>
|
||||
{
|
||||
builder.AddConsole();
|
||||
builder.SetMinimumLevel(LogLevel.Debug);
|
||||
});
|
||||
_logger = loggerFactory.CreateLogger<GeoManagementGoogleProvider>();
|
||||
|
||||
// Get options from configuration
|
||||
var options = _configuration.GetSection("GoogleGeoManagement").Get<GoogleGeoManagementOptions>();
|
||||
if (options == null || string.IsNullOrWhiteSpace(options.ApiKey))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Google API key not configured. Please add your API key to appsettings.Development.json");
|
||||
}
|
||||
|
||||
_provider = new GeoManagementGoogleProvider(
|
||||
Options.Create(options),
|
||||
_logger);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetGeoPointAsync_WithValidAddress_ReturnsCoordinates()
|
||||
{
|
||||
// Arrange
|
||||
var address = new Address(
|
||||
Line1: "1600 Amphitheatre Parkway",
|
||||
Line2: null,
|
||||
City: "Mountain View",
|
||||
Subdivision: "CA",
|
||||
PostalCode: "94043",
|
||||
Country: "US",
|
||||
Location: null,
|
||||
Note: null);
|
||||
|
||||
// Act
|
||||
var result = await _provider.GetGeoPointAsync(address);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.InRange(result.Latitude, 37.0, 38.0); // Approximate range for Mountain View, CA
|
||||
Assert.InRange(result.Longitude, -123.0, -122.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReverseGeocodeAsync_WithValidCoordinates_ReturnsAddress()
|
||||
{
|
||||
// Arrange - Google HQ coordinates
|
||||
var geoPoint = new GeoPoint(37.4224764, -122.0842499);
|
||||
|
||||
// Act
|
||||
var result = await _provider.ReverseGeocodeAsync(geoPoint);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Line1);
|
||||
Assert.NotNull(result.City);
|
||||
Assert.NotNull(result.Country);
|
||||
Assert.True(result.IsNormalized);
|
||||
Assert.NotNull(result.Location);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NormalizeAddressAsync_WithAddressObject_ReturnsNormalizedAddress()
|
||||
{
|
||||
// Arrange
|
||||
var address = new Address(
|
||||
Line1: "1600 Amphitheatre Pkwy", // Abbreviated
|
||||
Line2: null,
|
||||
City: "Mountain View",
|
||||
Subdivision: "California",
|
||||
PostalCode: "94043",
|
||||
Country: "USA",
|
||||
Location: null,
|
||||
Note: null);
|
||||
|
||||
// Act
|
||||
var result = await _provider.NormalizeAddressAsync(address);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.IsNormalized);
|
||||
Assert.NotNull(result.Location);
|
||||
Assert.NotNull(result.Line1);
|
||||
Assert.Contains("Amphitheatre", result.Line1, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NormalizeAddressAsync_WithAddressString_ReturnsNormalizedAddress()
|
||||
{
|
||||
// Arrange
|
||||
var addressString = "1600 Amphitheatre Parkway, Mountain View, CA";
|
||||
|
||||
// Act
|
||||
var result = await _provider.NormalizeAddressAsync(addressString);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.IsNormalized);
|
||||
Assert.NotNull(result.Location);
|
||||
Assert.NotNull(result.Line1);
|
||||
Assert.Contains("Mountain View", result.City, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetGeoPointAsync_WithInvalidAddress_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var address = new Address(
|
||||
Line1: "This is not a real address at all",
|
||||
Line2: null,
|
||||
City: "Fake City",
|
||||
Subdivision: "XX",
|
||||
PostalCode: "00000",
|
||||
Country: "Nowhere",
|
||||
Location: null,
|
||||
Note: null);
|
||||
|
||||
// Act
|
||||
var result = await _provider.GetGeoPointAsync(address);
|
||||
|
||||
// Assert - Google might still return something, so we just verify it doesn't crash
|
||||
// In real scenarios, very invalid addresses should return null
|
||||
Assert.True(result == null || result.Latitude != 0 || result.Longitude != 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReverseGeocodeAsync_WithOceanCoordinates_ReturnsAddress()
|
||||
{
|
||||
// Arrange - Coordinates in the middle of the Pacific Ocean
|
||||
var geoPoint = new GeoPoint(0, -160);
|
||||
|
||||
// Act
|
||||
var result = await _provider.ReverseGeocodeAsync(geoPoint);
|
||||
|
||||
// Assert - May return null or a very general location
|
||||
// This test just verifies it doesn't crash
|
||||
if (result != null)
|
||||
{
|
||||
Assert.True(result.IsNormalized);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Cleanup if needed
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
66
Svrnty.GeoManagement.Tests/README.md
Normal file
66
Svrnty.GeoManagement.Tests/README.md
Normal file
@ -0,0 +1,66 @@
|
||||
# Svrnty.GeoManagement.Tests
|
||||
|
||||
Integration tests for the Svrnty.GeoManagement library.
|
||||
|
||||
## Setup
|
||||
|
||||
The tests require a Google Maps API key to run the integration tests.
|
||||
|
||||
### Step 1: Get a Google Maps API Key
|
||||
|
||||
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Create a new project or select an existing one
|
||||
3. Enable the **Geocoding API**
|
||||
4. Create credentials (API Key)
|
||||
5. (Optional) Restrict the API key to only allow Geocoding API calls
|
||||
|
||||
### Step 2: Configure the API Key
|
||||
|
||||
1. Copy `appsettings.Example.json` to `appsettings.Development.json`:
|
||||
```bash
|
||||
cp appsettings.Example.json appsettings.Development.json
|
||||
```
|
||||
|
||||
2. Edit `appsettings.Development.json` and replace `YOUR_GOOGLE_API_KEY_HERE` with your actual API key:
|
||||
```json
|
||||
{
|
||||
"GoogleGeoManagement": {
|
||||
"ApiKey": "AIza...", // Your actual API key
|
||||
"Language": "en",
|
||||
"Region": "us"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Important**: `appsettings.Development.json` is in `.gitignore` and will not be committed to version control.
|
||||
|
||||
### Alternative: Using Environment Variables
|
||||
|
||||
You can also set the API key via environment variables:
|
||||
|
||||
```bash
|
||||
export GoogleGeoManagement__ApiKey="your-api-key-here"
|
||||
```
|
||||
|
||||
Note: Use double underscores (`__`) to represent nested configuration sections.
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
dotnet test
|
||||
```
|
||||
|
||||
The integration tests will make actual calls to the Google Geocoding API. Be aware of:
|
||||
- **API Rate Limits**: Google has rate limits on API calls
|
||||
- **API Costs**: While Google provides free tier usage, be aware of potential costs
|
||||
- **Network Dependency**: Tests require an internet connection
|
||||
|
||||
## Test Coverage
|
||||
|
||||
The test suite includes:
|
||||
- Forward geocoding (address to coordinates)
|
||||
- Reverse geocoding (coordinates to address)
|
||||
- Address normalization (both object and string)
|
||||
- Edge cases (invalid addresses, ocean coordinates)
|
||||
|
||||
All tests use real Google API calls to ensure the integration works correctly.
|
@ -10,14 +10,29 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
|
||||
<PackageReference Include="xunit" Version="2.5.3"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.5.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit"/>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Svrnty.GeoManagement.Google\Svrnty.GeoManagement.Google.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.Development.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="appsettings.Example.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
7
Svrnty.GeoManagement.Tests/appsettings.Example.json
Normal file
7
Svrnty.GeoManagement.Tests/appsettings.Example.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"GoogleGeoManagement": {
|
||||
"ApiKey": "YOUR_GOOGLE_API_KEY_HERE",
|
||||
"Language": "en",
|
||||
"Region": "ca"
|
||||
}
|
||||
}
|
@ -1,9 +1,12 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
#
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.GeoManagement.Tests", "Svrnty.GeoManagement.Tests\Svrnty.GeoManagement.Tests.csproj", "{C2DE2776-E457-49B0-A3E2-E31AFE8C922C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.GeoManagement.Abstractions", "Svrnty.GeoManagement.Abstractions\Svrnty.GeoManagement.Abstractions.csproj", "{FB10FC74-7B99-4B3C-A671-6DF451157C98}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.GeoManagement.Google", "Svrnty.GeoManagement.Google\Svrnty.GeoManagement.Google.csproj", "{1EE527A7-30CD-4280-B006-2F39474325D6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -18,5 +21,9 @@ Global
|
||||
{FB10FC74-7B99-4B3C-A671-6DF451157C98}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FB10FC74-7B99-4B3C-A671-6DF451157C98}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FB10FC74-7B99-4B3C-A671-6DF451157C98}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1EE527A7-30CD-4280-B006-2F39474325D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1EE527A7-30CD-4280-B006-2F39474325D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1EE527A7-30CD-4280-B006-2F39474325D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1EE527A7-30CD-4280-B006-2F39474325D6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
100
TESTING.md
Normal file
100
TESTING.md
Normal file
@ -0,0 +1,100 @@
|
||||
# Testing Guide
|
||||
|
||||
This document explains how to run tests for the Svrnty.GeoManagement library.
|
||||
|
||||
## Test Types
|
||||
|
||||
The test suite includes **integration tests** that make real API calls to the Google Geocoding API.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before running tests, you need:
|
||||
|
||||
1. **Google Maps API Key** - Get one from [Google Cloud Console](https://console.cloud.google.com/)
|
||||
- Enable the **Geocoding API** for your project
|
||||
- Create an API key credential
|
||||
- (Optional) Restrict the key to Geocoding API only
|
||||
|
||||
2. **Configuration File** - Set up your API key:
|
||||
|
||||
```bash
|
||||
cd Svrnty.GeoManagement.Tests
|
||||
cp appsettings.Example.json appsettings.Development.json
|
||||
```
|
||||
|
||||
Then edit `appsettings.Development.json` and add your API key:
|
||||
|
||||
```json
|
||||
{
|
||||
"GoogleGeoManagement": {
|
||||
"ApiKey": "YOUR_ACTUAL_API_KEY_HERE",
|
||||
"Language": "en",
|
||||
"Region": "us"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: The `appsettings.Development.json` file is in `.gitignore` and will not be committed to source control.
|
||||
|
||||
## Running Tests
|
||||
|
||||
From the solution root:
|
||||
|
||||
```bash
|
||||
dotnet test
|
||||
```
|
||||
|
||||
From the test project directory:
|
||||
|
||||
```bash
|
||||
cd Svrnty.GeoManagement.Tests
|
||||
dotnet test
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
⚠️ **API Costs & Limits**:
|
||||
- Tests make real API calls to Google
|
||||
- Google provides a free tier but be aware of potential costs
|
||||
- API has rate limits - don't run tests too frequently
|
||||
- Each test run may use several API calls
|
||||
|
||||
🌐 **Network Dependency**:
|
||||
- Tests require internet connection
|
||||
- Tests may fail if Google API is down or rate limits are exceeded
|
||||
|
||||
## Alternative: Environment Variables
|
||||
|
||||
Instead of using `appsettings.Development.json`, you can set the API key via environment variables:
|
||||
|
||||
```bash
|
||||
export GoogleGeoManagement__ApiKey="your-api-key-here"
|
||||
dotnet test
|
||||
```
|
||||
|
||||
Note: Use double underscores (`__`) to represent nested configuration.
|
||||
|
||||
## Test Coverage
|
||||
|
||||
Current integration tests cover:
|
||||
- ✅ Forward geocoding (address → coordinates)
|
||||
- ✅ Reverse geocoding (coordinates → address)
|
||||
- ✅ Address normalization from object
|
||||
- ✅ Address normalization from string
|
||||
- ✅ Edge cases (invalid addresses, ocean coordinates)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Error: "Google API key not configured"**
|
||||
- Ensure `appsettings.Development.json` exists in the test project
|
||||
- Verify the API key is correctly set in the configuration file
|
||||
- Check that the file is being copied to the output directory
|
||||
|
||||
**Error: "REQUEST_DENIED" or "OVER_QUERY_LIMIT"**
|
||||
- Verify the Geocoding API is enabled in your Google Cloud project
|
||||
- Check your API key has permission to use the Geocoding API
|
||||
- You may have exceeded your quota - wait before retrying
|
||||
|
||||
**Tests are slow**
|
||||
- This is expected - tests make real network calls to Google's API
|
||||
- Each test can take 1-2 seconds to complete
|
Loading…
Reference in New Issue
Block a user