Testing the dummy login picker
A step-by-step checklist for verifying that every role in the dummy login picker (/admin/login) does what it should. Run this whenever you touch the picker, the admin session cookie, the capability gating, or the role-profile model in src/lib/dummy-login-roles.ts.
The "dummy" in dummy login refers to user identity (no IdP backs it yet — that arrives with Okta/Authentik). The minted tokens are real HS256 JWTs signed with JWT_SECRET; PostgREST and RLS enforce capability gates exactly as in production.
Preconditions
Before running the spec:
.envhas at minimumPOSTGREST_URL,POSTGREST_ANON_JWT,JWT_SECRET. See Getting started.- PostgREST at
POSTGREST_URLis reachable and verifies HS256 against the sameJWT_SECRET. Ifnpm run smoke:adminfails with500 PGRST300 "Server lacks JWT secret"or401 role "railway_web_anon" does not exist, see INVESTIGATE postgrest admin connection — those are UIS-side regressions, not a Railway-side bug. - Next.js dev server is running on port 3010:
npm run dev. - Browser DevTools open with the Application → Cookies panel visible for
localhost:3010. The cookie to watch israilway_admin_session.
What the picker shows
Visit http://localhost:3010/admin/login (no query string). Expected:
| Row | Kind | Clickable | Notes |
|---|---|---|---|
railway_owner | PG role | No (greyed out) | Disabled reason visible: "Owns DDL / SECURITY DEFINER functions — not a session role." |
anon | Profile | Yes | Description: "Offentlig PostgREST-økt. Sletter admin-cookien." No capability badges. |
| Full admin | Profile | Yes | Cap badge: admin |
| Registrations admin | Profile | Yes | Cap badges: registrations:read, registrations:write |
| Content editor | Profile | Yes | Cap badges: content:read, content:write |
| App-log viewer | Profile | Yes | Cap badge: app_log:read |
| Users admin | Profile | Yes | Cap badges: users:read, users:write |
authenticator | PG role | No (greyed out) | Disabled reason visible: "PostgREST runtime connection role — never appears in a user session." |
There should also be a small "Manuell innlogging (lim inn staff‑JWT)" link below the picker — clicking it appends ?manual=1 and shows the JWT-paste form for power users.
Per-role test
Run each block below in order. After each test, log out before the next test:
- From inside
/admin, click the "Logg ut" button (bottom of the sidebar). You should land back on/admin/loginwith the picker. - Equivalently, manually clear the
railway_admin_sessioncookie in DevTools and reload.
1. anon — clears the admin cookie
- From
/admin/login, clickanon. - Expected: page redirects to
/(the public registration form). Norailway_admin_sessioncookie present in DevTools. - Visit
http://localhost:3010/admindirectly. Expected: redirected back to/admin/login(the dashboard layout insrc/app/admin/(dashboard)/layout.tsxenforces this).
2. Full admin — admin capability (all sidebar sections)
- From
/admin/login, click Full admin. - Expected: redirected to
/admin.railway_admin_sessioncookie set. - Verify the sidebar shows all five groups:
- Oversikt
- Registreringer (Liste, CSV eksport)
- Utskrift (Manuskript, Papirskjema)
- Aktivitet og skjema (Aktiviteter, Tilleggsaktiviteter, Skjematekster, Skjemadata)
- Drift (App‑logg)
- Konto (Mine tilganger)
- Click Registreringer → Liste. Page loads with the table of seeded registrations.
- Decode the cookie at jwt.io (paste the cookie value, ignore the "invalid signature" warning — it's HS256, jwt.io can't verify without the secret). Payload should contain:
role: "authenticated"capabilities: ["admin"]aud: "railway"exp≈ 7 days in the future
3. Registrations admin — narrow capability set
- Logout. Click Registrations admin.
- Expected sidebar: Oversikt, Registreringer, Konto. (No Utskrift, no Aktivitet og skjema, no Drift.)
- Visit
/admin/registrations. Page loads. - Manually visit
/admin/text-content(a content surface, not linked from the sidebar for this role). Expected: page loads but data fetch shows an error or empty state — PostgREST/RLS rejects the read because the JWT lackscontent:read. - Cookie payload (decoded):
capabilities: ["registrations:read","registrations:write"].
4. Content editor — content caps only
- Logout. Click Content editor.
- Expected sidebar: Oversikt, Utskrift, Aktivitet og skjema, Konto. (No Registreringer, no Drift.)
- Visit
/admin/activities. Page loads with seeded activities. - Manually visit
/admin/registrations. Expected: page loads but the data fetch shows an error or empty state (RLS rejects). - Cookie payload:
capabilities: ["content:read","content:write"].
5. App-log viewer — app_log:read only
- Logout. Click App-log viewer.
- Expected sidebar: Oversikt, Drift, Konto.
- Visit
/admin/app-log. Page loads (likely empty —auth.app_loghad 0 rows at last UIS DB snapshot). - Manually visit
/admin/registrationsand/admin/activities. Both should show empty/error from RLS. - Cookie payload:
capabilities: ["app_log:read"].
6. Users admin — users caps (sidebar currently has no users-cap items)
- Logout. Click Users admin.
- Expected sidebar: Oversikt, Konto only. The sidebar has no entries gated on
users:*today — Users admin shows the same nav as an authenticated user with no caps would. This is correct; the cap is exercised at the PostgREST/RLS layer, not in the sidebar. - Cookie payload:
capabilities: ["users:read","users:write"]. - (Optional — once a users-admin page exists, extend this block to verify it loads here and fails for the other roles.)
7. railway_owner — disabled
- Logout. Hover on the
railway_ownerrow. Expected: row is muted, cursor isnot-allowed. The disabled reason text is visible below the label. - Try to click it. Expected: nothing happens; no cookie is set; no redirect.
- From DevTools console, run:
Expected:fetch('/api/admin/login/dummy', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({profileId: 'railway_owner'})}).then(r => r.json().then(j => ({status: r.status, body: j})))
{status: 409, body: {error: "Profilen «railway_owner» kan ikke logges inn som: Owns DDL / SECURITY DEFINER functions — not a session role."}}. The server enforces the disabled state independently of the UI.
8. authenticator — disabled
Same as railway_owner. Repeat the row inspection and the console probe with profileId: 'authenticator'. Expect 409 with the "PostgREST runtime connection role — never appears in a user session." message.
Manual JWT-paste fallback
- From
/admin/login, click "Manuell innlogging (lim inn staff‑JWT)". - URL becomes
/admin/login?manual=1. The picker is still visible and the JWT-paste form renders below it. - Paste a valid staff JWT (e.g.,
POSTGREST_ADMIN_JWTfrom.env) into the textarea, click Logg inn. Expected: redirect to/admin, cookie set.
Env-JWT auto-bootstrap (CI / smoke)
This path bypasses the picker for automation:
- Visit
/admin/login?auto=1withbootstrapAllowedtrue (it is in dev) and a valid env JWT (POSTGREST_ADMIN_JWTorPOSTGREST_STAFF_JWT_UIS). - Expected: 307 redirect to
/api/admin/bootstrap-session, which sets the cookie and redirects to/admin.
This is what npm run smoke:admin exercises indirectly via the /api/admin/login route (it POSTs the staff JWT rather than visiting the page, but the resulting cookie is the same shape).
API-level smoke (no browser)
If the browser UI is broken, you can still verify the route behavior:
# Happy paths
for id in anon authenticated_full_admin authenticated_registrations authenticated_content authenticated_applog authenticated_users; do
printf "%-32s " "$id"
curl -s -o /dev/null -w "HTTP %{http_code}\n" -X POST http://localhost:3010/api/admin/login/dummy \
-H "Content-Type: application/json" \
-d "{\"profileId\":\"$id\"}"
done
# Should all be 200.
# Disabled roles
for id in railway_owner authenticator; do
printf "%-32s " "$id"
curl -s -o /dev/null -w "HTTP %{http_code}\n" -X POST http://localhost:3010/api/admin/login/dummy \
-H "Content-Type: application/json" \
-d "{\"profileId\":\"$id\"}"
done
# Should both be 409.
# Bad input
curl -s -o /dev/null -w "unknown: HTTP %{http_code}\n" -X POST http://localhost:3010/api/admin/login/dummy \
-H "Content-Type: application/json" -d '{"profileId":"bogus"}' # 404
curl -s -o /dev/null -w "empty body: HTTP %{http_code}\n" -X POST http://localhost:3010/api/admin/login/dummy \
-H "Content-Type: application/json" -d '{}' # 400
What to do when a test fails
- Picker doesn't render: check the Next dev server console. Likely a TypeScript or JSX error in
src/components/admin/dummy-login-picker.tsx. - Click does nothing: open the Network tab. The POST to
/api/admin/login/dummyshould appear. If it returns 4xx/5xx, the response body has the Norwegian error message. - Cookie not set: response was 200 but
Set-Cookieheader missing — checksrc/lib/admin-session.tsadminSessionCookieOptsForToken. - Sidebar shows wrong items: decode the cookie's JWT payload. The
capabilitiesarray there is whatstaffEffectiveCapabilitySet()insrc/lib/staff-jwt-caps.tsconsumes. If the JWT looks right but the sidebar is wrong, the bug is in the cap-set logic, not the picker. - PostgREST returns 500 PGRST300 or 401 role does not exist: not a Railway-side bug. See INVESTIGATE postgrest admin connection for the UIS-side recovery commands.
Related
- PostgreSQL roles — what
anon/authenticatedmean at the DB level - Project conventions — the PostgREST-only data-access rule
- Plan: Dummy login picker — the implementation plan this spec validates
End-user-facing description of each role
This spec covers the functional test of each role. The end-user-facing description of what each role sees and does lives under Administrasjon:
| Tested role | End-user guide |
|---|---|
anon | Slik melder du deg på |
| Full admin | Full administrator |
| Registrations admin | Registreringsadministrator |
| Content editor | Innholdsredaktør |
| App-log viewer | App-logg-leser |
| Users admin | Brukeradministrator |
When the functional test for a role passes, sanity-check that the sidebar shape in the end-user guide still matches what your tester saw — the guide makes claims about which sidebar groups are visible per role, and those need to stay in sync.