[AZ-1126] Migrate capturedAt to DateTimeOffset
ci/woodpecker/push/01-test Pipeline failed
ci/woodpecker/push/02-build-push unknown status

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-06-26 13:34:35 +03:00
parent b055450e40
commit 50d4a76be3
19 changed files with 242 additions and 43 deletions
@@ -0,0 +1,45 @@
using System.Text.Json;
using FluentAssertions;
using SatelliteProvider.Common.DTO;
using SatelliteProvider.Common.Json;
namespace SatelliteProvider.Tests.Json;
public class UtcOffsetRequiredDateTimeOffsetConverterTests
{
private static readonly JsonSerializerOptions Options = new()
{
PropertyNameCaseInsensitive = true,
Converters = { new UtcOffsetRequiredDateTimeOffsetConverter() },
};
[Theory]
[InlineData("\"2026-06-26T12:00:00Z\"")]
[InlineData("\"2026-06-26T12:00:00+00:00\"")]
[InlineData("\"2026-06-26T12:00:00.1234567Z\"")]
public void Deserialize_ExplicitUtcOffset_Parses(string capturedAtJson)
{
// Arrange
var json = $$"""{"latitude":50.1,"longitude":36.1,"tileZoom":18,"tileSizeMeters":200,"capturedAt":{{capturedAtJson}}}""";
// Act
var metadata = JsonSerializer.Deserialize<UavTileMetadata>(json, Options);
// Assert
metadata.Should().NotBeNull();
metadata!.CapturedAt.Offset.Should().Be(TimeSpan.Zero);
}
[Fact]
public void Deserialize_OffsetLessIsoString_ThrowsJsonException()
{
// Arrange
const string json = """{"latitude":50.1,"longitude":36.1,"tileZoom":18,"tileSizeMeters":200,"capturedAt":"2026-06-26T12:00:00"}""";
// Act
var act = () => JsonSerializer.Deserialize<UavTileMetadata>(json, Options);
// Assert
act.Should().Throw<JsonException>().WithMessage("*explicit UTC offset*");
}
}
@@ -114,7 +114,7 @@ public class UavTileQualityGateTests
var now = new DateTime(2026, 5, 11, 12, 0, 0, DateTimeKind.Utc);
var gate = BuildGate(timeProvider: new FixedTimeProvider(now));
var bytes = UavTileImageFactory.CreateRandomJpeg();
var metadata = ValidMetadata() with { CapturedAt = now.AddHours(1) };
var metadata = ValidMetadata() with { CapturedAt = new DateTimeOffset(now.AddHours(1), TimeSpan.Zero) };
// Act
var result = gate.Validate(bytes, JpegContentType, metadata);
@@ -131,7 +131,7 @@ public class UavTileQualityGateTests
var now = new DateTime(2026, 5, 11, 12, 0, 0, DateTimeKind.Utc);
var gate = BuildGate(timeProvider: new FixedTimeProvider(now));
var bytes = UavTileImageFactory.CreateRandomJpeg();
var metadata = ValidMetadata() with { CapturedAt = now.AddDays(-8) };
var metadata = ValidMetadata() with { CapturedAt = new DateTimeOffset(now.AddDays(-8), TimeSpan.Zero) };
// Act
var result = gate.Validate(bytes, JpegContentType, metadata);
@@ -148,7 +148,7 @@ public class UavTileQualityGateTests
var now = new DateTime(2026, 5, 11, 12, 0, 0, DateTimeKind.Utc);
var gate = BuildGate(timeProvider: new FixedTimeProvider(now));
var bytes = UavTileImageFactory.CreateRandomJpeg();
var metadata = ValidMetadata() with { CapturedAt = now.AddSeconds(20) };
var metadata = ValidMetadata() with { CapturedAt = new DateTimeOffset(now.AddSeconds(20), TimeSpan.Zero) };
// Act
var result = gate.Validate(bytes, JpegContentType, metadata);
@@ -216,7 +216,7 @@ public class UavTileQualityGateTests
Longitude = 37.647063,
TileZoom = 18,
TileSizeMeters = 200.0,
CapturedAt = new DateTime(2026, 5, 11, 11, 30, 0, DateTimeKind.Utc),
CapturedAt = new DateTimeOffset(2026, 5, 11, 11, 30, 0, TimeSpan.Zero),
};
private sealed class FixedTimeProvider : TimeProvider
@@ -275,7 +275,7 @@ public class UavTileUploadHandlerTests : IDisposable
Longitude = 37.647063,
TileZoom = 18,
TileSizeMeters = 200.0,
CapturedAt = new DateTime(2026, 5, 11, 11, 30, 0, DateTimeKind.Utc),
CapturedAt = new DateTimeOffset(2026, 5, 11, 11, 30, 0, TimeSpan.Zero),
};
private sealed class FixedTimeProvider : TimeProvider
@@ -11,7 +11,7 @@ namespace SatelliteProvider.Tests.Validators;
public class UavTileBatchMetadataPayloadValidatorTests
{
private readonly UavTileBatchMetadataPayloadValidator _validator;
private readonly DateTime _now;
private readonly DateTimeOffset _now;
public UavTileBatchMetadataPayloadValidatorTests()
{
@@ -22,7 +22,7 @@ public class UavTileBatchMetadataPayloadValidatorTests
MaxAgeDays = 7,
CapturedAtFutureSkewSeconds = 30,
});
_now = new DateTime(2026, 5, 22, 12, 0, 0, DateTimeKind.Utc);
_now = new DateTimeOffset(2026, 5, 22, 12, 0, 0, TimeSpan.Zero);
_validator = new UavTileBatchMetadataPayloadValidator(config, new FixedTimeProvider(_now));
}
@@ -96,8 +96,8 @@ public class UavTileBatchMetadataPayloadValidatorTests
private sealed class FixedTimeProvider : TimeProvider
{
private readonly DateTime _utcNow;
public FixedTimeProvider(DateTime utcNow) => _utcNow = utcNow;
public override DateTimeOffset GetUtcNow() => new(_utcNow, TimeSpan.Zero);
private readonly DateTimeOffset _utcNow;
public FixedTimeProvider(DateTimeOffset utcNow) => _utcNow = utcNow;
public override DateTimeOffset GetUtcNow() => _utcNow;
}
}
@@ -13,7 +13,7 @@ namespace SatelliteProvider.Tests.Validators;
public class UavTileMetadataValidatorTests
{
private readonly UavTileMetadataValidator _validator;
private readonly DateTime _now;
private readonly DateTimeOffset _now;
public UavTileMetadataValidatorTests()
{
@@ -23,7 +23,7 @@ public class UavTileMetadataValidatorTests
MaxAgeDays = 7,
CapturedAtFutureSkewSeconds = 30,
});
_now = new DateTime(2026, 5, 22, 12, 0, 0, DateTimeKind.Utc);
_now = new DateTimeOffset(2026, 5, 22, 12, 0, 0, TimeSpan.Zero);
_validator = new UavTileMetadataValidator(config, new FixedTimeProvider(_now));
}
@@ -32,12 +32,12 @@ public class UavTileMetadataValidatorTests
// consumer appears, promote to SatelliteProvider.TestSupport.
private sealed class FixedTimeProvider : TimeProvider
{
private readonly DateTime _utcNow;
public FixedTimeProvider(DateTime utcNow) => _utcNow = utcNow;
public override DateTimeOffset GetUtcNow() => new(_utcNow, TimeSpan.Zero);
private readonly DateTimeOffset _utcNow;
public FixedTimeProvider(DateTimeOffset utcNow) => _utcNow = utcNow;
public override DateTimeOffset GetUtcNow() => _utcNow;
}
private static UavTileMetadata ValidMetadata(DateTime capturedAt) => new()
private static UavTileMetadata ValidMetadata(DateTimeOffset capturedAt) => new()
{
Latitude = 50.10,
Longitude = 36.10,