[AZ-371] Refactor C18: magic numbers to ProcessingConfig/MapConfig

Promotes 8 operational levers into config keys with defaults that match
the prior source literals byte-for-byte:
  ProcessingConfig: RegionProcessingTimeoutSeconds (300),
  RouteProcessingPollIntervalSeconds (5),
  MaxRoutePointSpacingMeters (200), LatLonTolerance (0.0001).
  MapConfig: TileSizePixels (256), AllowedZoomLevels ([15..19]),
  RetryBaseDelaySeconds (1), RetryMaxDelaySeconds (30).

Sites updated: RegionService, RouteProcessingService,
RoutePointGraphBuilder, RouteValidator, RouteService 4-arg ctor,
RouteImageRenderer, GoogleMapsDownloaderV2, TileService. Closes LF-2 by
forwarding HttpContext.RequestAborted from GetTileByLatLon into the
downloader. appsettings.json gains the 8 new keys at default values.

Tests: 141 / 141 unit + 5 / 5 smoke green. New ConfigDefaultsTests pins
defaults to original literals; new TileService unit test asserts CT
identity from caller to downloader (AZ-371 AC-3).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 03:30:07 +03:00
parent ee42b1716b
commit 1dcd089d39
21 changed files with 404 additions and 68 deletions
@@ -0,0 +1,62 @@
using FluentAssertions;
using SatelliteProvider.Common.Configs;
namespace SatelliteProvider.Tests;
// AZ-371 / C18 — verifies the config defaults preserve the original literal values
// that lived in source code prior to the magic-numbers-to-config refactor.
public class ConfigDefaultsTests
{
[Fact]
public void ProcessingConfig_RegionProcessingTimeout_PreservesOriginal_AZ371_AC2()
{
// Assert
new ProcessingConfig().RegionProcessingTimeoutSeconds.Should().Be(300, "RegionService used TimeSpan.FromMinutes(5) before AZ-371");
}
[Fact]
public void ProcessingConfig_RouteProcessingPollInterval_PreservesOriginal_AZ371_AC2()
{
// Assert
new ProcessingConfig().RouteProcessingPollIntervalSeconds.Should().Be(5, "RouteProcessingService polled every 5s before AZ-371");
}
[Fact]
public void ProcessingConfig_MaxRoutePointSpacingMeters_PreservesOriginal_AZ371_AC2()
{
// Assert
new ProcessingConfig().MaxRoutePointSpacingMeters.Should().Be(200.0, "RoutePointGraphBuilder used 200m spacing before AZ-371");
}
[Fact]
public void ProcessingConfig_LatLonTolerance_PreservesOriginal_AZ371_AC2()
{
// Assert
new ProcessingConfig().LatLonTolerance.Should().Be(0.0001, "RouteValidator and GoogleMapsDownloaderV2 used 0.0001 before AZ-371");
}
[Fact]
public void MapConfig_TileSizePixels_PreservesOriginal_AZ371_AC2()
{
// Assert
new MapConfig().TileSizePixels.Should().Be(256, "TileService and GoogleMapsDownloaderV2 used 256 px before AZ-371");
}
[Fact]
public void MapConfig_AllowedZoomLevels_PreservesOriginal_AZ371_AC2()
{
// Assert
new MapConfig().AllowedZoomLevels.Should().Equal(15, 16, 17, 18, 19);
}
[Fact]
public void MapConfig_RetryDelays_PreserveOriginal_AZ371_AC2()
{
// Arrange
var cfg = new MapConfig();
// Assert
cfg.RetryBaseDelaySeconds.Should().Be(1);
cfg.RetryMaxDelaySeconds.Should().Be(30);
}
}
@@ -1,7 +1,9 @@
using FluentAssertions;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using SatelliteProvider.Common.Configs;
using SatelliteProvider.Common.DTO;
using SatelliteProvider.Common.Interfaces;
using SatelliteProvider.DataAccess.Models;
@@ -65,7 +67,8 @@ public class InfrastructureTests
var logger = NullLogger<TileService>.Instance;
// Act
var service = new TileService(downloader, tileRepo, cache, logger);
var mapConfig = Options.Create(new MapConfig { Service = "GoogleMaps", ApiKey = "" });
var service = new TileService(downloader, tileRepo, cache, mapConfig, logger);
// Assert
service.Should().NotBeNull();
@@ -36,7 +36,8 @@ public class RegionServiceTests : IDisposable
Mock<ITileService> tileService)
{
var storage = Options.Create(new StorageConfig { ReadyDirectory = _readyDir, TilesDirectory = "/tiles" });
return new RegionService(regionRepo.Object, queue.Object, tileService.Object, storage, NullLogger<RegionService>.Instance);
var processing = Options.Create(new ProcessingConfig());
return new RegionService(regionRepo.Object, queue.Object, tileService.Object, storage, processing, NullLogger<RegionService>.Instance);
}
[Fact]
@@ -17,7 +17,8 @@ public class RouteImageRendererTests
{
loggerMock = new Mock<ILogger<RouteImageRenderer>>();
var storageOptions = Options.Create(new StorageConfig());
return new RouteImageRenderer(storageOptions, loggerMock.Object);
var mapOptions = Options.Create(new MapConfig { Service = "GoogleMaps", ApiKey = "" });
return new RouteImageRenderer(storageOptions, mapOptions, loggerMock.Object);
}
private static void VerifyWarningLogged(Mock<ILogger<RouteImageRenderer>> loggerMock, string substringInState)
@@ -1,4 +1,6 @@
using FluentAssertions;
using Microsoft.Extensions.Options;
using SatelliteProvider.Common.Configs;
using SatelliteProvider.Common.DTO;
using SatelliteProvider.Common.Utils;
using SatelliteProvider.Services.RouteManagement;
@@ -8,13 +10,18 @@ namespace SatelliteProvider.Tests;
public class RoutePointGraphBuilderTests
{
private static readonly ProcessingConfig DefaultProcessingConfig = new();
private static RoutePointGraphBuilder MakeBuilder() =>
new(Options.Create(new ProcessingConfig()));
private static List<RoutePoint> ToRoutePoints(IEnumerable<(double Lat, double Lon)> points) =>
points.Select(p => new RoutePoint { Latitude = p.Lat, Longitude = p.Lon }).ToList();
[Fact]
public void Build_TwoUserPoints_FirstIsStart_LastIsEnd_BetweenAreIntermediate()
{
var sut = new RoutePointGraphBuilder();
var sut = MakeBuilder();
var input = ToRoutePoints(TestCoordinates.Route.Route01Points);
var graph = sut.Build(input);
@@ -28,7 +35,7 @@ public class RoutePointGraphBuilderTests
[Fact]
public void Build_ConsecutivePointsRespectMaxSpacing()
{
var sut = new RoutePointGraphBuilder();
var sut = MakeBuilder();
var input = ToRoutePoints(TestCoordinates.Route.Route01Points);
var graph = sut.Build(input);
@@ -40,15 +47,15 @@ public class RoutePointGraphBuilderTests
var distance = GeoUtils.CalculateDistance(
new GeoPoint(prev.Latitude, prev.Longitude),
new GeoPoint(cur.Latitude, cur.Longitude));
distance.Should().BeLessThanOrEqualTo(RoutePointGraphBuilder.MaxPointSpacingMeters + 0.5,
$"point {i - 1}→{i} must be ≤{RoutePointGraphBuilder.MaxPointSpacingMeters}m");
distance.Should().BeLessThanOrEqualTo(DefaultProcessingConfig.MaxRoutePointSpacingMeters + 0.5,
$"point {i - 1}→{i} must be ≤{DefaultProcessingConfig.MaxRoutePointSpacingMeters}m");
}
}
[Fact]
public void Build_TenPointRoute_HasOneStartOneEndAndEightAction()
{
var sut = new RoutePointGraphBuilder();
var sut = MakeBuilder();
var input = ToRoutePoints(TestCoordinates.Route.Route04Points);
var graph = sut.Build(input);
@@ -62,7 +69,7 @@ public class RoutePointGraphBuilderTests
[Fact]
public void Build_TotalDistanceEqualsSumOfHaversineSegments()
{
var sut = new RoutePointGraphBuilder();
var sut = MakeBuilder();
var input = ToRoutePoints(TestCoordinates.Route.Route01Points);
var graph = sut.Build(input);
@@ -83,7 +90,7 @@ public class RoutePointGraphBuilderTests
[Fact]
public void Build_SequenceNumbersAreContiguousAndStartAtZero()
{
var sut = new RoutePointGraphBuilder();
var sut = MakeBuilder();
var input = ToRoutePoints(TestCoordinates.Route.Route04Points);
var graph = sut.Build(input);
@@ -95,7 +102,7 @@ public class RoutePointGraphBuilderTests
[Fact]
public void Build_FirstPointHasNullDistanceFromPrevious()
{
var sut = new RoutePointGraphBuilder();
var sut = MakeBuilder();
var input = ToRoutePoints(TestCoordinates.Route.Route01Points);
var graph = sut.Build(input);
@@ -107,7 +114,7 @@ public class RoutePointGraphBuilderTests
[Fact]
public void Build_FewerThanTwoPoints_Throws()
{
var sut = new RoutePointGraphBuilder();
var sut = MakeBuilder();
var input = new List<RoutePoint> { new() { Latitude = 47.46, Longitude = 37.64 } };
Action act = () => sut.Build(input);
@@ -118,7 +125,7 @@ public class RoutePointGraphBuilderTests
[Fact]
public void Build_NullInput_Throws()
{
var sut = new RoutePointGraphBuilder();
var sut = MakeBuilder();
Action act = () => sut.Build(null!);
+3 -1
View File
@@ -1,6 +1,8 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using SatelliteProvider.Common.Configs;
using SatelliteProvider.Common.DTO;
using SatelliteProvider.Common.Interfaces;
using SatelliteProvider.Common.Utils;
@@ -17,7 +19,7 @@ public class RouteServiceTests
Mock<IRouteRepository> routeRepo,
Mock<IRegionService> regionService)
{
return new RouteService(routeRepo.Object, regionService.Object, NullLogger<RouteService>.Instance);
return new RouteService(routeRepo.Object, regionService.Object, Options.Create(new ProcessingConfig()), NullLogger<RouteService>.Instance);
}
private static CreateRouteRequest BuildRequest(IEnumerable<(double Lat, double Lon)> points, double regionSize = 500, int zoom = 18, bool requestMaps = false, Geofences? geofences = null)
+15 -10
View File
@@ -1,4 +1,6 @@
using FluentAssertions;
using Microsoft.Extensions.Options;
using SatelliteProvider.Common.Configs;
using SatelliteProvider.Common.DTO;
using SatelliteProvider.Services.RouteManagement;
using SatelliteProvider.Tests.Fixtures;
@@ -7,6 +9,9 @@ namespace SatelliteProvider.Tests;
public class RouteValidatorTests
{
private static RouteValidator MakeValidator() =>
new(Options.Create(new ProcessingConfig()));
private static CreateRouteRequest BuildValidRequest()
{
return new CreateRouteRequest
@@ -25,7 +30,7 @@ public class RouteValidatorTests
[Fact]
public void Validate_ValidRequest_DoesNotThrow_AZ365_AC2()
{
var sut = new RouteValidator();
var sut = MakeValidator();
var request = BuildValidRequest();
Action act = () => sut.Validate(request);
@@ -36,7 +41,7 @@ public class RouteValidatorTests
[Fact]
public void Validate_FewerThanTwoPoints_Throws()
{
var sut = new RouteValidator();
var sut = MakeValidator();
var request = BuildValidRequest();
request.Points = new List<RoutePoint> { new() { Latitude = 47.46, Longitude = 37.64 } };
@@ -48,7 +53,7 @@ public class RouteValidatorTests
[Fact]
public void Validate_RegionSizeOutOfRange_Throws()
{
var sut = new RouteValidator();
var sut = MakeValidator();
var request = BuildValidRequest();
request.RegionSizeMeters = 50;
@@ -61,7 +66,7 @@ public class RouteValidatorTests
[Fact]
public void Validate_BlankName_Throws()
{
var sut = new RouteValidator();
var sut = MakeValidator();
var request = BuildValidRequest();
request.Name = " ";
@@ -73,7 +78,7 @@ public class RouteValidatorTests
[Fact]
public void Validate_GeofencePolygonZeroZero_Throws()
{
var sut = new RouteValidator();
var sut = MakeValidator();
var request = BuildValidRequest();
request.Geofences = new Geofences
{
@@ -92,7 +97,7 @@ public class RouteValidatorTests
[Fact]
public void Validate_GeofenceInvertedLatitudes_Throws()
{
var sut = new RouteValidator();
var sut = MakeValidator();
var request = BuildValidRequest();
request.Geofences = new Geofences
{
@@ -114,7 +119,7 @@ public class RouteValidatorTests
[Fact]
public void Validate_NullPolygonCorner_Throws()
{
var sut = new RouteValidator();
var sut = MakeValidator();
var request = BuildValidRequest();
request.Geofences = new Geofences
{
@@ -133,7 +138,7 @@ public class RouteValidatorTests
[Fact]
public void Validate_OutOfRangeLatitude_Throws()
{
var sut = new RouteValidator();
var sut = MakeValidator();
var request = BuildValidRequest();
request.Geofences = new Geofences
{
@@ -156,7 +161,7 @@ public class RouteValidatorTests
[Fact]
public void Validate_MultipleErrors_AggregatesIntoSingleException_AZ365_AC2()
{
var sut = new RouteValidator();
var sut = MakeValidator();
var request = BuildValidRequest();
request.Name = "";
request.RegionSizeMeters = 50;
@@ -175,7 +180,7 @@ public class RouteValidatorTests
[Fact]
public void Validate_NullRequest_Throws()
{
var sut = new RouteValidator();
var sut = MakeValidator();
Action act = () => sut.Validate(null!);
@@ -24,6 +24,7 @@ public class TileServiceTests
downloader.Object,
tileRepo.Object,
cache ?? new MemoryCache(new MemoryCacheOptions()),
Options.Create(new MapConfig { Service = "GoogleMaps", ApiKey = "" }),
NullLogger<TileService>.Instance);
}
@@ -372,6 +373,28 @@ public class TileServiceTests
tileRepo.Verify(r => r.InsertAsync(It.IsAny<TileEntity>()), Times.Once);
}
[Fact]
public async Task DownloadAndStoreSingleTileAsync_ForwardsCancellationTokenToDownloader_AZ371_AC3()
{
// Arrange
const int zoom = 18;
var downloader = new Mock<ISatelliteDownloader>();
var tileRepo = new Mock<ITileRepository>();
CancellationToken capturedToken = default;
downloader
.Setup(d => d.DownloadSingleTileAsync(It.IsAny<double>(), It.IsAny<double>(), zoom, It.IsAny<CancellationToken>()))
.Callback<double, double, int, CancellationToken>((_, _, _, ct) => capturedToken = ct)
.ReturnsAsync(new DownloadedTileInfoV2(1, 2, zoom, 0, 0, "p.jpg", 100.0));
var service = BuildService(downloader, tileRepo);
using var cts = new CancellationTokenSource();
// Act
await service.DownloadAndStoreSingleTileAsync(0, 0, zoom, cts.Token);
// Assert
capturedToken.Should().Be(cts.Token, "AZ-371 AC-3: caller-supplied CT must reach the downloader");
}
[Fact]
public async Task DownloadAndStoreSingleTileAsync_DownloaderThrows_DoesNotInsert_AZ311_AC2b()
{