mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-23 14:31:15 +00:00
275ee1b554
- 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.
194 lines
8.2 KiB
C#
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;
|
|
}
|
|
}
|