Commit Graph
125 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.7 2c0d5fb9fc fix(playwright/personas): treat Keycloak round-trip as auth proof + wait for injected logout
* Set a `keycloakRoundTripCompleted` flag once `performKeycloakLogin`
  has returned and the page is on the role's canonical hostname.
  oauth2-proxy-gated roles whose upstream UI ships no Account/Logout
  menu (Prometheus, status pages, raw upstream surfaces) now satisfy
  the persona auth-marker without a probe-able in-app control. The
  visible-marker poll still runs for roles that DO surface one.

* Add `[data-injected-logout]` as the first logout candidate so the
  shared `inAppLogout` helper picks up the per-role JS-injected
  fallback button (e.g. the Prometheus navbar patch) before falling
  through to the regex-driven candidates.

* Replace the implicit "snapshot isVisible" gate at the top of
  `inAppLogout` with a `waitForAnyLogoutCandidate` poll that pings the
  candidate set for up to 10 s. Required because the universal /
  per-role logout helpers inject their button asynchronously after
  DOMContentLoaded plus a MutationObserver settle; the previous
  synchronous check failed before the button could mount.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:48:53 +02:00
kevinveenbirkenbachandClaude Opus 4.7 9763388229 fix(playwright/personas): harden auth chain + logout discovery
* Wait for the role's Login link with `Locator.waitFor` instead of the
  snapshot-only `isVisible` — `isVisible({timeout})` only governs locator
  resolution and never polls for visibility, so an OIDC-driven UI that
  injects the Login link asynchronously (e.g. dashboard's `oidc.js`
  fetched at runtime from the CDN) was treated as logged-out and the
  helper returned in milliseconds without ever clicking the link.

* Once the link is visible, prefer a direct `page.goto(href)` to the
  embedded openid-connect/auth URL when the link exposes one; fall back
  to a real click plus `page.waitForURL(/openid-connect\/auth/)` so a
  JS-handler-intercepted click (keycloak adapter, single-page-app shell)
  still drives the test through Keycloak.

* Restrict the post-login URL fallback to nested frames. The main
  frame can park on the role's canonical domain without an authenticated
  session (the role's auth chain failed silently), so accepting the main
  frame URL as proof of authentication produced false positives. The
  iframe fallback still covers the legitimate "app surface rendered via
  embed" case.

* Broaden the auth marker so role=link account/profile/user-menu
  controls and `aria-label='account'`, `aria-haspopup`, Bootstrap
  `data-bs-toggle='dropdown'`, and a `href*='logout|end_session'` link
  count as the post-login surface signal. Roles that hide the auth
  marker behind a closed dropdown still surface a probe-able anchor.

* Rebuild `inAppLogout` to iterate every visible menu trigger (Bootstrap
  dropdowns, `.user-menu-toggle`, `data-testid='user'`, ARIA-popup
  controls), not just the first match. After each click the helper
  polls for logout/menuitem/link candidates inside the dropdown,
  closes the menu again before trying the next trigger, and falls back
  to a top-level `a[href*='logout']` so static-only logout links work
  even when no dropdown wraps them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:48:52 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b1b73bc1df refactor(lint): tighten inline-script lint to 11 lines + extract overflows
Drops `MAX_LINES` from 12 to 11 in `test_inline_script_block_size.py`,
catching the five blocks that were sitting exactly on the previous
threshold. Pure-bash blocks move to dedicated `.sh` files invoked via
`ansible.builtin.script:` or `run: bash …`; the keycloak heredoc with a
templated JSON payload splits into a `set_fact` plus a folded
`printf | kcadm … -f -` one-liner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:06:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 14700e0658 feat(test-e2e-playwright): add PLAYWRIGHT_KEEP_ALL artefact-retention toggle
Forwards `PLAYWRIGHT_KEEP_ALL` from host env / GitHub Actions repo var
through `TEST_E2E_PLAYWRIGHT_KEEP_ALL` (inventory-overridable) into the
container and flips trace / screenshot / video to `on` for every test
when truthy.

SPOT for the propagation chain lives in
`docs/contributing/actions/testing/playwright.md#artefact-retention-`;
CI Configuration and the agent iteration loops link to it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:06:31 +02:00
kevinveenbirkenbachandClaude Opus 4.7 f6f4236ffd fix(playwright/personas): wire PERSONA_*_BLOCKED + handle oauth2-proxy auth chain
Five fixes against the matomo deploy CI failure mode:

* The persona helpers ([admin.js], [biber.js]) now perform the
  Keycloak login on either auth shape — oauth2-proxy gate (the goto
  is intercepted and the page lands directly on
  `openid-connect/auth`) AND in-app OIDC plugin (Login link → click
  → redirect). Previously the Keycloak step was gated on
  `oidcEnabled`, which left oauth2-proxy-only roles like prometheus
  with no auth path. The login-link regex also tolerates leading /
  trailing whitespace (`/^\s*(log\s*in|...)\s*$/i`), unblocking the
  dashboard's `" Login"` link whose accessible name carries an
  icon-induced leading space.
* `roles/web-app-keycloak/templates/playwright.env.j2`: declare
  `PERSONA_BIBER_BLOCKED=true` and `PERSONA_ADMINISTRATOR_BLOCKED=true`.
  The persona stubs in the keycloak spec exist purely to satisfy the
  persona-naming lint; Keycloak IS the auth provider so neither
  persona has an in-role app surface. The bespoke
  master-realm / normal-realm tests already cover the contract.
* `roles/web-app-matomo/templates/playwright.env.j2`: same — matomo's
  local login form is not OIDC / oauth2-proxy, so the generic helper
  has no useful flow. The bespoke "matomo administrator" test covers
  the admin journey.
* `roles/web-app-prometheus/templates/playwright.env.j2`: only
  `PERSONA_BIBER_BLOCKED=true`. Prometheus is admin-only via
  oauth2-proxy with `allowed_groups:
  web-app-prometheus-administrator`; biber's session is denied at the
  proxy and biber has no in-app surface to drive a logout from.
  Administrator goes through the standard oauth2-proxy → Keycloak
  chain. Also rendered the missing `PROMETHEUS_BASE_URL`,
  `CANONICAL_DOMAIN`, and pointed `APP_BASE_URL` at prometheus itself
  so the persona helper drives the role under test (not the dashboard).
* `roles/web-app-prometheus/files/playwright.spec.js`: read
  `process.env.CANONICAL_DOMAIN` directly so the env-keys-used lint
  recognises the new env entry as consumed.

The 019 matrix note for prometheus is updated to describe the
biber-only opt-out and the admin's oauth2-proxy chain.

Out of scope: the matomo CSP biber-deny test still fails because
`roles/web-app-matomo/meta/services.yml` has `oauth2.enabled: false`
(TODO at line 12 — wire matomo's oauth2-proxy gate). That is a
deploy-config change, not a test fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:53:39 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0e651e4a22 chore(lint): tighten ESLint rules and clean up flagged offenders
Promote four cheap rules in `eslint.config.js`:
* `no-unused-vars`: warn → error (with `argsIgnorePattern: "^_"`)
* `no-var`: error
* `eqeqeq`: error (always)
* `playwright/no-wait-for-navigation`: off → error
* `linterOptions.reportUnusedDisableDirectives`: off → warn

Add a repo-root CommonJS block so `eslint.config.js` itself sees Node
globals (`require`, `module`).

Fix every flagged offender:
* 138 stale persona-helper imports across 70 spec files
  (`isServiceEnabled`, `skipUnlessServiceEnabled`, `runAdminFlow`,
  ... — leftovers from the SPOT consolidation).
* 4 unused catch bindings → modern `catch {}` syntax.
* 2 unused `dashboardBaseUrl` declarations (minio, moodle).
* 1 unused `label` arg → renamed to `_label` to opt out per the
  argsIgnorePattern.
* 1 intentionally-kept legacy helper in web-app-wordpress's spec
  receives a per-line `eslint-disable-next-line` with reason; the
  preceding docstring already explained why it stays.
* 1 `page.waitForNavigation` in web-app-fediwall's Mastodon OIDC flow
  receives a per-line disable with reason — the navigation target
  depends on the runtime OIDC issuer; `waitForURL` would need a
  runtime-built pattern.
* 10 stale `// eslint-disable-next-line no-console` directives now
  surfaced by `reportUnusedDisableDirectives`; removed.

ESLint after this pass: 0 errors, 154 warnings (148 pre-existing
`playwright/expect-expect` plus 6 `playwright/no-force-option` that
were already inherited from `playwright/flat/recommended`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:25:23 +02:00
kevinveenbirkenbachandClaude Opus 4.7 c55ad69c41 refactor: drop requirement-file references from code, add doc rule + lint
Strip every `req <NNN>`, `req-<NNN>`, `Requirement <NNN>`,
`requirement-<NNN>`, and `docs/requirements/<NNN>-...md` mention from
non-Markdown source files. Comments anchored to requirement numbers
rot when requirements are renumbered or rewritten and conflate
present-state behaviour (which the comment should describe) with the
audit trail of how the code got there (which belongs in commit
messages, PR descriptions, and the requirement file itself).

* Document the rule in `docs/contributing/documentation.md` under
  "Comments". Code MUST describe the *why* in present-state language;
  contract enforcement lives in tests, not folklore.
* Clean 254 source files across `roles/`, `tests/`, `plugins/`,
  `cli/`, `scripts/`, `utils/`, `group_vars/`, and the repo-root
  `eslint.config.js`. Pure comment / docstring / log-message edits;
  no behavioural change.
* Add `tests/lint/repository/test_no_req_references_in_code.py` to
  enforce the rule going forward across `.js`, `.py`, `.yml`,
  `.yaml`, `.j2`, and `.sh` files. The legacy
  `test_no_stale_requirement_refs.py` is exempt as the only file
  whose purpose is to detect these very tokens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:25:22 +02:00
kevinveenbirkenbachandClaude Opus 4.7 eb7344cc04 refactor(playwright): share DOM + regex utility helpers
Five role specs (dashboard, nextcloud, odoo, pixelfed, taiga)
duplicated four small utility helpers across one-cluster-each
inline copies:

  - isVisible(locator)               — `locator.first().isVisible()`
                                       with the strict-mode / detached
                                       error swallowed.
  - waitForFrameUrl(iframe, ...)     — poll the embedded iframe URL
                                       until it contains a matcher.
  - findFirstVisibleCandidate(list)  — pick the first visible
                                       candidate from a label/locator
                                       list and return the resolved
                                       single-element locator.
  - escapeRegex(value)               — escape regex metacharacters
                                       so a raw string can be used as
                                       a `new RegExp(...)` source.

Changes
-------
- New `roles/test-e2e-playwright/files/personas/utils/dom.js` collects
  all four helpers (canonical 1-cluster bodies, byte-equivalent to the
  former inline copies).
- `personas/utils/index.js` re-exports the new module so callers reach
  the helpers via `require("./personas")`.
- The five specs strip their inline definitions and amend the
  destructuring with whichever helpers their body still references.

Drift handling
--------------
None. All four helpers were 1-cluster across the call sites; this is
mechanical substitution.

Verification
------------
- 419 passed / 1 skipped / 0 failed across `tests/lint/`,
  `tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors, 283 warnings (unchanged from
  baseline).
- 0 inline `function isVisible|waitForFrameUrl|findFirstVisibleCandidate|escapeRegex`
  definitions remain in any role spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:25:21 +02:00
kevinveenbirkenbachandClaude Opus 4.7 370f381d1c refactor(playwright): share Keycloak login-form helper
Fourteen role specs (bigbluebutton, discourse, fider, gitea, mailu,
matrix, mattermost, odoo, openwebui, peertube, postmarks, prometheus,
yourls, libretranslate) each carried an inline `performOidcLogin`
that drove the Keycloak login form. The bodies fell into four
clusters split between the role-based selector strategy
(`getByRole`) and the legacy input-name strategy
(`input[name='username']`); the existing shared
`performKeycloakLogin` already covered both via `.or()` chains but
also asserted post-login navigation, so it could not be reused
verbatim.

Changes
-------
- `personas/utils/keycloak.js` gains `performKeycloakLoginForm
  (target, username, password)` — form-fill + submit only, accepts
  `Page` OR `FrameLocator` so iframe-embedded Keycloak pages work
  without branching at the call site. The selectors are the union
  of all four prior cluster shapes (role-based getter chained with
  `.or(input-name fallback)`, sign-in matched against
  `/sign in|login|log in/i` plus `input#kc-login` family). The
  existing `performKeycloakLogin` and
  `performKeycloakLoginExpectingDenial` now delegate to the new
  helper, so their public API is unchanged.
- The 14 specs strip their inline `performOidcLogin` and amend
  `require("./personas")` with `performKeycloakLoginForm`. Every
  call site and comment reference is renamed in lockstep.

Drift handling
--------------
- Cluster 1 (5 specs) expected the accessible name `"Password"`
  exactly; the canonical regex `/^password$/i` is case-insensitive
  and matches every prior name plus lowercase variants — strictly
  more permissive.
- Cluster 1's sign-in button matched `/sign in/i`; the canonical
  matches `/sign in|login|log in/i` plus the legacy `input#kc-login`
  family — strictly broader.
- Cluster 1 awaited `usernameField.press("Tab")` without a `.catch`;
  the canonical wraps the press in `.catch(() => {})` so an
  unexpected Tab failure no longer aborts the flow.
- Cluster 2 / 3 used `expect(usernameField).toBeVisible({ timeout })`
  for the visibility wait; the canonical uses
  `usernameField.waitFor({ state: "visible", timeout })`, the
  Locator-method counterpart of the same wait — functionally
  equivalent.

Verification
------------
- 419 passed / 1 skipped / 0 failed across `tests/lint/`,
  `tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors, 283 warnings (unchanged from
  baseline).
- 0 inline `performOidcLogin` definitions remain in any role spec,
  and 0 stale references to the legacy name in any context (call
  sites and comments are renamed in the same pass).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:55:05 +02:00
kevinveenbirkenbachandClaude Opus 4.7 7018b3c991 refactor(playwright): share CSP header/meta/violation helpers
Nine role specs (bigbluebutton, dashboard, discourse, keycloak,
matomo, matrix, moodle, openwebui, wordpress) each carried inline
copies of the same six CSP helpers and the `EXPECTED_CSP_DIRECTIVES`
constant. The bodies were semantically identical; only message
wording, brace style, and one captured-field-set differed.

Changes
-------
- Extends `roles/test-e2e-playwright/files/personas/utils/csp.js`
  (already home to `assertCspInjections`) with
  `installCspViolationObserver`, `readCspViolations`,
  `parseCspHeader`, `assertCspResponseHeader`,
  `assertCspMetaParity`, `expectNoCspViolations`, and the
  `EXPECTED_CSP_DIRECTIVES` constant. The helpers are re-exported
  through `personas/utils/index.js` so specs that
  `require("./personas")` get them via the existing aggregator.
- Strips the inline definitions from the nine role specs and amends
  each spec's `require("./personas")` destructuring with whichever
  helpers the body still references.

Drift handling
--------------
- moodle's reduced violation-record shape (only `violatedDirective`
  + `blockedURI`) is replaced by the canonical 5-field record.
  Moodle's downstream consumer asserts `.toEqual([])` on the array
  only, so the extra fields are diagnostic-only — no behaviour
  change.
- dashboard's 2-arg `expectNoCspViolations(page, label)` call site
  is updated to the canonical 3-arg form
  `expectNoCspViolations(page, null, label)`. The shared helper
  guards `if (diagnostics && Array.isArray(diagnostics.cspRelated))`
  before checking the diagnostics arm, so passing `null` keeps the
  exact pre-migration behaviour for that call site.
- bigbluebutton and matrix had subtly different failure-message text
  in `assertCspResponseHeader` and `expectNoCspViolations`. The
  canonical wording is more descriptive; passing tests are
  unaffected, failing tests get clearer diagnostics.

Doc
---
`docs/contributing/artefact/files/role/playwright.specs.js.md` is
extended in two places: the invariants section names the full CSP
test surface and pins the "import from `./personas`, no inline copy"
rule, and the per-service catalogue's CSP / injectors row lists the
helpers by name.

Verification
------------
- 419 passed / 1 skipped / 0 failed across `tests/lint/`,
  `tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors, 283 warnings (unchanged from
  baseline).
- `markdownlint-cli2`: 0 errors on the touched doc.
- 0 inline definitions of any extracted helper / constant remain in
  any role spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:55:05 +02:00
kevinveenbirkenbachandClaude Opus 4.7 122c82910b refactor(playwright): share decodeDotenvQuotedValue + normalizeBaseUrl
Both helpers were inlined verbatim in 71 of the role-local
`playwright.spec.js` files (52 with the compact form, 19 with the
verbose form, plus one parameter-name variant). All bodies were
semantically identical.

Changes
-------
- New `roles/test-e2e-playwright/files/personas/utils/dotenv.js` holds
  the canonical pair (`decodeDotenvQuotedValue` and `normalizeBaseUrl`),
  re-exported through `personas/utils/index.js` so specs that
  `require("./personas")` get them via the existing aggregator.
- Every role spec drops its inline copies and pulls the helpers from
  `./personas` instead. Specs that only reference one of the two
  carry only that one in the destructuring (no unused-import noise).

Doc
---
`docs/contributing/artefact/files/role/playwright.specs.js.md` no
longer carries the "once a third spec needs them, MUST be promoted"
directive: the helpers live at the shared path and inline copies are
forbidden.

Verification
------------
- 419 passed / 1 skipped / 0 failed across `tests/lint/`,
  `tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors, 283 warnings (unchanged from
  baseline; no new unused-imports introduced).
- 0 inline `function decodeDotenvQuotedValue|normalizeBaseUrl`
  definitions remain in any role spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:55:05 +02:00
kevinveenbirkenbachandClaude Opus 4.7 db2a2d9638 refactor(playwright): move cross-service tests to provider specs
Cross-service surface assertions for dashboard, prometheus and matomo
no longer run in every consumer role's persona. They are owned by the
provider's own spec, parameterised once over a `<NAME>_TARGET_ROLES_JSON`
manifest rendered into the provider's `templates/playwright.env.j2` via
the new `lookup('roles_with_service', '<svc>')` Ansible lookup.

Changes
-------
- New `plugins/lookup/roles_with_service.py` enumerates roles that
  declare `services.<svc>.{enabled, shared}` truthy in the merged
  applications config, returning `[{id, canonical_domain}, …]`.
- `roles/web-app-{dashboard,prometheus,matomo}/files/playwright.spec.js`
  parse `<NAME>_TARGET_ROLES_JSON` and parameterise the per-consumer
  assertion (dashboard tile reachability, prometheus `up=1`, matomo
  SitesManager registration). The matomo spec gains an explicit
  biber-denial test (request body validated against admin DOM markers
  per the body-marker rule); the prometheus admin-reach test drops
  the dashboard-iframe path in favour of direct goto.
- Persona helpers `roles/test-e2e-playwright/files/personas/{admin,
  biber,guest}.js` enter at `${APP_BASE_URL}/` directly. The
  `clickRoleTileFromDashboard` step and the `assertPrometheus*` /
  `assertMatomo*` cross-service probes are gone; `utils/{dashboard,
  prometheus,matomo}.js` are deleted. `utils/csp.js` switches to
  literal `process.env.<KEY>` reads so the env-keys-used parity guard
  recognises matomo's CSP injector consumption.
- 42 consumer-role `templates/playwright.env.j2` templates drop
  `DASHBOARD_BASE_URL`, `DASHBOARD_SERVICE_ENABLED`, `PROMETHEUS_BASE_URL`,
  `PROMETHEUS_SERVICE_ENABLED`. `MATOMO_BASE_URL` and
  `MATOMO_SERVICE_ENABLED` stay because the per-role CSP injection
  helper still gates on matomo's tracker host.
- `tests/integration/roles/test_playwright_env_services_match.py`
  globally exempts `dashboard` and `prometheus` from the
  services.yml-to-env parity rule.
- `tests/unit/plugins/lookup/test_roles_with_service.py` adds 14
  unit tests for the lookup, including an explicit-contract test
  that pins the over-inclusive behaviour on raw Jinja strings (the
  lookup expects a templar-rendered payload).
- Docs: `docs/requirements/019-playwright-meta-services-parity.md`
  rewords Rules 1, 3, 12 and adds Rule 13 describing the SPOT shift.
  `docs/contributing/artefact/files/role/playwright.specs.js.md`
  reflects the new persona shape and per-service catalogue, and is
  cleaned per `docs/contributing/documentation.md` (no SPOT self-
  labels, no req-references, em dashes replaced with complete
  sentences, multi-clause lines split onto separate lines, broken
  anchor `#two-personas-fixed-flow-` corrected).

Verification
------------
- 418 passed / 1 skipped / 0 failed across `tests/lint/`,
  `tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors.
- `markdownlint-cli2`: 0 errors on all touched docs.
- `make autoformat`: clean (FURB171 single-item-set inequality fixed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:55:05 +02:00
kevinveenbirkenbachandClaude Opus 4.7 fa39d5522b fix(test-e2e-playwright/personas): gate dashboard via safeIsEnabled
`tests/integration/roles/test_playwright_spec_env_gates.py` failed
across 35 roles: every spec that imports `runBiberFlow` /
`runAdminFlow` ships `DASHBOARD_SERVICE_ENABLED=` in its env template,
but the persona helpers consume the dashboard purely via
`process.env.DASHBOARD_BASE_URL` without ever passing the literal
`"dashboard"` to `safeIsEnabled` / `requireService` / friends. The
integration test grep'd for the string-literal call and (correctly)
saw zero matches, so flagged the flag as declared-but-never-gated.

Adds `safeIsEnabled("dashboard");` as a bare-statement gate to
admin.js and biber.js, parallel to the existing `safeIsEnabled
("oauth2"); safeIsEnabled("logout");` lines that already pin those
two flags to the persona surface. The integration test walks every
file under `roles/test-e2e-playwright/files/personas/` once any
persona runner is referenced in the spec, so a single mention covers
all 35 roles in one shot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:55:04 +02:00
kevinveenbirkenbachandClaude Opus 4.7 a35636051e feat(playwright/personas): early-skip + URL fallback + drop bogus dashboard gate
Three coupled changes to `runBiberFlow` / `runAdminFlow`:

1. Move `PERSONA_BIBER_BLOCKED` / `PERSONA_ADMINISTRATOR_BLOCKED`
   to position 0 of the flow. A role that opts out now short-circuits
   the entire dashboard → app → logout journey instead of walking
   through the auth chain and tripping the in-app logout step where
   a late opt-out check could not catch it.

2. Add a URL-based fallback to the auth-surface detection: when the
   selector-based marker (`getByRole("button", { name: /^(log\s*out…)/i
   })`) finds no logout / user-menu control on any frame, accept any
   nested frame whose URL has settled on the role's canonical domain
   (and is NOT on Keycloak / oauth2-proxy denial / about:blank) as
   evidence that the persona reached the authenticated surface.
   Iframe-wrapped role surfaces (dashboard tile + role app inside)
   and SPAs whose logout sits behind icon-only avatar dropdowns then
   count as authenticated.

3. Drop `safeSkipUnlessEnabled("dashboard")` from the top of both
   flows. The flag is rendered as `<NAME>_SERVICE_ENABLED=` from the
   *provider* perspective in `meta/services.yml` (matomo's services
   declare `dashboard.enabled: <web-app-dashboard in group_names>`,
   meaning "is the dashboard role part of *this* role's stack").
   When matomo deploys solo, the flag rendered as `false` even
   though the dashboard URL was reachable, and every persona test in
   the matrix run silently `-`-skipped. The auth-less collapse check
   immediately below (`if (\!dashboardBaseUrl || \!canonicalDomain)
   skip`) is the correct gate — a role that legitimately cannot
   start the journey has no `DASHBOARD_BASE_URL` rendered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:06:50 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d845a8b34e feat(playwright/personas): dashboard dropdown handling + iframe-aware deny-checks + keycloak auth-provider exemption
Persona helpers learn three new behaviours:

* `clickRoleTileFromDashboard` and `clickSiblingTileFromDashboard`
  open every visible Bootstrap dropdown / accordion trigger
  (`[data-bs-toggle='dropdown']`, `[aria-expanded='false']`, ...) on
  the dashboard before searching for the role tile, so roles that
  live inside a collapsed category (e.g. keycloak under "Identity")
  are reachable without coupling personas to the dashboard menu shape.

* `assertPrometheusForbiddenForBiber` and
  `assertMatomoForbiddenForBiber` switch from fragile iframe DOM
  inspection to a direct authoritative probe via
  `page.request.get(<role-url>, { maxRedirects: 0 })`. They peek at
  the FIRST hop only: 401 / 403 or a redirect into the auth chain
  (`/oauth2/start|sign_in|callback`, `openid-connect/auth`) means
  denial; any other status is examined further. A first-hop 200 is
  acceptable ONLY when the body carries role-specific markers
  (`prometheus_build_info` / `<title>Prometheus</title>` for
  prometheus; matomo's login-form markers / `piwik|matomo` for
  matomo). A 200 with a non-matching body is treated as a
  misconfigured proxy or a denial-as-200 surface and fails loudly,
  matching req 019 Rule 12.

* `web-app-keycloak`'s spec drops the generic persona-flow runners
  (`runGuestFlow` / `runBiberFlow` / `runAdminFlow`) under the
  auth-provider exemption: the bespoke "master-realm super
  administrator", "normal-realm administrator", and "normal-realm
  biber logs in through account interface" tests already cover the
  three persona contracts via the realm account UI. The matching env
  flags are marked `# nocheck: playwright-service-gate` /
  `# nocheck: persona-required-envs` with auth-provider rationales.

Drift matrix in `019-playwright-meta-services-parity.md` strikes
through `web-app-matomo`, `web-app-prometheus`, `web-app-keycloak`,
`web-app-akaunting`, `web-app-baserow`, `web-svc-cdn`, and
`web-svc-simpleicons` after their FULL_CYCLE deploys produced green
Playwright runs (with the well-known kcadm.sh + sys-svc-mail
infrastructure failures documented in `web-app-mailu/TODO.md` per
the autonomy escape clause).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 02:51:01 +02:00
kevinveenbirkenbachandClaude Opus 4.7 672c61e37e test(playwright/personas): hard-fail on un-executable persona journeys
The persona-flow runners now fail loudly when biber / administrator
cannot reach an authenticated surface on the role under test. The
previous runtime auto-detection silently `test.skip(...)`'d, hiding
real regressions (broken OIDC mapping, removed logout button,
misconfigured oauth2-proxy, drifted UI selectors) behind a green
deploy.

Roles where the persona legitimately has no access (admin-only roles,
auth-provider roles) MUST declare an explicit env opt-out
`PERSONA_BIBER_BLOCKED=true` / `PERSONA_ADMINISTRATOR_BLOCKED=true`
in `templates/playwright.env.j2` with a documented rationale; without
the flag the helper fails the test with a diagnostic naming the
last-seen URL.

A new lint guard (`test_persona_strict_mode.py`) hard-fails any
future `test.skip(...)` in the persona helpers that is not guarded
by an allowed env opt-out / auth-less-collapse / service-gate
condition, and any `status === 200` branch in the prometheus / matomo
deny-check helpers that does not validate role-specific body markers.

Req 019 Rule 11 (strict-skip contract) and Rule 12 (200-body validation)
plus a matching "Strict failure on un-executable persona cases"
section in `playwright.specs.js.md` document the contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 02:44:25 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d3e5ebe441 feat(test-e2e-playwright): add shared persona-flow library + first batch
Introduces roles/test-e2e-playwright/files/personas/ — runGuestFlow,
runBiberFlow, runAdminFlow plus 10 utils (csp, dashboard, env, keycloak,
landing, logout, matomo, prometheus, interaction, index) — and wires
both the Ansible runner (run_one.yml) and the local rerun helper
(scripts/tests/e2e/rerun-spec.sh) to stage the tree alongside
service-gating.js so specs can `require("./personas")`.

First batch of role specs migrated to consume the shared library
(dashboard, keycloak, mailu, matomo, prometheus, web-svc-cdn,
web-svc-simpleicons) — each gets the spec.js persona scenarios plus
the matching meta/services.yml + playwright.env.j2 entries; matomo is
marked role-closed in req 019. dashboard/keycloak/mailu TODO.md
updated; docs/requirements/019 reflects matomo closure.

Reason: req 019 Rule 3 mandates that every role's guest/biber/
administrator flow runs the same canonical steps; centralising them
in one shared library keeps per-role drift from re-introducing the
stub-shaped bodies the new lint forbids.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:05:57 +02:00
kevinveenbirkenbachandClaude Opus 4.7 256e027525 docs: drop stale requirement refs + reflow soft-wraps to one sentence per line
Removes 67 live references to docs/requirements/<NNN>-*.md files across
43 contributor docs, role READMEs, code comments, task files, and tests.
Requirement docs are initial-creation-only artefacts; live references
freeze a snapshot that rots once the per-feature SPOT (a contributor
doc, a code module, a lint test) takes over the live behaviour.

In the same pass, fixes documentation.md violations on the touched
files: drops em-dashes used as thought breaks, removes "SPOT for ..."
self-labels, strips historical phrasing ("the legacy", "previously",
"used to", "is no longer used", "matching the historical"), and
reflows soft-wrapped sentences so each sentence stays on one logical
line.

The `test_inventory_vars_file_spot.py` whitelist for the literal
inventory path drops the docs/requirements/003 entry and instead
skips docs/requirements/ entirely; future requirement docs that
mention the literal path are then ignored automatically.

Verified by tests/lint/repository/test_no_stale_requirement_refs.py
(green tree-wide).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 05:00:46 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d009a35116 feat(rbac): hierarchical /roles/<app>/<role> + service-gated Playwright + Discourse round-trip
Implements requirements 005, 006, and 007.

005: per-application Keycloak `group-ldap-mapper`s anchor each app's
role groups under `/roles/<application_id>/<role_name>` (and, for
tenant-aware apps, `/roles/<application_id>/<tenant>/<role_name>`).
The flat-CN LDAP layout keeps each CN globally unique so Keycloak's
group-tree resolver never collides; per-app mappers use the LDAP
`description` as the visible Keycloak group name to keep the path
free of redundant prefixes. Migration purges legacy pre-005 entries
and forces a Keycloak `logout-all` so stale tokens drop pre-005
group paths.

006: per-service `<SERVICE>_SERVICE_ENABLED` env flags and the
`service-gating.js` helper let role specs `skipUnlessServiceEnabled`
cleanly when a shared service is missing or `SERVICES_DISABLED` is
set; `MODE_CI` markers (`INFINITO_MAKE_DEPLOY`, `GITHUB_ACTIONS`,
`ACT`) propagate through the deploy CLI into the test stage.

007: WordPress->Discourse round-trip Playwright scenario gated on the
`web-app-discourse` deployment.

WordPress Playwright runs 5/5 green on the single-site flow; the
multisite + Discourse scenarios are correctly skipped when their
service prerequisites are absent. The `rbac_group_path` lookup is the
single sanctioned producer for OIDC group paths and is wired into
every consumer (yourls, lam, minio, openwebui, pgadmin, phpldapadmin,
phpmyadmin, prometheus, xwiki).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 08:51:01 +02:00
Environment TestandClaude Sonnet 4.6 c47ac1c4cb feat(playwright): make images.playwright.version the SPOT for version
- Add image_version filter plugin to extract bare semver from image tag
- Derive TEST_E2E_PLAYWRIGHT_VERSION via image_version filter
- Convert files/package.json to templates/package.json.j2 using the
  derived version so image tag and devDependency stay in sync
- Use lookup('image', 'playwright', 'ref') in run_one.yml instead of
  building the image ref manually from package.json
- Add unit tests for image_version filter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 12:50:42 +02:00
dependabot[bot] de6fe82047 build(deps-dev): bump @playwright/test
Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.58.2 to 1.59.1.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.58.2...v1.59.1)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.59.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-11 01:52:25 +00:00
kevinveenbirkenbachandClaude Sonnet 4.6 10bdf4fe18 fix(test-e2e-playwright): set retries to 2 to reduce flaky test failures
Transient issues (DNS timeout, slow CI runner) no longer cause immediate
failure — each test gets up to 2 retries before being marked as failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 20:31:38 +02:00
kevinveenbirkenbachandClaude Sonnet 4.6 d0b662a4c2 fix(playwright): fix timeout, selectors and first-run wizard modal
- Increase global test timeout from 60s to 300s; the OIDC login flow
  alone can consume the full 60s budget, causing timeouts in subsequent
  wait steps (waitForVisibleCandidate at line 171 in the CI logs)

- Replace non-existent NC 30+ selectors verified against the actual
  nextcloud:production-fpm-alpine image:
  - remove [aria-controls="header-menu-user-menu"] (not in NC 30+)
  - remove settingsMenuButton role "Settings menu" (not confirmed)
  - replace #content-vue / #appmenu with #header-start__appmenu
    (layout.user.php uses <nav id="header-start__appmenu">)
  - remove #logout (no such ID in layout.user.php)

- Dismiss the firstrunwizard modal before clicking the user menu:
  firstrunwizard-activate.mjs opens a NcModal after DOMContentLoaded
  on first login; its .modal-mask intercepts all clicks and would
  cause the user-menu click to fail silently on first login

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 22:35:08 +01:00
kevinveenbirkenbach a93729e780 Remove local Playwright recording wrapper 2026-03-18 22:30:09 +01:00
kevinveenbirkenbach aef39f7542 Refactor central Playwright runner files 2026-03-18 22:16:46 +01:00