[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,106 @@
# Migrate UAV upload capturedAt to DateTimeOffset
**Task**: AZ-1126_captured_at_datetimeoffset
**Name**: Migrate UavTileMetadata.capturedAt to DateTimeOffset (F-AZ810-2)
**Description**: Close security carry-over F-AZ810-2 by typing `UavTileMetadata.CapturedAt` as `DateTimeOffset` instead of `DateTime`, eliminating ambiguous `DateTimeKind.Unspecified` handling on the UAV upload metadata input path.
**Complexity**: 2 points
**Dependencies**: AZ-810 (HARD — metadata validation layer); AZ-488 (original upload endpoint)
**Component**: SatelliteProvider.Common (UavTileMetadata) + SatelliteProvider.Api (validators) + SatelliteProvider.Services.TileDownloader (quality gate + upload handler)
**Tracker**: AZ-1126
**Epic**: AZ-795
## Problem
Security finding F-AZ810-2 (cycle 8, open through cycle 12) flags that `UavTileMetadata.CapturedAt` is typed `DateTime` rather than `DateTimeOffset`. `DateTime` accepts `DateTimeKind.Unspecified` values that deserialize from offset-less ISO-8601 strings, forcing manual `Kind` normalization in the upload handler and quality gate. This is a time-handling correctness gap that can skew freshness-window checks in non-UTC dev environments.
## Outcome
- `capturedAt` on the UAV upload metadata input is unambiguously UTC-aware at the type level
- Offset-less or `Unspecified` timestamps are rejected before persistence
- F-AZ810-2 is marked resolved in the next security audit cycle
- Wire compatibility preserved for clients sending ISO-8601 UTC with explicit offset (`Z` or `+00:00`)
## Scope
### Included
- Change `UavTileMetadata.CapturedAt` from `DateTime` to `DateTimeOffset`
- Update FluentValidation rules, quality gate, and upload handler to compare via UTC without manual `Kind` branching
- Reject offset-less / ambiguous `capturedAt` values with HTTP 400
- Unit tests for the new rejection path and existing freshness-window rules
- Integration test proving offset-less `capturedAt` is rejected
- Patch `_docs/02_document/contracts/api/uav-tile-upload.md` 1.2.0 → 1.2.1 (change log + clarify offset requirement)
### Excluded
- `TileInventoryEntry.CapturedAt` response field (remains `DateTime?` — DB read path)
- `TileEntity.CapturedAt` persistence layer type
- Changes to gRPC tile delivery or other non-UAV-upload surfaces
- MAJOR contract version bump (wire JSON shape unchanged for compliant clients)
## Acceptance Criteria
**AC-1: Type migration**
Given the UAV upload metadata DTO
When deserialized from JSON
Then `CapturedAt` is `DateTimeOffset` and freshness comparisons use UTC without manual `DateTimeKind` normalization
**AC-2: Reject ambiguous timestamps**
Given a UAV upload batch with `capturedAt` lacking an explicit UTC offset (offset-less ISO string)
When POST `/api/satellite/upload`
Then HTTP 400 with a validation or deserialization error referencing `capturedAt`
**AC-3: Backward-compatible UTC clients**
Given a UAV upload batch with `capturedAt` as ISO-8601 UTC (`...Z` or `...+00:00`)
When POST `/api/satellite/upload` with otherwise valid payload
Then HTTP 200 (or the same non-timestamp rejection as before timestamp validation)
**AC-4: Contract patch**
Given the implementation is complete
When `uav-tile-upload.md` is reviewed
Then version is 1.2.1 with a change-log entry documenting the offset requirement and F-AZ810-2 closure
## Non-Functional Requirements
**Compatibility**
- Compliant clients already sending `Z`-suffixed timestamps must not break
**Security**
- Closes F-AZ810-2 (Low / informational time-handling finding)
## Unit Tests
| AC Ref | What to Test | Required Outcome |
|--------|-------------|-----------------|
| AC-1 | `UavTileMetadataValidator` freshness window with `DateTimeOffset` | Future/too-old rules still fire |
| AC-2 | Deserializer or validator with offset-less `capturedAt` | Rejected |
| AC-1 | `UavTileQualityGate` captured-at rules | Still accept/reject correctly |
## Blackbox Tests
| AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References |
|--------|------------------------|-------------|-------------------|----------------|
| AC-2 | Valid JPEG + metadata with `capturedAt: "2026-06-26T12:00:00"` (no offset) | POST `/api/satellite/upload` | HTTP 400 mentioning `capturedAt` | — |
| AC-3 | Valid JPEG + metadata with `capturedAt` as `DateTime.UtcNow.ToString("o")` | POST `/api/satellite/upload` | HTTP 200 (happy path unchanged) | Compatibility |
## Constraints
- Must remain a child of epic AZ-795 strict-validation theme
- No breaking wire change for offset-aware clients
## Risks & Mitigation
**Risk 1: Client sends offset-less timestamps**
- *Risk*: Legitimate clients using `"2026-06-26T12:00:00"` without `Z` start failing
- *Mitigation*: Contract patch documents requirement; rejection is intentional per F-AZ810-2
## Contract
This task patches the producer contract at `_docs/02_document/contracts/api/uav-tile-upload.md` (1.2.0 → 1.2.1).
Consumers: `gps-denied-onboard`, mission planner UI, any UAV upload client.
### Document Dependencies
- `_docs/02_document/contracts/api/uav-tile-upload.md` v1.2.0 (patch to 1.2.1)
- `_docs/02_document/contracts/api/error-shape.md` v1.0.0 (unchanged)