[AZ-447] Close testability refactor run; advance to Step 5

- Archive Batch 2 task specs (AZ-448, AZ-449, AZ-453) to
  _docs/02_tasks/done/.
- Write testability_changes_summary.md (refactor Phase 4.5; user-acked
  via autodev existing-code Step 4 gate).
- Write FINAL_report.md closing the 01-testability-refactoring run.
- Advance autodev state pointer to Step 5 (Decompose Tests).

Refactor Phases 5/6/7 are no-ops for testability runs (no tests exist
yet; doc updates are deferred to autodev Step 13). Verification axis
for this run is the static-check matrix recorded in
testability_changes_summary.md § Verification snapshot.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 00:50:33 +03:00
parent ed81034511
commit 729ad1cac8
6 changed files with 112 additions and 6 deletions
@@ -0,0 +1,86 @@
# Externalize OpenWeatherMap API key
**Task**: AZ-448_refactor_owm_api_key
**Name**: Externalize OWM API key to VITE_OWM_API_KEY
**Description**: Move the hardcoded OpenWeatherMap API key from `flightPlanUtils.ts` into a Vite-driven env var so it can be redacted in source and overridden in the test profiles.
**Complexity**: 2 points
**Dependencies**: None
**Component**: `src/features/flights/flightPlanUtils.ts`, `.env.example`, `src/vite-env.d.ts`
**Tracker**: AZ-448
**Epic**: AZ-447
## Problem
`src/features/flights/flightPlanUtils.ts:60` inlines `const apiKey = '335799082893fad97fa36118b131f919'`. The literal:
- Violates restriction O6 (no hardcoded credentials) and AC-O6 (`acceptance_criteria.md`).
- Blocks the security static check NFT-SEC-09 (results_report.md row 63 — quarantined until this fix lands).
- Prevents the e2e profile's `owm-stub` from intercepting OWM calls deterministically.
## Outcome
- Source tree has no literal OWM key string after the change.
- The fast and e2e test profiles can supply test-time keys via env vars without touching source.
- NFT-SEC-09 source-string check (`scripts/run-tests.sh --static-only`) passes.
## Scope
### Included
- Reading `import.meta.env.VITE_OWM_API_KEY` at call time inside `getWeatherData`.
- Adding `.env.example` (workspace-root) documenting the variable.
- Extending the `ImportMetaEnv` type in `src/vite-env.d.ts`.
### Excluded
- Changing the `getWeatherData(lat, lon)` signature or callers.
- Reworking the weather-fetch error handling — only the key sourcing changes.
- Touching the OWM base URL (that is C02 / AZ-449).
## Acceptance Criteria
**AC-1: Key sourcing**
Given the SPA is built with `VITE_OWM_API_KEY=<key>` in the environment,
When `getWeatherData(lat, lon)` is invoked,
Then the resulting HTTP request URL contains `appid=<key>`.
**AC-2: No literal in source**
Given a `grep -r "335799082893fad97fa36118b131f919" src/` after the change,
When the grep runs,
Then the literal key produces no matches.
**AC-3: Missing-env tolerance**
Given the SPA is built with `VITE_OWM_API_KEY` unset,
When `getWeatherData(lat, lon)` is invoked,
Then it returns `null` (matches the existing try/catch fallback) and does NOT throw.
**AC-4: Type declaration**
Given a TypeScript build (`bun run build`),
When the env interface is resolved,
Then `ImportMetaEnv` in `src/vite-env.d.ts` declares `readonly VITE_OWM_API_KEY: string | undefined`.
## Non-Functional Requirements
**Compatibility**
- Build tooling pinned to Vite 6 (S3) — `import.meta.env` is built-in; no new dependency.
**Security**
- `.env.example` MUST use a placeholder (`<your-key>`) — never check in the production key.
## Blackbox Tests
| AC Ref | Initial Data / Conditions | What to Test | Expected Behavior | NFR References |
|--------|--------------------------|--------------|-------------------|----------------|
| AC-1 | `VITE_OWM_API_KEY=test-key-123` set at build; `getWeatherData(0, 0)` called | Outbound fetch URL contains `appid=test-key-123` | Pass | — |
| AC-2 | Repo state after task lands | `grep -r "335799082893fad97fa36118b131f919" src/` | No matches | Security |
| AC-3 | `VITE_OWM_API_KEY` undefined at build; `getWeatherData(0, 0)` called | Function returns null without throwing | Pass | — |
| AC-4 | Repo state after task lands | `src/vite-env.d.ts` extends `ImportMetaEnv` with the field | Declaration present | Compat |
## Constraints
- Adhere to S3 (Vite 6) and the existing project tooling — no new dependency, no `process.env` polyfill.
- Adhere to AC-N4 (no signature library) — env-driven config only.
## Risks & Mitigation
**Risk 1: Build-time leakage**
- *Risk*: Vite inlines `import.meta.env.VITE_*` at build time, so a build run with the production key embedded into `dist/` leaks the key into the bundle.
- *Mitigation*: This is a known Vite behavior, identical to today's hardcoded key. Long-term fix is to proxy OWM through the suite's `loader/` (per E10) — out of scope for this testability task; flagged for Phase B.
@@ -0,0 +1,66 @@
# Externalize OpenWeatherMap base URL
**Task**: AZ-449_refactor_owm_base_url
**Name**: Externalize OWM base URL to VITE_OWM_BASE_URL
**Description**: Move the hardcoded `https://api.openweathermap.org/data/2.5/weather` URL into a Vite env var so the e2e profile's `owm-stub` can intercept OWM calls without code edits.
**Complexity**: 2 points
**Dependencies**: AZ-448_refactor_owm_api_key (same file)
**Component**: `src/features/flights/flightPlanUtils.ts`, `.env.example`, `src/vite-env.d.ts`
**Tracker**: AZ-449
**Epic**: AZ-447
## Problem
The OWM endpoint is hardcoded at `flightPlanUtils.ts:60`. The e2e test profile spins up an `owm-stub:8081` service to keep tests deterministic and avoid rate-limiting, but the stub cannot intercept calls when the base URL is baked into source.
## Outcome
- The base URL is resolvable from `import.meta.env.VITE_OWM_BASE_URL` at call time.
- Default behavior (unset env var) preserves today's production endpoint.
- The e2e profile sets `VITE_OWM_BASE_URL=http://owm-stub:8081/data/2.5` at build time and weather calls hit the stub.
## Scope
### Included
- Reading `VITE_OWM_BASE_URL` (default `https://api.openweathermap.org/data/2.5`) inside `getWeatherData`.
- Adding the entry to `.env.example` and `src/vite-env.d.ts`.
### Excluded
- Any change to error handling, caching, or response shape.
- Touching the API key (AZ-448 covers it).
## Acceptance Criteria
**AC-1: Default endpoint preserved**
Given `VITE_OWM_BASE_URL` is unset at build time,
When `getWeatherData(lat, lon)` is invoked,
Then the fetch target hostname is `api.openweathermap.org`.
**AC-2: Override honored**
Given `VITE_OWM_BASE_URL=http://owm-stub:8081/data/2.5` at build time,
When `getWeatherData(lat, lon)` is invoked,
Then the fetch target URL begins with `http://owm-stub:8081/data/2.5/weather?lat=...`.
**AC-3: Type declaration**
Given a TypeScript build,
When the env interface resolves,
Then `ImportMetaEnv` declares `readonly VITE_OWM_BASE_URL: string | undefined`.
## Blackbox Tests
| AC Ref | Initial Data / Conditions | What to Test | Expected Behavior | NFR References |
|--------|--------------------------|--------------|-------------------|----------------|
| AC-1 | env unset; `getWeatherData(0, 0)` called | Outbound URL host | `api.openweathermap.org` | Compat |
| AC-2 | env set to stub; `getWeatherData(0, 0)` called | Outbound URL prefix | Matches stub URL | E2E determinism |
| AC-3 | Build run | `vite-env.d.ts` declaration present | Pass | Compat |
## Constraints
- Land in one commit with AZ-448 to keep the file's mid-state coherent.
- Must not break the (unset-env) production behavior.
## Risks & Mitigation
**Risk 1: Trailing-slash drift**
- *Risk*: A consumer might set the env to `https://host/` and the path concatenation produces `//weather`.
- *Mitigation*: Normalize by trimming a trailing slash before concatenation; document the rule in `.env.example`.
@@ -0,0 +1,71 @@
# Wrap login redirect in a thin accessor
**Task**: AZ-453_refactor_navigate_to_login
**Name**: `navigateToLoginImpl()` accessor for the failed-refresh redirect
**Description**: Replace the direct `window.location.href = '/login'` write in `request()` with a thin overridable function so tests can verify the redirect call without globally stubbing `window.location`.
**Complexity**: 2 points
**Dependencies**: None
**Component**: `src/api/client.ts`
**Tracker**: AZ-453
**Epic**: AZ-447
## Problem
`src/api/client.ts:34` performs `window.location.href = '/login'` directly after a failed refresh. The DOM API is global; jsdom can intercept it, but a test cannot distinguish "library code redirected" from "user code redirected" without overriding `window.location` globally — a heavy and side-effecty pattern.
## Outcome
- A module-level `navigateToLoginImpl: () => void` is introduced, defaulting to the existing redirect.
- `setNavigateToLogin(fn)` is exported for test override.
- `request()` calls `navigateToLoginImpl()` instead of touching `window.location` directly.
- Production behavior is identical (default impl still navigates to `/login`).
## Scope
### Included
- `navigateToLoginImpl` and `setNavigateToLogin` additions in `src/api/client.ts`.
- One call-site change inside `request()`.
### Excluded
- Any change to the 401 / refresh flow itself.
- Any change to the redirect target string (`/login`).
## Acceptance Criteria
**AC-1: Production default unchanged**
Given the default implementation,
When a 401 returns and `refreshToken()` resolves to `false`,
Then `window.location.href === '/login'` after the failed-refresh branch executes.
**AC-2: Override is honored**
Given a test calls `setNavigateToLogin(spy)`,
When a 401 returns and refresh fails,
Then `spy` is called exactly once and `window.location.href` is NOT mutated by `client.ts`.
**AC-3: API surface preserved**
Given the rest of the module after the change,
When the exports are enumerated,
Then `setToken`, `getToken`, `api`, `setNavigateToLogin` are present and no existing export is renamed or removed.
## Non-Functional Requirements
**Compatibility**
- Tests that already override `window.location` continue to work (the default impl still goes through it).
## Blackbox Tests
| AC Ref | Initial Data / Conditions | What to Test | Expected Behavior | NFR References |
|--------|--------------------------|--------------|-------------------|----------------|
| AC-1 | No override; 401 + failed refresh | `window.location.href` after the call | `/login` | Compat |
| AC-2 | `setNavigateToLogin(spy)` then 401 + failed refresh | Spy call count + `window.location.href` | Spy = 1; href untouched by `client.ts` | Compat |
| AC-3 | Module exports diff | Existing exports | Present | API stability |
## Constraints
- Allowed-change category: "Wrap global singletons in thin accessors that tests can override" (existing-code flow Step 4 allowed list).
## Risks & Mitigation
**Risk 1: Forgetting to reset the override between tests**
- *Risk*: If a test sets the override and doesn't reset it, later tests see the wrong impl.
- *Mitigation*: Document in `_docs/02_document/tests/test-data.md` that helpers must reset accessors in their teardown. Optional follow-up: a `resetNavigateToLogin()` helper — but adding it expands scope; defer unless decompose requires it.