# Consolidate JWT test-mint helpers

**Task**: AZ-491_consolidate_jwt_test_helpers
**Name**: Consolidate JWT test-mint helpers
**Description**: Eliminate the duplicate JWT-minting logic that exists in both `SatelliteProvider.Tests/TestUtilities/JwtTokenFactory.cs` and `SatelliteProvider.IntegrationTests/JwtTestHelpers.cs` by moving the canonical implementation to a single shared location consumed by both test projects.
**Complexity**: 3 points
**Dependencies**: None (no in-repo task dependency; logically follows AZ-487 which introduced both copies)
**Component**: Test infrastructure (`SatelliteProvider.Tests` + `SatelliteProvider.IntegrationTests`)
**Tracker**: AZ-491
**Epic**: none (cycle-3 test-infrastructure hardening)

## Problem

During cycle 2 (AZ-487), JWT token-minting logic was written twice — once in `SatelliteProvider.Tests/TestUtilities/JwtTokenFactory.cs` (unit-side) and once in `SatelliteProvider.IntegrationTests/JwtTestHelpers.cs::MintValidToken` (integration-side). The two implementations are near-identical: same signing algorithm, same claim shape, same negative-lifetime workaround. Both contained the same `Expires < NotBefore` bug and both required independent fixes in separate commits (`f64d0d7` for unit-side, `11b7074` for integration-side).

The cycle-2 retrospective identified this as Improvement Action 1: "Consolidate JWT token-mint helpers into one shared utility" (`_docs/06_metrics/retro_2026-05-11_cycle2.md` § 7). LESSONS.md ring buffer carries the lesson under `[testing]`. Until this is resolved, every future JWT-side change pays a dual-fix tax, and the two implementations will continue to drift on shape, claim handling, or signing parameters — a real security-relevance risk for the surface that mints test credentials.

## Outcome

- A single canonical JWT-minting implementation exists, consumable by both `SatelliteProvider.Tests` and `SatelliteProvider.IntegrationTests`.
- `SatelliteProvider.IntegrationTests/JwtTestHelpers.cs::MintValidToken` and `MintExpiredToken` and `TamperSignature` are deleted; existing call sites (`Program.cs`, `JwtIntegrationTests`, `UavUploadTests`) consume the consolidated factory.
- Runner-side concerns (`JwtTestHelpers.cs::ResolveSecretOrThrow`, `AttachDefaultAuthorization`) remain in `SatelliteProvider.IntegrationTests` — only the mint logic moves.
- All 253 unit tests + the full integration test suite pass end-to-end with the consolidated factory.
- A code-review checklist item is added to `.cursor/skills/code-review/SKILL.md` Phase 6 so that future duplicate-helper situations are flagged at review time, not at retrospective time.

## Scope

### Included

- Move (or re-locate) the canonical `JwtTokenFactory` class so both test projects can reference it. Implementer chooses between two viable shapes (see § Risks & Mitigation Risk 1):
  - **Option A**: new test-support class library `SatelliteProvider.TestSupport` (no test framework), referenced by both `SatelliteProvider.Tests` and `SatelliteProvider.IntegrationTests`. Larger upfront cost; cleaner long-term layering.
  - **Option B**: `SatelliteProvider.IntegrationTests` gains a `ProjectReference` to `SatelliteProvider.Tests`, consuming the existing `TestUtilities/JwtTokenFactory.cs` directly. Smaller cost; integration tests acquire a dependency on unit tests (slightly unusual but precedented in some .NET test layouts).
- Delete `SatelliteProvider.IntegrationTests/JwtTestHelpers.cs::MintValidToken`, `MintExpiredToken`, and `TamperSignature`. Their integration-test call sites switch to the canonical factory.
- Update all call sites: `SatelliteProvider.IntegrationTests/Program.cs`, `JwtIntegrationTests.cs`, `UavUploadTests.cs`.
- Add a code-review checklist row in `.cursor/skills/code-review/SKILL.md` Phase 6 (Cross-Task Consistency): "If two test projects contain near-identical helper logic with no shared source-of-truth, flag as a Medium finding for consolidation."
- Update `_docs/02_document/modules/tests_unit.md` and `tests_integration.md` to point at the new consolidated location (one-line each).

### Excluded

- Any change to production code in `SatelliteProvider.Api`, `SatelliteProvider.Common`, or service projects.
- Any change to `UavTileImageFactory` (also a candidate for consolidation but out of scope here — keep this PBI surgical).
- Any change to `ResolveSecretOrThrow` / `AttachDefaultAuthorization` — these are runner-side concerns and stay in `SatelliteProvider.IntegrationTests`.
- Changes to the JWT algorithm, claim shape, or `TokenValidationParameters` — this PBI is a pure refactor, not a behavior change.

## Acceptance Criteria

**AC-1: Single source of truth**
Given the repository after this PBI lands
When a developer searches for `JwtSecurityToken(` constructor calls in test code
Then exactly one location implements JWT minting; no duplicate implementation exists in another test project.

**AC-2: Existing integration tests pass unchanged in behavior**
Given the full integration test suite (`scripts/run-tests.sh --full`)
When the suite executes against the consolidated factory
Then all integration test scenarios produce the same pass/fail outcomes as they did prior to this PBI.

**AC-3: Existing unit tests pass unchanged in behavior**
Given the full unit test suite (`dotnet test SatelliteProvider.Tests`)
When the suite executes against the consolidated factory
Then all 253 unit test scenarios produce the same pass/fail outcomes as they did prior to this PBI.

**AC-4: Runner-side concerns preserved**
Given the integration test runner
When the runner starts up
Then `ResolveSecretOrThrow` still reads `JWT_SECRET` from the environment with the same minimum-length validation, AND `AttachDefaultAuthorization` still sets the `Authorization: Bearer …` header on the shared `HttpClient`.

**AC-5: Cycle-2 fixes remain effective**
Given a fixture that requests an expired-lifetime token (negative `TimeSpan`)
When the consolidated factory mints the token
Then `Expires` is greater than `NotBefore` (the cycle-2 `IDX12401` fix is preserved), AND the token fails `ValidateLifetime` downstream (the lifetime check still fires).

**AC-6: Code-review rule lands**
Given a future PR that adds a near-identical helper to two test projects
When the code-review skill runs Phase 6 (Cross-Task Consistency)
Then the checklist item from this PBI fires and the finding is recorded as a Medium-severity duplicate-helper item.

## Non-Functional Requirements

**Compatibility**
- Must compile against `net8.0` target framework (same as all existing projects).
- Must not introduce new external NuGet dependencies — the consolidated factory uses only `System.IdentityModel.Tokens.Jwt` and `Microsoft.IdentityModel.Tokens`, already referenced by both test projects.

**Reliability**
- The negative-lifetime workaround documented in the existing factory (cycle-2 `f64d0d7` / `11b7074` commits) must be preserved exactly — `Expires > NotBefore` is invariant.

## Unit Tests

| AC Ref | What to Test | Required Outcome |
|--------|-------------|-----------------|
| AC-1   | The consolidated factory is the sole `JwtSecurityToken` constructor call site in test code | A grep of `new JwtSecurityToken(` across all `**/*.cs` under both test projects returns exactly one match (in the consolidated factory) |
| AC-3   | All existing `JwtTokenFactoryTests` cases | Pass against the consolidated factory at its new location |
| AC-5   | Expired-token fixture path | The factory's `Create` (or new equivalent) with `lifetime: TimeSpan.FromMinutes(-5)` produces `Expires > NotBefore` AND the resulting token fails `ValidateLifetime` |

## Blackbox Tests

| AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References |
|--------|------------------------|-------------|-------------------|----------------|
| AC-2   | Integration test runner started with a valid `JWT_SECRET` | `JwtIntegrationTests` + `UavUploadTests` run end-to-end through the consolidated factory | Same pass/fail outcomes as the pre-PBI baseline | Compatibility |
| AC-4   | Integration test runner started with `JWT_SECRET` set to a 32-byte value | `ResolveSecretOrThrow` returns the secret; `AttachDefaultAuthorization` sets the `Authorization` header on the shared `HttpClient` | Runner-side behavior unchanged | — |

## Constraints

- Stay within the existing project layout — the chosen Option A or B must not require renaming or restructuring `SatelliteProvider.Tests` / `SatelliteProvider.IntegrationTests`.
- The consolidated factory must remain a `public static class` (current shape) — do not introduce DI, abstractions, or factories around it. This is test infrastructure, not production code.
- Comment in the consolidated factory must retain the explanation of the `Expires <= NotBefore` workaround so future readers don't "fix" the apparent oddity.

## Risks & Mitigation

**Risk 1: Option A vs Option B trade-off**
- *Risk*: Option A (new `SatelliteProvider.TestSupport` library) is the cleaner long-term shape but costs ~1 extra csproj, a new ProjectReference graph entry, and a new module in `module-layout.md`. Option B (integration tests reference unit tests) is cheaper but creates a slightly unusual dependency direction.
- *Mitigation*: The implementer chooses based on whether other shared test utilities (e.g., `UavTileImageFactory`) are likely to be consolidated in the next 1–2 cycles. If yes, Option A pays for itself quickly. If no, Option B is fine. Document the choice in the batch report so future agents understand the precedent.

**Risk 2: Drift in the comment block that documents the `Expires <= NotBefore` workaround**
- *Risk*: Moving the file may invite a "cleanup" of the comment, removing the rationale and re-introducing the `IDX12401` bug on a future edit.
- *Mitigation*: AC-5 + the unit test that exercises the expired-token path catch any regression. The comment block survives the move verbatim.

**Risk 3: Removal of duplicate file exposes hidden coupling**
- *Risk*: Some other test file may currently `using SatelliteProvider.IntegrationTests` with intent to consume `JwtTestHelpers` symbols beyond just `MintValidToken` (e.g., `DefaultSubject` constant).
- *Mitigation*: Before deletion, grep for all references to `JwtTestHelpers.` symbols in the codebase and replace each one with the consolidated equivalent. The `DefaultSubject` constant moves with the mint logic.
