Skip to content

Commit 76b6f17

Browse files
authored
Merge pull request #142 from entur/fix/issue141
fix(#141): clear search filters on route change (import filter leak)
2 parents 1ea13ed + 5976e18 commit 76b6f17

7 files changed

Lines changed: 295 additions & 136 deletions

File tree

.claude/skills/e2e_runner/SKILL.md

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,11 @@ curl -s -o /dev/null -w '%{http_code}\n' -X POST \
7171
```
7272

7373
Interpret:
74-
- **Sobek down** → live mode is impossible; tell the user to start it (`sobek`: `docker compose up`
75-
then `mvn -pl sobek-app spring-boot:run`).
74+
- **Sobek down** → live mode is impossible; bring it up (`sobek`: `docker compose up` then
75+
`mvn -pl sobek-app spring-boot:run`). For the **Autosys import** flow you also need **Shepet** on
76+
:37998. Both are Java-21 apps with non-obvious snags (JDK-26 default breaks the build, Sobek↔Shepet
77+
`sobek-common` API drift, Shepet's half-configured OAuth2 `internal` client) — full recipe + fixes
78+
in `references/bringing-up-backends.md`.
7679
- **401 from Sobek** → live mode needs a real token. You MUST do the login handoff (next section);
7780
`config-no-auth.json` sends no token and will 401 against a secured Sobek.
7881
- **A dev server already on :5000 from another checkout** → Playwright reuses it
@@ -133,15 +136,17 @@ session token. This is the **proven recipe** (verified end-to-end):
133136
⚠️ Playwright's `storageState` persists cookies + **localStorage only, not sessionStorage** — so
134137
you cannot rely on `storageState`. Capture the `oidc.user:*` entry explicitly and re-seed it via
135138
`addInitScript` for the serial run (snippet in harness-internals).
136-
5. **Verify the token before running — make-or-break.** Decode the JWT payload and confirm a
137-
**`roles` claim exists**. A `partner.dev` token with only `scope: openid` and **no `roles`
138-
claim** makes `organisations(onlyUserAuthorized:true)` return `INTERNAL_ERROR` (confirmed live) →
139-
the org dropdown throws → `currentOrganisation` null → the "Loading data…" stall. That's a
140-
token/permissions problem (roles not provisioned) or a Sobek bug — **not** a hathor query bug;
141-
hathor's org query shape is correct. The non-authorized `organisations` path returns all orgs
142-
fine, so use it to sanity-check connectivity. Fix paths: provision the login with role
143-
assignments, guard Sobek's `getRoleAssignmentsForUser`, or have hathor stop sending
144-
`onlyUserAuthorized: true` (dev-only — shows all orgs).
139+
5. **Verify the token before running — make-or-break.** The JWT does **not** need a `roles` claim —
140+
Sobek resolves role assignments **backend-side** (from the token's `organisationID` claim via the
141+
permission store), so a partner.dev token with only `scope: openid` is fine. Verify the *effect*,
142+
not the claim: call `organisations(onlyUserAuthorized:true)` (filter type `OrganisationsFilter`)
143+
and confirm it returns the user's authorized orgs. Empirically (2026-06-22) a no-roles-claim
144+
partner.dev token returned 2 orgs (AtB + Agder) here. If that query **errors or comes back empty**,
145+
the account has **no role assignments provisioned backend-side** (or the permission-store lookup
146+
failed) → the org dropdown can't populate → `currentOrganisation` null → the "Loading data…"
147+
stall. That's a backend role-provisioning/permission issue, **not** a missing-JWT-claim or hathor
148+
query-shape problem (hathor's org query is correct). The non-authorized `organisations` path (no
149+
filter) returns all orgs regardless — use it only to sanity-check connectivity, not authorization.
145150

146151
Never paste the JWT into chat or commit it. Keep it in a gitignored file for the run only; short-lived.
147152

@@ -203,3 +208,6 @@ per-entity reality rather than pretending a uniform create exists.
203208
vehicle-list trio, `waitForVehicleInList`), fixtures, serial config. Read before editing helpers.
204209
- `references/surfaces-and-testids.md` — per-entity create/edit surfaces, the full `data-testid`
205210
inventory, and which row-count assertions are hardcoded. Read before writing or auditing a spec.
211+
- `references/bringing-up-backends.md` — build + run recipes for Sobek (:37999) and Shepet (:37998)
212+
under JDK 21, health checks, and the three startup snags (JDK version, `sobek-common` cross-repo
213+
drift, Shepet's OAuth2 `internal` client). Read before starting the live backends.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Bringing up the live backends (Sobek :37999 + Shepet :37998)
2+
3+
Live mode needs **Sobek** for GraphQL/CRUD. The **Autosys import** flow additionally needs
4+
**Shepet** on :37998 (hathor's import dialog fetches `…/services/autosys?registrationNumber=…`
5+
from Shepet). PostGIS (:37433, Sobek's DB) comes up via `docker compose up` in `sobek/`.
6+
7+
Both are **Java 21** Spring Boot apps. Build under JDK 21, run in the background, then confirm a
8+
`401` on the secured endpoints (= up + serving; the live e2e supplies a real partner JWT via the
9+
login handoff). Recipes below were verified end-to-end on 2026-06-22.
10+
11+
## Recipe
12+
13+
```bash
14+
J=~/.sdkman/candidates/java/21.0.10-oracle # NOT the default 26 — see snag 1
15+
MVN=~/.sdkman/candidates/maven/current/bin/mvn # `mvn` is a shell-fn alias; use the real binary
16+
17+
# 0. PostGIS (if not already on :37433)
18+
( cd ~/entur/sobek && docker compose up -d )
19+
20+
# 1. Build Sobek FIRST — it installs sobek-common into .m2 that Shepet compiles against
21+
( cd ~/entur/sobek && JAVA_HOME=$J $MVN clean install -DskipTests )
22+
( cd ~/entur/shepet && JAVA_HOME=$J $MVN clean install -DskipTests ) # AFTER sobek — see snag 2
23+
24+
# 2. Run Sobek (3 local profiles), background
25+
( cd ~/entur/sobek && JAVA_HOME=$J $MVN -pl sobek-app spring-boot:run \
26+
-Dspring-boot.run.profiles=local,local-blobstore,local-changelog )
27+
28+
# 3. Run Shepet (local profile), background — env mapping + completed internal client, see snag 3
29+
( cd ~/entur/shepet && JAVA_HOME=$J \
30+
AUTOSYS_API_APIKEY="$AUTOSYS_API_API_KEY" \
31+
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_INTERNAL_PROVIDER=internal \
32+
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_INTERNAL_AUTHORIZATION_GRANT_TYPE=client_credentials \
33+
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_INTERNAL_TOKEN_URI=https://partner.dev.entur.org/oauth/token \
34+
$MVN -pl shepet-app spring-boot:run -Dspring-boot.run.profiles=local )
35+
```
36+
37+
Verify (don't trust "Started … in Ns" alone — check the ports answer):
38+
39+
```bash
40+
ss -ltn | grep -E ':37999|:37998' # both listening
41+
curl -so/dev/null -w '%{http_code}\n' -XPOST localhost:37999/services/vehicles/graphql \
42+
-H 'Content-Type: application/json' -d '{"query":"{__typename}"}' # 401 = up, secured
43+
curl -so/dev/null -w '%{http_code}\n' 'localhost:37998/services/autosys?registrationNumber=A-1' # 401 = up, secured
44+
curl -so/dev/null -w '%{http_code}\n' localhost:37998/health/ready # 200 = permitAll health
45+
```
46+
47+
`401` is the **healthy** state — both apps require a Bearer token. `000`/connection-refused = not up.
48+
49+
## Snags (each cost real time the first time)
50+
51+
1. **JDK 26 default breaks the build.** SDKMAN default is `26-oracle`; both projects need 21
52+
(`<java.version>21</java.version>`). Under 26 the superpom's JaCoCo 0.8.13 dies with
53+
`Unsupported class file major version 70`. Force `JAVA_HOME=~/.sdkman/candidates/java/21.0.10-oracle`
54+
for every build/run. Also: `mvn` is a zsh shell-function alias (`mvn-or-mvnw`) that won't resolve
55+
in non-interactive bash — call `~/.sdkman/candidates/maven/current/bin/mvn` directly.
56+
57+
2. **Sobek ↔ Shepet cross-repo API drift.** Both pin `sobek.version=1.0.1-SNAPSHOT` (a *local*
58+
SNAPSHOT), and Sobek `main` evolves independently. On 2026-06-22 Sobek had renamed
59+
`KeyValuesHelper.AddToKeyValues``SetToKeyValues` (commit "Move functions used in Autosys to
60+
sobek-common" + "Support mapping keyvalues both ways"); Shepet's HEAD still called the old name at
61+
3 sites in `shepet-app/.../mapping/MapperService.java`. Building Sobek installs the renamed API
62+
into `.m2`, so Shepet then fails to compile (`cannot find symbol AddToKeyValues`). The new
63+
`SetToKeyValues(DataManagedObjectStructure,…)` is a type-safe, semantics-preserving superset
64+
(upsert; `VehicleType extends DataManagedObjectStructure`), so the local unblock is a mechanical
65+
rename of those call sites. **This is a real drift that wants a Shepet PR — do NOT commit the
66+
rename from a hathor session; treat it as an uncommitted local workaround and flag it.** When the
67+
build breaks on a `sobek-common` symbol, suspect this first; build Sobek *before* Shepet so Shepet
68+
compiles against the fresh jar.
69+
70+
3. **Shepet's OAuth2 `internal` client fails startup with partial config.** The env supplies the
71+
client *credentials* (`SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_INTERNAL_CLIENT_ID` / `…_SECRET`)
72+
but not the provider/token-uri, so Boot's oauth2-**client** autoconfig builds a registration named
73+
`internal` with no provider → `Provider ID must be specified for client registration 'internal'`
74+
at startup. That client is only used by the **baba** role-extractor path
75+
(`@ConditionalOnProperty shepet.security.role.assignment.extractor=baba`); the default is `jwt`,
76+
so it's never used at runtime — it just has to *construct*. Supply the three missing
77+
`…INTERNAL_PROVIDER` / `…INTERNAL_AUTHORIZATION_GRANT_TYPE` / `…PROVIDER_INTERNAL_TOKEN_URI` env
78+
vars (recipe above) and it boots. Also note the Autosys key env var name mismatch: the env holds
79+
`AUTOSYS_API_API_KEY` but the property reads `${AUTOSYS_API_APIKEY}` — remap on launch.
80+
81+
4. **These are session-scoped background processes.** Started via the agent's background Bash, they
82+
die when the session/processes are killed. Re-run steps 2–3 (build artifacts persist in `.m2`) to
83+
bring them back; no rebuild needed unless source changed.
84+
85+
## Secrets
86+
87+
The three Shepet secrets are already exported in the shell env (`AUTOSYS_API_API_KEY`, partner
88+
`SHEPET_OAUTH2_RESOURCESERVER_AUTH0_ENTUR_PARTNER_JWT_{ISSUER_URI,AUDIENCE}`, plus the internal
89+
client id/secret). **Never echo their values into chat, commits, or this file** — reference them by
90+
env-var name only.

.claude/skills/e2e_runner/references/harness-internals.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Three skip flavours exist in the suite:
3333
- `test.skip(E2E_BACKEND === 'true', 'mock-bound assertions')` — mock-only specs
3434
(e.g. `vehicle-type-sidebar.spec.ts` save block, `vehicle-slider-form.spec.ts`).
3535
- `test.skip(() => E2E_BACKEND !== 'true', 'needs local Sobek')` — live-only specs
36-
(`vehicle-type-save-live.spec.ts`, `import-to-detail.spec.ts`).
36+
(`vehicle-type-save-live.spec.ts`).
3737

3838
The conformance goal is to shrink categories 2 and 3 — most specs should be one body that works in
3939
both modes (relative assertions + read-back), reserving skips for genuinely mode-specific checks.
@@ -102,18 +102,25 @@ console.log('CAPTURED ' + user.k); await browser.close();
102102
103103
```bash
104104
TOK=$(jq -r '.v.access_token' playwright/.auth/oidc-user.json)
105-
# decode payload — MUST contain a `roles` claim
105+
# decode payload — just confirm it's a partner.dev token and not expired. It need NOT carry a
106+
# `roles` claim: Sobek resolves role assignments backend-side from the `organisationID` claim.
106107
echo "$TOK" | cut -d. -f2 | sed 's/-/+/g;s/_/\//g' | base64 -d 2>/dev/null | jq 'keys'
107-
# live: this throws INTERNAL_ERROR if the token has no roles claim
108+
# the REAL precondition — this returns the user's AUTHORIZED orgs (empty/error ⇒ no roles
109+
# provisioned backend-side, not a JWT-claim gap). filter type is OrganisationsFilter.
108110
curl -s -X POST http://localhost:37999/services/vehicles/graphql -H "Authorization: Bearer $TOK" \
109111
-H 'Content-Type: application/json' \
110112
-d '{"query":"query($f:OrganisationsFilter){organisations(filter:$f,size:3){totalElements}}","variables":{"f":{"onlyUserAuthorized":true}}}'
111-
# sanity (always works): the non-authorized path returns all orgs (~339)
113+
# sanity (works on any valid token): the non-authorized path returns all orgs (~339)
112114
curl -s -X POST http://localhost:37999/services/vehicles/graphql -H "Authorization: Bearer $TOK" \
113115
-H 'Content-Type: application/json' -d '{"query":"{organisations(size:3){totalElements}}"}'
114116
```
115-
A `partner.dev` token with `scope:openid` and **no `roles`** → `organisations(onlyUserAuthorized:true)`
116-
`INTERNAL_ERROR` (confirmed 2026-06-05). That's the role-claim gap, not a hathor bug.
117+
The JWT needs **no `roles` claim** — Sobek resolves role assignments backend-side from the
118+
`organisationID` claim (permission store). Empirically (2026-06-22) a `scope:openid`, no-roles
119+
partner.dev token returned the user's 2 authorized orgs (AtB + Agder) here. If
120+
`onlyUserAuthorized:true` instead errors/empties, the account lacks backend-provisioned roles (or
121+
the permission lookup failed) — a backend issue, not a missing-claim or hathor-query bug. (An
122+
earlier 2026-06-05 note claimed the no-roles token `INTERNAL_ERROR`s — superseded; verify the
123+
effect, not the claim.)
117124

118125
### 3. Use it in the run
119126

@@ -150,5 +157,5 @@ unfiltered list up to 5×/250ms for a freshly-created id; swallows transient err
150157

151158
`playwright.config.ts` ~L32: `workers: process.env.CI || isNoAuth ? 1 : undefined`. Serial for
152159
no-auth (shared `config.json`) and in CI. Multi-step / mutating specs add
153-
`test.describe.configure({ mode: 'serial' })` (`no-auth.spec.ts` ~L38, `import-to-detail.spec.ts`
154-
~L6) so a step can read what the previous one wrote.
160+
`test.describe.configure({ mode: 'serial' })` (`no-auth.spec.ts` ~L38, `import-state-refresh.spec.ts`
161+
~L114) so a step can read what the previous one wrote.

e2e-tests/no-auth/autosys-multi-import.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ import { IS_LIVE, writeConfig, seedAuth } from './live-auth-helpers';
1717
* - Aggregated import summary parsed from a single REG_NR fixture (1/1/1/1).
1818
* Modes:
1919
* - mock (E2E_SUITE=no-auth): intercepts the VehicleTypes + Autosys GraphQL queries with fixtures; full dialog flow runs offline.
20-
* - live (E2E_BACKEND=true): would fetch the real Autosys data via shepet on :37998 (no query intercepts).
21-
* - skip-live: skipped when IS_LIVE — needs the shepet Autosys backend (:37998), a separate app not part of this Sobek-focused live run.
20+
* - live (E2E_BACKEND=true): fetches real Autosys data via shepet on :37998 (no query intercepts).
21+
* Needs a real plate — set E2E_AUTOSYS_REG_NR (the fixture "A-1" is not a real Autosys plate);
22+
* the 1/1/1/1 summary is plate-dependent under live.
2223
*/
2324
test.describe('Autosys multi-import dialog', () => {
24-
// The live path fetches from the shepet Autosys backend on :37998, which is a
25-
// separate app outside this Sobek-focused live run (not running). Mock mode
26-
// serves the Autosys fixture and still exercises the dialog end-to-end.
27-
test.skip(IS_LIVE, 'requires the shepet Autosys backend (:37998), not part of this live run');
25+
// Runs in BOTH modes now that shepet (:37998) is available locally: mock serves the
26+
// Autosys fixture; live fetches real Autosys via shepet. The shepet-not-running guard
27+
// was a temporary skip and has been removed.
2828

2929
// writeConfig() (not a raw config-no-auth copy) so a live run leaves
3030
// config-with-auth on disk for the next serial spec, even though this one

0 commit comments

Comments
 (0)