Commit Graph

30 Commits

Author SHA1 Message Date
Oleksandr Bezdieniezhnykh f64d0d760a [AZ-487] fix: JWT factory + tests now pass on net8.0
- JwtTokenFactory.Create: negative `lifetime` produced Expires < NotBefore
  which `JwtSecurityToken` rejects at construction time. Shift NotBefore
  behind Expires whenever the requested lifetime is non-positive so the
  expired-token fixture round-trips and lifetime validation can fire.
- JwtTokenFactoryTests: validate against a handler with
  `MapInboundClaims = false` so assertions read the factory's own claim
  names ("sub", "email", "permissions") rather than the .NET-default
  remapped ClaimTypes.* aliases.

These were latent — masked by the CS0104 build break fixed in 753be43.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 23:45:12 +03:00
Oleksandr Bezdieniezhnykh 753be43d11 [AZ-487] fix: resolve CS0104 ambiguity in AuthN tests
`AuthenticationServiceCollectionExtensions` is also a built-in
.NET type under `Microsoft.Extensions.DependencyInjection`. With
both namespaces imported the unqualified references in this test
file failed with CS0104, breaking the entire test project build.
Resolved via a `using` alias so the call sites stay short while
the build stays unambiguous.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 23:41:31 +03:00
Oleksandr Bezdieniezhnykh 96cd3c4495 [AZ-487] JWT validation baseline (HS256, all endpoints)
ci/woodpecker/push/01-test Pipeline failed
ci/woodpecker/push/02-build-push unknown status
Adds Microsoft.AspNetCore.Authentication.JwtBearer 8.0.21 and the
SatelliteProvider.Api.Authentication.AddSatelliteJwt extension that
validates HS256 tokens against a shared JWT_SECRET (>=32 bytes, fail
fast at startup). Every minimal-API endpoint now carries
.RequireAuthorization(); the middleware chain is UseExceptionHandler ->
UseHttpsRedirection -> UseCors -> UseAuthentication -> UseAuthorization
-> endpoints. Swagger UI gets a Bearer security definition so the
Authorize button works.

Test infrastructure: JwtTokenFactory (unit) and JwtTestHelpers
(integration) mint deterministic tokens against the same secret; the
integration test runner attaches a default Bearer token to its shared
HttpClient so existing tests continue to exercise protected endpoints.
JwtIntegrationTests adds AC-1..AC-4 and AC-7 (Swagger advertises
Bearer) end-to-end; AuthenticationServiceCollectionExtensionsTests
covers AC-5 (missing/empty/short secret fail-fast) plus env-var
precedence; JwtTokenFactoryTests covers AC-6 (claims pass through
the JwtSecurityTokenHandler.ValidateToken path JwtBearer uses).

docker-compose and scripts/run-tests.sh now propagate JWT_SECRET to
the api and integration-tests containers, with a >=32-byte guard.
.env.example documents the required keys; .env stays gitignored.

Code review verdict: PASS_WITH_WARNINGS (2 Low findings surfaced
in _docs/03_implementation/reviews/batch_01_cycle2_review.md).

Cross-component coordination: gps-denied-onboard and the mission
planner UI must attach Bearer tokens before this lands in dev.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 23:06:23 +03:00
Oleksandr Bezdieniezhnykh e9d6db077c [AZ-484] Fix multi-source tile reads: drop Dapper enum handler
Two integration-test failures uncovered after the initial commit:

1) GetTilesByRegionAsync outer ORDER BY referenced 'updated_at' but
   the inner DISTINCT ON subquery aliased it to 'UpdatedAt' (Postgres
   folds to 'updatedat'). DISTINCT ON already guarantees one row per
   (latitude, longitude, ...) so the third tiebreak was unreachable;
   removed it.

2) Dapper 2.1.35 silently bypasses SqlMapper.TypeHandler<T> for enum
   types during read deserialization (Dapper issue #259). The
   TileSourceTypeHandler worked for writes but reads fell through to
   Enum.TryParse, which cannot map 'google_maps' to GoogleMaps.

   Pivoted: TileEntity.Source is now a string (the wire value).
   TileSource enum stays as the public producer surface in
   Common.Enums; TileSourceConverter (Common.Enums) provides
   ToWireValue / FromWireValue / IsValidWireValue at the boundary.
   TileSourceTypeHandler deleted; registration removed from
   DapperEnumTypeHandlers.RegisterAll.

   tile-storage.md Inv-5 amended to document the storage choice.
   _docs/LESSONS.md L-001 records the Dapper bypass for future cycles.

Full suite passes (213 unit + integration suite incl. AZ-484
AC-1..AC-5, security SEC-01..SEC-04, AZ-356/362/357).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 06:44:34 +03:00
Oleksandr Bezdieniezhnykh 687d6bdd5b [AZ-484] Multi-source tile storage: source + captured_at
Add per-source tile rows to support multi-provider imagery (Google
Maps + future UAV). Migration 013 (transactional) introduces
source/captured_at columns, backfills existing rows to
(source='google_maps', captured_at=created_at), and replaces the
4-column unique index with a 5-column index that includes source.

TileRepository:
- ColumnList includes source + captured_at
- GetByTileCoordinatesAsync returns most-recent row across sources
  (ORDER BY captured_at DESC, updated_at DESC, id DESC)
- GetTilesByRegionAsync uses DISTINCT ON to pick the most-recent
  tile per cell, restoring caller-facing row order
- Insert/Update upsert on the new 5-column conflict key

TileSource enum lives in Common.Enums. Snake_case wire format
(google_maps, uav) is enforced by a focused TileSourceTypeHandler
because the generic ToLowerInvariant pattern would emit
"googlemaps", violating contract v1.0.0.

TileService stamps Source=GoogleMaps + CapturedAt=UtcNow on every
new tile. Tile-storage contract is now frozen at v1.0.0.

AC coverage 7/7. New unit + integration tests cover all ACs;
existing 200 unit + 5 smoke tests preserved.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 06:21:59 +03:00
Oleksandr Bezdieniezhnykh 9a53bff92e [AZ-375] [AZ-377] HashSet tile lookup + consolidate Earth constants
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
Batch 24 of 03-code-quality-refactoring run; closes the run.

AZ-375 (C22): GoogleMapsDownloaderV2.DownloadTilesGridAsync now
builds a HashSet<(int X, int Y, int Z)> once from existingTiles
and tests Contains((x, y, zoomLevel)) per cell. Removes the per-cell
FirstOrDefault tolerance scan and the unused _processingConfig
.LatLonTolerance reference at this site.

AZ-377 (C24): promote Earth + tile-pixel constants to a single
home. GeoUtils now exposes EarthRadiusMeters, EarthEquatorial
CircumferenceMeters, MetersPerDegreeLatitude as public const.
MapConfig adds DefaultTileSizePixels (const) wired as the
TileSizePixels property default. TileRepository and Google
MapsDownloaderV2 read those constants instead of duplicating
the literals 6378137, 40075016.686, 111000.0, and 256.

Tests: +6 new (DownloaderRefactorTests, extended GeoUtils
RefactorTests). 200/200 unit tests pass.

Cumulative K=3 review (batches 22-24): PASS_WITH_WARNINGS,
4 Low findings only — see
_docs/03_implementation/reviews/cumulative_review_22-24.md.

Tooling fix: scripts/run-tests.sh --unit-only path now restores
before testing (was failing on SixLabors resolution in clean
container). Stripped stray BOM from MapConfig.cs to satisfy the
.editorconfig charset gate.

Updates _dependencies_table.md to reflect all 27 03-code-quality-
refactoring tasks done; updates _autodev_state.md to refactor
phase 5 (test-sync).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 05:14:51 +03:00
Oleksandr Bezdieniezhnykh 6099d1c86b [AZ-376] [AZ-378] [AZ-379] [AZ-380] Repo cleanup: dead code, logger discipline, ColumnList consts
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
Batch 23 of refactor 03-code-quality-refactoring (4 tasks, 5 SP):

- AZ-376 (C23): Delete unused FindExistingTileAsync from
  ITileRepository / TileRepository. No callers; method also took the
  obsolete `version` arg removed by C06/AZ-357.
- AZ-378 (C25): Repository _logger discipline.
  TileRepository.GetTilesByRegionAsync now emits LogWarning when the
  query exceeds SlowQueryThresholdMs (500 ms). RegionRepository and
  RouteRepository drop the unused ILogger<TRepo> field, parameter, and
  using; Program.cs DI registrations updated.
- AZ-379 (C26): Extract `private const string ColumnList` per repo
  (TileRepository, RegionRepository, RouteRepository); SELECTs use
  $@"SELECT {ColumnList} FROM ..." (C# 10+ const interpolation).
  INSERT/UPDATE/DELETE unchanged; route_points single-site SELECT left
  inline.
- AZ-380 (C27): Delete dead alias GeoUtils.CalculatePolygonDiagonalDistance.

Tests: +9 new (RepositoryRefactorTests x8, GeoUtilsRefactorTests x1)
covering each AC via reflection / file-content assertions; pattern
mirrors ToolingConfigurationTests (b22) and AcceptanceCriteriaRT2Tests
(b19). Unit suite 181 -> 190, all green. dotnet format clean.

Code review: PASS_WITH_WARNINGS (3 Low findings, all informational or
out-of-scope for this batch). See
_docs/03_implementation/reviews/batch_23_review.md.

Cumulative review counter 2/3; next K=3 review fires after batch 24.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 04:57:49 +03:00
Oleksandr Bezdieniezhnykh 68359350fc [AZ-372] Add .editorconfig, Directory.Build.props, format/coverage wiring
Wires the C19 tooling baseline so dotnet format and Coverlet gate the
test script and a small NetAnalyzers ruleset (CA1001, CA1051, CA1816,
CA2227) at warning severity is visible from the next build.

- .editorconfig (new, root=true): whitespace rules, per-extension
  indent sizes, C# style preferences as suggestions, initial CA rules.
- Directory.Build.props (new): EnableNETAnalyzers=true,
  AnalysisLevel=latest, AnalysisMode=None so only rules explicitly
  enabled in .editorconfig fire; EnforceCodeStyleInBuild=false to keep
  build clean from style.
- scripts/run-tests.sh: Step 0 runs dotnet format whitespace
  --verify-no-changes via Docker SDK; unit/integration test calls now
  collect XPlat Code Coverage into TestResults/. New --skip-format
  escape hatch.
- .gitignore: TestResults/, coverage.cobertura.xml, *.coverage.
- SatelliteProvider.Tests/ToolingConfigurationTests.cs (new, 6 tests):
  runtime assertions that the config files, script wiring, and
  coverlet.collector reference are all in place; mirrors the
  AcceptanceCriteriaRT2Tests pattern.

Whitespace cleanup that the new format gate uncovers is staged for the
next commit (per AZ-372 spec: "commit cleanup as a separate batch").

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 04:42:45 +03:00
Oleksandr Bezdieniezhnykh 89b4bfd245 [AZ-374] Refactor C21: named GoogleMapsTiles HttpClient in DI
- Register IHttpClientFactory named client "GoogleMapsTiles" inside
  AddTileDownloader() with User-Agent and 100s timeout (preserves
  HttpClient's implicit default).
- Resolve the same named client from all three CreateClient() call
  sites in GoogleMapsDownloaderV2 (session token, single-tile,
  batch-tile retry lambda) and drop the duplicated per-call
  UserAgent.ParseAdd setup.
- Expose USER_AGENT, the client name, and the timeout as internal
  consts on GoogleMapsDownloaderV2 so the extension and the
  downloader share one source of truth.
- Add AC test that builds the DI container, resolves the named
  client, and asserts both the User-Agent header and the timeout.
- Archive AZ-374 task file: todo/ -> done/.

175 unit + 5 smoke pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 04:11:57 +03:00
Oleksandr Bezdieniezhnykh 7c37636fdf [AZ-373] Refactor C20: drop MapsVersion from new writes (option a)
- Stop writing "downloaded_YYYY-MM-DD" into tiles.maps_version: new rows
  bind @MapsVersion to NULL via TileService.BuildTileEntity.
- Retain the tiles.maps_version column (coderule.mdc forbids unprompted
  column drops); pre-existing rows keep their values for forensics.
- Remove MapsVersion property from DownloadTileResponse (API wire shape)
  and TileMetadata (internal DTO); OpenAPI schema regenerates from the
  DTO via Swashbuckle.
- Add 3 AC tests in TileServiceTests covering the captured-entity write
  (AC-1) and the DTO/wire-shape removal (AC-2).
- Update integration-test local DTO + console output; refresh docs in
  common_dtos.md, services_tile_service.md, data_model.md.
- Archive AZ-373 task file: todo/ -> done/.

174 unit + 5 smoke pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 04:05:40 +03:00
Oleksandr Bezdieniezhnykh 23ab05766d [AZ-370] Refactor C17: status / point-type enums + AC RT2 update
Replaces bare strings with two enums in Common/Enums/:
  RegionStatus { Queued, Processing, Completed, Failed }
  RoutePointType { Start, End, Action, Intermediate }

Adds a Dapper EnumStringTypeHandler<T> (DataAccess/TypeHandlers/)
that round-trips enums to/from lowercase strings, registered once
at startup via DapperEnumTypeHandlers.RegisterAll(). DataAccess now
references Common (project ref) so entities can carry the enum types.

Sites converted: RegionService (5), RouteProcessingService (3),
RoutePointGraphBuilder (4), entity Status/PointType columns. Log
message and summary file format preserved via .ToLowerInvariant().

API JSON contract preserved by adding JsonStringEnumConverter with
JsonNamingPolicy.CamelCase to the http JSON options — single-word
enum members serialize to the same lowercase strings as before.

DTO renamed: Common.DTO.RegionStatus -> RegionStatusResponse to
free the RegionStatus name for the new enum (forced by the task's
explicit enum name); the renamed DTO has no public-API impact at
the JSON wire level. Stale doc references updated.

AC RT2 in _docs/00_problem/acceptance_criteria.md now lists all 4
point types (start/end/action/intermediate).

Tests: 171 / 171 unit + 5 / 5 smoke green (was 141 + 5; +30 new tests
covering type handler round-trip, set/parse, unknown-value rejection,
idempotent registration, and the AC RT2 doc check).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 03:55:22 +03:00
Oleksandr Bezdieniezhnykh 1dcd089d39 [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>
2026-05-11 03:30:07 +03:00
Oleksandr Bezdieniezhnykh 6f23120c49 [AZ-364] [AZ-360] Refactor C11+C08: decompose RouteProcessingService
Extracts RouteRegionMatcher, RouteCsvWriter, RouteSummaryWriter,
RouteImageRenderer, TilesZipBuilder, RegionFileCleaner from the
~750-LOC RouteProcessingService god-class. Moves TileInfo to its
own file as a sealed record. Replaces IServiceProvider scope-
locator with a direct IRegionService injection (folds AZ-360 / C08).
Updates DI registration and tests.

Tests: 133 / 133 unit + 5 / 5 smoke green; integration suite exit 0.
Pixel-equivalent stitched route image and byte-equivalent CSV /
summary / ZIP outputs verified through the smoke run.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 03:12:49 +03:00
Oleksandr Bezdieniezhnykh 10d31b4c1c [AZ-367] Refactor C14: extract shared TileGridStitcher
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 02:55:25 +03:00
Oleksandr Bezdieniezhnykh f7ad7aa5ab [AZ-365] Refactor C12: decompose RouteService.CreateRouteAsync
Extract RouteValidator (aggregating validator), RoutePointGraphBuilder
(point interpolation + sequence numbering), GeofenceGridCalculator
(NW/SE region centers), and RouteResponseMapper (entity -> DTO; also
used by GetRouteAsync, eliminating duplicate DTO assembly).

CreateRouteAsync shrinks 184 -> 52 LOC of orchestration. RouteService.cs
shrinks 295 -> 138 LOC overall. Validation aggregates all failures into
a single ArgumentException (AC-2); single-violation messages preserved
verbatim so existing RouteServiceTests pass unchanged. 28 new unit
tests for the four helpers (112/112 unit tests, smoke green).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 02:08:21 +03:00
Oleksandr Bezdieniezhnykh 89260d0ec4 [AZ-368] Refactor C15: extract shared TileCsvWriter
Both RegionService.GenerateCsvFileAsync and
RouteProcessingService.GenerateRouteCsvAsync wrote the same CSV
shape: header "latitude,longitude,file_path", same
OrderByDescending(Latitude).ThenBy(Longitude) ordering, same F6
numeric format. Two near-identical writers with no shared abstraction.

Extracted TileCsvWriter (instance class, no DI dependencies) plus a
TileCsvRow record bridging the per-pipeline DTOs (TileMetadata vs
TileInfo) to a single contract. The header constant, ordering rule,
and StreamWriter lifecycle now live in one place.

Both call sites collapse to a one-line projection plus a delegated
WriteAsync call. Region method becomes static (no longer references
instance state). Route method preserves its existing logger line.

Coverage:
- 7 new unit tests including a byte-for-byte equivalence test that
  writes the same input via both the new TileCsvWriter and the
  inlined-original code path side by side and asserts file bytes
  are identical.
- Integration smoke + full suite green; route + region CSV outputs
  unchanged across all existing scenarios (verified by extended-route
  CSV verification step in the integration suite).
- 84/84 unit tests pass (was 77).

Side improvement: writer now respects CancellationToken mid-loop.
The pre-refactor inline code did not. Strict improvement; consistent
with every other async API in the codebase.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 01:07:51 +03:00
Oleksandr Bezdieniezhnykh 330bccd724 [AZ-366] Refactor C13: consolidate Haversine + tile-coord parsing
RouteProcessingService.CalculateDistance(double, double, double, double)
re-implemented Haversine using EARTH_RADIUS=6371000 alongside the
canonical GeoUtils.CalculateDistance(GeoPoint, GeoPoint) which uses
EARTH_RADIUS=6378137. Two implementations of the same formula for the
same problem.

Separately, ExtractTileCoordinatesFromFilename in RouteProcessingService
parsed the tile_{z}_{x}_{y}_{ts}.jpg filename pattern that's *generated*
by StorageConfig.GetTileFilePath in another assembly — the writer and
parser were coupled by string convention only and a format change in
one would silently break the other.

Both fixes:

(a) Deleted the duplicate Haversine in RouteProcessingService. The
single call site (region-matching nearest-neighbor OrderBy) now uses
GeoUtils.CalculateDistance with GeoPoint instances. The constant
difference is monotonic-equivalent for OrderBy purposes — same region
is picked.

(b) Added static StorageConfig.TryExtractTileCoordinates(string, out
int, out int): bool — pure parser, co-located with GetTileFilePath so
the inverse-pair invariant is structural, not by-convention.
RouteProcessingService.ExtractTileCoordinatesFromFilename becomes a
thin wrapper that delegates to the helper and emits the existing
warning log on malformed input — the AZ-352 tests for warning behavior
all still pass.

Verification:
- 77/77 unit tests green (was 71 → +6 new StorageConfigTests including
  a writer/parser round-trip test for AC-2).
- Smoke + full integration suite green.
- AC-1 grep verification: Math.Sin/EARTH_RADIUS patterns are now
  confined to GeoUtils.cs only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:56:46 +03:00
Oleksandr Bezdieniezhnykh 2393bff1f2 [AZ-362] Refactor C09: idempotent POST contract for caller-supplied GUIDs
Both POST /api/satellite/request and POST /api/satellite/route accept
a caller-supplied id (Guid). Before this change, a retried POST with
the same id would either crash with a unique-key violation (regions)
or quietly create a divergent row (routes), neither of which matched
the documented intent of caller-supplied GUIDs.

RegionService.RequestRegionAsync and RouteService.CreateRouteAsync
now check for an existing row by id at the top of the method. If one
is found, the existing resource is returned with HTTP 200 and the
side effects (insert + enqueue + point regeneration + geofence-region
queueing) are all skipped. The Information-level log line on the
idempotent path makes retries observable.

OpenAPI Description metadata documents the contract on both endpoints
so client integrators see it in Swagger.

Coverage:
- 2 new unit tests (one per service) assert that on duplicate id no
  insert / enqueue / point-generation / region-queueing call is made.
- 2 new integration tests (IdempotentPostTests.cs) exercise the
  contract end-to-end via HTTP, asserting both calls return 200 and
  CreatedAt matches within 1ms (PostgreSQL truncates TIMESTAMP to
  microseconds while .NET DateTime keeps 100ns ticks; a real
  re-insertion would shift CreatedAt by milliseconds at minimum).

Note: the check-first pattern leaves a TOCTOU window for concurrent
retries. The repository unique key still surfaces the race as a
PostgresException which AZ-353 maps to a clean error. Acceptable for
realistic sequential-retry patterns; recorded in batch report as a
non-blocking observation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:45:51 +03:00
Oleksandr Bezdieniezhnykh 581dff206e [AZ-357] Refactor C06: drop tile Version concept; cumulative review batches 7-9
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
AZ-357 — eliminate year-based tile cache expiry (LF-1):
- Migration 012: drop 5-col unique index, dedupe by (lat,lon,zoom,
  size) keeping max(updated_at), add new 4-col unique index, make
  version column nullable + drop default. Column itself preserved
  per coderule (column drops require explicit confirmation; tracked
  in AZ-373 / C20).
- TileEntity.Version, TileMetadata.Version, DownloadTileResponse.
  Version: int -> int? (HTTP shape preserved; field still in JSON).
- TileService.DownloadAndStoreTilesAsync: drop currentVersion year
  computation and the .Where(t => t.Version == currentVersion)
  cache filter. BuildTileEntity: drop year arg; write Version=null.
- TileRepository: ON CONFLICT now 4-col; lookup queries
  ORDER BY updated_at DESC instead of version DESC.
- Tests: replace inverted BT02b with positive AZ357_AC1
  (prior-year cached tile is reused). Add BuildTileEntity_
  DoesNotPopulateVersion_AZ357 to enforce the no-write contract.
- 69 unit + 5 smoke + 3 stub-contract integration tests pass.

Cumulative code review (batches 7-9, 7 tasks): VERDICT=PASS.
Report at _docs/03_implementation/reviews/batch_09_review.md.
Zero Critical/High/Medium/Low findings. Architecture baseline
remains clean.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:20:47 +03:00
Oleksandr Bezdieniezhnykh 5a28f67d33 [AZ-359] Refactor C07: collapse RegionService catch ladder via RegionFailureClassifier
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
Replace 9 nearly-identical catch blocks in
RegionService.ProcessRegionAsync with a single catch (Exception ex)
that delegates to RegionFailureClassifier.Classify, returning a
typed (category, errorMessage) pair. Preserves all original error
messages stored in region summary files; failure-path call into
HandleProcessingFailureAsync is unchanged.

Net source delta: -38 lines in RegionService, +71 lines in new
RegionFailureClassifier (pure static), +10 unit tests covering
each category, precedence, status-code propagation, null guard.

Tests: 68 unit (was 58) + 5 smoke + 3 stub-contract integration
tests pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:07:31 +03:00
Oleksandr Bezdieniezhnykh 1d89cd9997 [AZ-353][AZ-354][AZ-356] Refactor 03 batch 2: harden API surface
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
AZ-353: Centralize 500 handling via GlobalExceptionHandler /
AddProblemDetails / UseExceptionHandler. Sanitized ProblemDetails
body carries a generic title, RFC9110 type link, and the request's
TraceIdentifier as correlationId; the leaky exception message stays
server-side in the ERR log entry. Strip per-endpoint
try/catch (Exception) wrappers and the unused ILogger<Program>
parameters they served. Preserve the typed ArgumentException catch
in CreateRoute (AC-3). The handler maps BadHttpRequestException
back to its framework-supplied StatusCode so model-binding /
malformed-body failures stay 4xx instead of being promoted to 500.

AZ-354: Extract CorsConfigurationValidator (pure static helpers)
and wire it into Program.cs. Production with empty
CorsConfig:AllowedOrigins and no CorsConfig:AllowAnyOrigin opt-in
now throws InvalidOperationException at host startup. Development
keeps the permissive default but logs a warning post-build. Adds
the explicit CorsConfig:AllowAnyOrigin escape hatch.

AZ-356: GetSatelliteTilesByMgrs and UploadImage now return
Results.Problem(StatusCode 501) with ProblemDetails. Added
.ProducesProblem(501) so swagger.json documents the
not-implemented status.

Tests: SatelliteProvider.Tests now references SatelliteProvider.Api
(downward, idiomatic) so unit tests can reach the new helpers.
+9 CorsConfigurationValidator unit tests, +3
GlobalExceptionHandler unit tests, +3 StubAndErrorContractTests
integration tests (added to smoke + full suites).

58/58 unit + 5/5 smoke + 3/3 stub-contract pass.
Code review verdict: PASS.
Batch report: _docs/03_implementation/batch_08_report.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 23:52:52 +03:00
Oleksandr Bezdieniezhnykh de4d4fa760 [AZ-351][AZ-352][AZ-363] Refactor 03 batch 1: critical defensive fixes
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
AZ-351: Resolve ILogger<DatabaseMigrator> directly from DI in
Program.cs instead of casting ILogger<Program> (which always
returned null). Migrator now logs through Serilog at startup.

AZ-352: Drop empty catch in
RouteProcessingService.ExtractTileCoordinatesFromFilename. Convert
the method from private static to internal instance so it can use
the existing _logger (per coderule: side-effecting code must not be
static). Add typed null-guard via ArgumentNullException.ThrowIfNull
so unexpected exceptions propagate. Adds InternalsVisibleTo on the
RouteManagement csproj for SatelliteProvider.Tests, plus 4 unit
tests in RouteProcessingServiceTests.cs covering AC-1 (valid /
malformed / non-numeric) and AC-2 (null path propagation).

AZ-363: Delete _totalEnqueued / _totalDequeued fields and the two
non-atomic ++ writes in RegionRequestQueue. Fields were write-only
dead code and a thread-safety hazard.

Tests: 44/44 unit + 5/5 smoke (scripts/run-tests.sh --smoke).
Code review verdict: PASS, 0 findings, 0 auto-fix attempts.
Batch report: _docs/03_implementation/batch_07_report.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 23:34:17 +03:00
Oleksandr Bezdieniezhnykh 8b0ddae075 [AZ-312] [AZ-313] [AZ-314] Split Services into per-component csprojs
Phase B of architecture coupling refactor (epic AZ-309). Replaces
the monolithic SatelliteProvider.Services with three per-component
csprojs to add a compiler-enforced module boundary (resolves F4):

- SatelliteProvider.Services.TileDownloader
- SatelliteProvider.Services.RegionProcessing
- SatelliteProvider.Services.RouteManagement

DI registrations relocated into per-component AddTileDownloader /
AddRegionProcessing / AddRouteManagement extension methods called
from Program.cs. RateLimitException moved to Common/Exceptions/ to
keep the three new csprojs as siblings (no Region->TileDownloader
ProjectReference). Dockerfiles and consumer csprojs (Api, Tests)
rewired to the new project paths. No DI lifetime or hosted-service
order changes.

Build: 0 warn, 0 err. Unit tests: 40/40. Smoke integration: green.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 07:15:44 +03:00
Oleksandr Bezdieniezhnykh 12b582deac [AZ-310] [AZ-311] Route tile endpoints through ITileService
Move cache+DB+download logic for /tiles/{z}/{x}/{y} and
/api/satellite/tiles/latlon out of Program.cs into TileService.
Endpoints now inject only ITileService + ILogger. Service owns
IMemoryCache (1h absolute / 30min sliding preserved). Added
TileBytes DTO; ITileService gains GetOrDownloadTileAsync and
DownloadAndStoreSingleTileAsync. 5 new unit tests cover cache
hit, repo hit, downloader fallback, and AZ-311 happy + error.

Build clean (0/0), unit suite 40/40. Resolves architecture
baseline F3 in code (docs handled by AZ-315).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 06:06:11 +03:00
Oleksandr Bezdieniezhnykh 7822841587 [AZ-289] [AZ-290] Batch 3 tests: integration ZIP cap, perf, security, queue
AZ-289 — RL-01 50MB ZIP cap added to RunRouteWithTilesZipTest;
existing integration tests already cover BT-08/BT-09 + AC-1/AC-2.

AZ-290:
- scripts/run-performance-tests.sh extended with PT-01/03/04/05
- SatelliteProvider.IntegrationTests/SecurityTests.cs (SEC-01..SEC-04),
  wired into Program.cs
- SatelliteProvider.Tests/RegionRequestQueueTests.cs covering RS-04 /
  RL-02 queue capacity behavior

Notes:
- RS-04 spec wording ("rejects overflow") drifts from the channel's
  BoundedChannelFullMode.Wait back-pressure semantics. Tests assert
  the actual behavior; spec to be reconciled in Step 12 (Test-Spec
  Sync). Tracked as Low/Spec-Gap in batch_03_review.md.
- Unit tests: 35/35 passed (Docker .NET 8 SDK).
- Integration test project builds clean (0 warnings, 0 errors).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 05:10:30 +03:00
Oleksandr Bezdieniezhnykh dea0b8b4c0 [AZ-286] [AZ-287] [AZ-288] Service-level unit tests (mocked deps)
Batch 2: 27 new tests, 31/31 total passing.

AZ-286 TileService (9 tests):
- BT-01 download stores tiles via repository
- BT-02 cache reuse passes existing tiles to downloader
- BT-02b stale-version cached tiles are NOT treated as still-valid
- BT-N02 GoogleMapsDownloaderV2 rejects zoom levels not in {15..19}
- GetTileAsync known/unknown id

AZ-287 RegionService (6 tests):
- AC-1 RequestRegionAsync inserts entity and queues request
- AC-2/AC-3 ProcessRegionAsync transitions queued→processing→completed
  and writes CSV + summary files
- AC-4 stitch path is set when stitchTiles=true
- ProcessRegionAsync handles RateLimitException by writing failed
  summary with the error message preserved
- GetRegionStatusAsync mapping
- Missing region id is logged and short-circuits without DB writes

AZ-288 RouteService (12 tests):
- AC-1 every consecutive pair is ≤200m apart on 2-point route
- AC-2 first/last user points keep their roles; middles are interpolated
- BT-10 10pt route → 1 start, 1 end, 8 action, plus intermediates
- BT-12 20pt route → 1 start, 1 end, 18 action
- AC-3 TotalDistanceMeters == Σ Haversine(point[i-1], point[i])
- BT-N03 < 2 points throws ArgumentException
- regionSize <100 / >10000 throws ArgumentException
- BT-N04 (0,0) corner throws
- BT-N05 inverted NW.lat <= SE.lat throws
- BT-11 valid geofence creates region requests + links them with
  isGeofence=true
- BT-07 GetRouteAsync returns route + points
- GetRouteAsync unknown id returns null

Code review: PASS_WITH_WARNINGS — 3 Low Spec-Gap findings:
  1. BT-N01 (invalid coords) deferred to AZ-289 integration tests
     (TileService doesn't validate coordinates — API-layer concern)
  2. Point-type label drift between blackbox-tests.md ("original")
     and code ("start"/"action"/"end")
  3. Region status drift: spec "pending" vs code "queued" — initial
     state on insert

Verification: docker dotnet test — 31/31 pass in 0.83s.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 05:02:00 +03:00
Oleksandr Bezdieniezhnykh 853b0a63df [AZ-285] Test infrastructure: scaffold unit test project + fixtures
- Add SatelliteProvider.DataAccess project reference to test csproj
  (enables mocking ITileRepository / IRegionRepository / IRouteRepository)
- Replace DummyTest placeholder with InfrastructureTests covering:
  * All mockable interfaces (ISatelliteDownloader, repos, queue, services)
    can be mocked via Moq
  * TileService can be constructed with mocked dependencies
  * Test coordinate fixtures load with expected values
- Add Fixtures/TestCoordinates.cs with REG-01..REG-03 + ROUTE-01/04/06
  shared test data
- Archive AZ-285 to _docs/02_tasks/done/
- Batch 1 review report: PASS_WITH_WARNINGS (Low/Spec-Gap deferred AC-2,
  Low/Maintainability pre-existing FluentAssertions 8.x license note)

Verification: docker dotnet test run — 4/4 tests pass in 2.35s.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 04:55:28 +03:00
Anton Martynenko a148df1697 fix warnings 2025-11-19 17:40:12 +01:00
Anton Martynenko 72eea71507 added stitching 2025-10-29 11:21:59 +01:00
Oleksandr Bezdieniezhnykh a7a645c7ab make structure
add tests
2025-10-26 09:15:06 +02:00