Files
satellite-provider/SatelliteProvider.Tests/RouteTileDeliveryOrchestratorTests.cs
T
Oleksandr Bezdieniezhnykh 275ee1b554
ci/woodpecker/push/01-test Pipeline failed
ci/woodpecker/push/02-build-push unknown status
Add TileProvision configuration and gRPC service for tile delivery
- Introduced new TileProvision settings in appsettings.json, including MaxTilesPerBatch and ProgressEmitIntervalSeconds.
- Configured TileProvisionConfig in Program.cs to bind the new settings.
- Added gRPC service for RouteTileDelivery in Program.cs to handle tile delivery requests.
- Updated SatelliteProvider.Api.csproj to include Grpc.AspNetCore package and added protobuf file for tile provision.
- Enhanced AuthenticationServiceCollectionExtensions to handle JWT token extraction from the Authorization header.
- Registered additional services in RouteManagementServiceCollectionExtensions for tile processing.

These changes enhance the API's capability to manage tile provisioning and delivery efficiently.
2026-06-23 13:18:59 +03:00

194 lines
8.2 KiB
C#

using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using SatelliteProvider.Common.Configs;
using SatelliteProvider.Common.Enums;
using SatelliteProvider.Common.Interfaces;
using SatelliteProvider.Common.Utils;
using SatelliteProvider.DataAccess.Models;
using SatelliteProvider.DataAccess.Repositories;
using SatelliteProvider.Services.RouteManagement;
using SatelliteProvider.Services.RouteManagement.TileProvision;
namespace SatelliteProvider.Tests;
public class RouteTileDeliveryOrchestratorTests
{
[Fact]
public async Task DeliverAsync_AllTilesSkippedByClient_EmitsManifestAndCompleteWithZeroDelivered()
{
var expander = new RouteTileExpander(
new RoutePointGraphBuilder(Options.Create(new ProcessingConfig { MaxRoutePointSpacingMeters = 200 })),
new GeofenceGridCalculator());
var tileRepo = new Mock<ITileRepository>();
tileRepo
.Setup(r => r.GetTilesByLocationHashesAsync(It.IsAny<IReadOnlyList<Guid>>()))
.ReturnsAsync(new Dictionary<Guid, TileEntity>());
var tileService = new Mock<ITileService>();
var orchestrator = new RouteTileDeliveryOrchestrator(
expander,
tileRepo.Object,
tileService.Object,
Options.Create(new MapConfig { TileSizePixels = 256, AllowedZoomLevels = [18] }),
Options.Create(new ProcessingConfig { MaxConcurrentDownloads = 2 }),
Options.Create(new TileProvisionConfig { MaxTilesPerBatch = 100 }),
NullLogger<RouteTileDeliveryOrchestrator>.Instance);
var waypoints = new List<(double Lat, double Lon)> { (47.0, 37.0), (47.001, 37.001) };
var candidates = expander.Expand(waypoints, 400, 18, [], false);
candidates.Should().NotBeEmpty();
var clientTiles = candidates.Select(c =>
{
var center = GeoUtils.TileToWorldPos(c.X, c.Y, c.Z);
var resolution = TileResolutionHelper.ResolutionMetersPerPixel(c.Z, center.Lat, 256);
return new ClientTileSnapshot(c.Z, c.X, c.Y, resolution, DateTime.UtcNow.AddHours(1), null);
}).ToList();
var sink = new RecordingSink();
var job = new RouteTileDeliveryJob(
Guid.NewGuid(),
waypoints,
400,
18,
[],
false,
clientTiles);
await orchestrator.DeliverAsync(job, sink, CancellationToken.None);
sink.Manifest.Should().NotBeNull();
sink.Manifest!.Value.Total.Should().Be((uint)candidates.Count);
sink.Manifest.Value.Skipped.Should().Be((uint)candidates.Count);
sink.Manifest.Value.ToDeliver.Should().Be(0);
sink.Complete.Should().NotBeNull();
sink.Complete!.Value.Delivered.Should().Be(0);
sink.Complete.Value.SkippedClient.Should().Be((uint)candidates.Count);
sink.Batches.Should().BeEmpty();
tileService.Verify(
s => s.DownloadAndStoreSingleTileAsync(It.IsAny<double>(), It.IsAny<double>(), It.IsAny<int>(), It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task DeliverAsync_CachedTileOnDisk_EmitsBatchWithoutDownload()
{
var tilesDir = Path.Combine(Path.GetTempPath(), "sp-grpc-test-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tilesDir);
try
{
var expander = new RouteTileExpander(
new RoutePointGraphBuilder(Options.Create(new ProcessingConfig { MaxRoutePointSpacingMeters = 200 })),
new GeofenceGridCalculator());
var waypoints = new List<(double Lat, double Lon)> { (47.0, 37.0), (47.001, 37.001) };
var candidates = expander.Expand(waypoints, 400, 18, [], false);
var candidate = candidates[0];
var jpeg = new byte[] { 0xFF, 0xD8, 0xFF, 0xD9 };
var tilePath = Path.Combine(tilesDir, "tile.jpg");
await File.WriteAllBytesAsync(tilePath, jpeg);
var hash = System.Security.Cryptography.SHA256.HashData(jpeg);
var center = GeoUtils.TileToWorldPos(candidate.X, candidate.Y, candidate.Z);
var locationHash = Uuidv5.LocationHashForTile(candidate.Z, candidate.X, candidate.Y);
var entity = new TileEntity
{
Id = Guid.NewGuid(),
TileZoom = candidate.Z,
TileX = candidate.X,
TileY = candidate.Y,
Latitude = center.Lat,
Longitude = center.Lon,
TileSizeMeters = 100,
TileSizePixels = 256,
ImageType = "jpg",
FilePath = tilePath,
Source = TileSourceConverter.GoogleMapsWireValue,
CapturedAt = DateTime.UtcNow,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
LocationHash = locationHash,
ContentSha256 = hash,
};
var tileRepo = new Mock<ITileRepository>();
tileRepo
.Setup(r => r.GetTilesByLocationHashesAsync(It.IsAny<IReadOnlyList<Guid>>()))
.ReturnsAsync((IReadOnlyList<Guid> hashes) =>
{
var dict = new Dictionary<Guid, TileEntity>();
if (hashes.Contains(locationHash))
{
dict[locationHash] = entity;
}
return dict;
});
var tileService = new Mock<ITileService>();
var orchestrator = new RouteTileDeliveryOrchestrator(
expander,
tileRepo.Object,
tileService.Object,
Options.Create(new MapConfig { TileSizePixels = 256, AllowedZoomLevels = [18] }),
Options.Create(new ProcessingConfig { MaxConcurrentDownloads = 2 }),
Options.Create(new TileProvisionConfig { MaxTilesPerBatch = 100 }),
NullLogger<RouteTileDeliveryOrchestrator>.Instance);
var sink = new RecordingSink();
var job = new RouteTileDeliveryJob(Guid.NewGuid(), waypoints, 400, 18, [], false, []);
await orchestrator.DeliverAsync(job, sink, CancellationToken.None);
sink.Manifest.Should().NotBeNull();
sink.Manifest!.Value.ToDeliver.Should().BeGreaterThan(0);
sink.Batches.Should().NotBeEmpty();
sink.Batches.SelectMany(b => b).Should().Contain(t => t.Candidate.X == candidate.X && t.Candidate.Y == candidate.Y);
sink.Complete.Should().NotBeNull();
sink.Complete!.Value.Delivered.Should().BeGreaterThan(0u);
}
finally
{
if (Directory.Exists(tilesDir))
{
Directory.Delete(tilesDir, recursive: true);
}
}
}
private sealed class RecordingSink : IRouteTileDeliverySink
{
public (uint Total, uint Skipped, uint ToDeliver)? Manifest { get; private set; }
public List<IReadOnlyList<PreparedTileDelivery>> Batches { get; } = new();
public (uint Delivered, uint SkippedClient, uint SkippedServer)? Complete { get; private set; }
public ValueTask WriteManifestAsync(uint totalCandidates, uint skippedByClient, uint toDeliver, CancellationToken cancellationToken)
{
Manifest = (totalCandidates, skippedByClient, toDeliver);
return ValueTask.CompletedTask;
}
public ValueTask WriteBatchAsync(uint batchSeq, IReadOnlyList<PreparedTileDelivery> tiles, CancellationToken cancellationToken)
{
Batches.Add(tiles);
return ValueTask.CompletedTask;
}
public ValueTask WriteProgressAsync(uint delivered, uint total, uint downloading, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
public ValueTask WriteCompleteAsync(uint delivered, uint skippedClient, uint skippedServerFilter, CancellationToken cancellationToken)
{
Complete = (delivered, skippedClient, skippedServerFilter);
return ValueTask.CompletedTask;
}
public ValueTask WriteErrorAsync(string code, string message, bool retryable, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
}
}