[AZ-374] Refactor C21: named GoogleMapsTiles HttpClient in DI

- Register IHttpClientFactory named client "GoogleMapsTiles" inside
  AddTileDownloader() with User-Agent and 100s timeout (preserves
  HttpClient's implicit default).
- Resolve the same named client from all three CreateClient() call
  sites in GoogleMapsDownloaderV2 (session token, single-tile,
  batch-tile retry lambda) and drop the duplicated per-call
  UserAgent.ParseAdd setup.
- Expose USER_AGENT, the client name, and the timeout as internal
  consts on GoogleMapsDownloaderV2 so the extension and the
  downloader share one source of truth.
- Add AC test that builds the DI container, resolves the named
  client, and asserts both the User-Agent header and the timeout.
- Archive AZ-374 task file: todo/ -> done/.

175 unit + 5 smoke pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 04:11:57 +03:00
parent 1ca8c80d7b
commit 89b4bfd245
4 changed files with 36 additions and 6 deletions
@@ -14,7 +14,9 @@ namespace SatelliteProvider.Services.TileDownloader;
public class GoogleMapsDownloaderV2 : ISatelliteDownloader public class GoogleMapsDownloaderV2 : ISatelliteDownloader
{ {
private const string TILE_URL_TEMPLATE = "https://mt{0}.google.com/vt/lyrs=s&x={1}&y={2}&z={3}&token={4}"; private const string TILE_URL_TEMPLATE = "https://mt{0}.google.com/vt/lyrs=s&x={1}&y={2}&z={3}&token={4}";
private const string USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36"; internal const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36";
internal const string GoogleMapsTilesClientName = "GoogleMapsTiles";
internal const int DefaultHttpClientTimeoutSeconds = 100;
private readonly ILogger<GoogleMapsDownloaderV2> _logger; private readonly ILogger<GoogleMapsDownloaderV2> _logger;
private readonly string _apiKey; private readonly string _apiKey;
@@ -46,7 +48,7 @@ public class GoogleMapsDownloaderV2 : ISatelliteDownloader
private async Task<string?> GetSessionToken() private async Task<string?> GetSessionToken()
{ {
var url = $"https://tile.googleapis.com/v1/createSession?key={_apiKey}"; var url = $"https://tile.googleapis.com/v1/createSession?key={_apiKey}";
using var httpClient = _httpClientFactory.CreateClient(); using var httpClient = _httpClientFactory.CreateClient(GoogleMapsTilesClientName);
try try
{ {
var str = JsonConvert.SerializeObject(new { mapType = "satellite" }); var str = JsonConvert.SerializeObject(new { mapType = "satellite" });
@@ -102,8 +104,7 @@ public class GoogleMapsDownloaderV2 : ISatelliteDownloader
var imageBytes = await ExecuteWithRetryAsync(async () => var imageBytes = await ExecuteWithRetryAsync(async () =>
{ {
using var httpClient = _httpClientFactory.CreateClient(); using var httpClient = _httpClientFactory.CreateClient(GoogleMapsTilesClientName);
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT);
var response = await httpClient.GetAsync(url, token); var response = await httpClient.GetAsync(url, token);
@@ -366,8 +367,7 @@ public class GoogleMapsDownloaderV2 : ISatelliteDownloader
var imageBytes = await ExecuteWithRetryAsync(async () => var imageBytes = await ExecuteWithRetryAsync(async () =>
{ {
using var httpClient = _httpClientFactory.CreateClient(); using var httpClient = _httpClientFactory.CreateClient(GoogleMapsTilesClientName);
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT);
var response = await httpClient.GetAsync(url, token); var response = await httpClient.GetAsync(url, token);
@@ -8,6 +8,11 @@ public static class TileDownloaderServiceCollectionExtensions
public static IServiceCollection AddTileDownloader(this IServiceCollection services) public static IServiceCollection AddTileDownloader(this IServiceCollection services)
{ {
services.AddMemoryCache(); services.AddMemoryCache();
services.AddHttpClient(GoogleMapsDownloaderV2.GoogleMapsTilesClientName, c =>
{
c.DefaultRequestHeaders.UserAgent.ParseAdd(GoogleMapsDownloaderV2.UserAgent);
c.Timeout = TimeSpan.FromSeconds(GoogleMapsDownloaderV2.DefaultHttpClientTimeoutSeconds);
});
services.AddSingleton<ISatelliteDownloader, GoogleMapsDownloaderV2>(); services.AddSingleton<ISatelliteDownloader, GoogleMapsDownloaderV2>();
services.AddSingleton<ITileService, TileService>(); services.AddSingleton<ITileService, TileService>();
return services; return services;
@@ -1,5 +1,6 @@
using FluentAssertions; using FluentAssertions;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moq; using Moq;
@@ -464,6 +465,30 @@ public class TileServiceTests
} }
} }
public class TileDownloaderRegistrationTests
{
[Fact]
public void AddTileDownloader_RegistersNamedGoogleMapsTilesHttpClient_AZ374_AC1()
{
// Arrange
var services = new ServiceCollection();
// Act
services.AddTileDownloader();
using var provider = services.BuildServiceProvider();
var factory = provider.GetRequiredService<IHttpClientFactory>();
using var client = factory.CreateClient("GoogleMapsTiles");
// Assert
client.DefaultRequestHeaders.UserAgent.Should().NotBeEmpty(
"AZ-374 AC-1: the named GoogleMapsTiles client must carry the User-Agent set once at registration time");
client.DefaultRequestHeaders.UserAgent.ToString()
.Should().Contain("Mozilla/5.0", "preserves the existing outbound User-Agent for Google Maps");
client.Timeout.Should().Be(TimeSpan.FromSeconds(100),
"preserves the HttpClient default (100s) until C18 wires this to MapConfig");
}
}
public class GoogleMapsDownloaderZoomValidationTests public class GoogleMapsDownloaderZoomValidationTests
{ {
private static GoogleMapsDownloaderV2 BuildDownloader() private static GoogleMapsDownloaderV2 BuildDownloader()