Google answers a revoked or expired refresh token with invalid_grant.
Every save then retried the dead token, and reconnecting never cleared
the old error, so it kept showing on Backup Settings afterwards.
- The Google Drive and Dropbox OAuth callbacks clear lastError when a
provider is reconnected.
- An invalid_grant, or a missing refresh token, now pauses the
integration with a readable "reconnect" message instead of retrying
it on every catch update. Other failures still retry as before.
- A banner on every page and an alert on the Pokédex page point to
Backup Settings, which shows a "Reconnect needed" badge. The Pokédex
page re-checks backup status after each export, because saving a
catch record also exports on the server and may pause a provider
first.
- Offline sync status and the "Save all artwork" link move from every
page to a new /offline-guide page, linked from the user menu and the
home and welcome pages. Only the offline read-only banner stays
sitewide.
- Unit tests cover every export path and the backup status store. BDD
covers revocation and reconnecting for both providers, and the
offline guide. The mock provider can now reject token refreshes, and
mock control calls fail loudly if a stale mock is reused. Coverage
thresholds are raised to the new baseline.
The endpoint overrides are read through `$env/dynamic/private`, so they are
evaluated per request in production, not baked in at build time. That made a
single injected environment variable enough to redirect the authorization-code
and refresh-token POSTs - which carry the OAuth client secret and the user's
refresh token - to an arbitrary host, and to redirect the user's authorize hop
to an arbitrary URL.
Overrides are now ignored unless ALLOW_PROVIDER_ENDPOINT_OVERRIDES is exactly
"true" and the value is a loopback URL. `npm run test:bdd` sets the flag;
nothing else should. resolveProviderEndpoints is pure so the refusals are unit
tested, including near-miss hosts such as http://127.0.0.1.example.
Also drops the unused `pokedex` parameter from buildCsv rather than silencing it
with `void`, and the dead hasGigantamaxed field from its fallback record.
Splits testing into five layers so a failure points at the responsible one:
- tests/unit isolated utility, repository and service tests
- tests/data validates the tracked Pokémon, game, region and dex files
- tests/integration schema, views, constraints, RLS and repositories
- tests/bdd executable Gherkin for user-visible behaviour
- tests/build service worker and manifest artifacts per build variant
Replaces the two Playwright specs in client-test/ and the two Vitest files in
test/. Adds a GitHub Actions workflow running the layers as separate jobs, a
mock OAuth provider server so the Drive and Dropbox scenarios never touch real
accounts, and a wrapper that reads the local Supabase keys from
`supabase status` rather than hard-coding them.
Extracts the pure formatting helpers out of PokedexExportService so they can be
unit tested, and makes the provider endpoints configurable so the mock server
can stand in for Google and Dropbox.