Commit Graph
17 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5 4ce588fea1 test(lint): forbid a JavaScript default for an unset environment variable
`process.env.X || <value>` is a second source for something the deployment
already renders, and the two drift without a sound. playwright.config.js read
`process.env.PLAYWRIGHT_ACTION_TIMEOUT || 30_000` while nothing ever set that
variable, so every run used the unscaled 30 s and no failure said so.

Normalising an absent variable stays allowed, because it substitutes nothing:
`|| ""`, `|| '[]'`, `|| false`, `|| 0` and falling through to another
`process.env` entry only let the caller test absence itself.

The six sites the rule found:

  n8n and flowise built a Postgres port from `|| '5432'` while env.j2 renders it
  from `lookup('database', application_id, 'port')`. Node throws
  ERR_SOCKET_BAD_PORT on the resulting NaN, so an unfed port now names itself
  instead of connecting somewhere plausible.

  The bluesky broker did the same with BROKER_PORT, which compose.yml.j2 renders
  unconditionally. Its INSECURE_TLS default was `"false"`, which is what `""`
  already evaluates to.

  matomo defaulted MATOMO_TRACKING_SCOPE to "sub"; the only comparison is against
  "root", so `""` takes the identical branch.

  moodle and simpleicons defaulted a name their playwright.env.j2 always renders.
  simpleicons already asserts the value is truthy; moodle now fails on the scope
  it cannot find rather than on a name nobody configured.

Four values the code itself owns keep a default under `// nocheck: env-default`:
PDS_INTERNAL_URL (which falls through to requireEnv, not to a literal),
HANDOFF_COOKIE_NAME, HANDOFF_COOKIE_MAX_AGE and PASSWORD_SALT_HASH_ROUNDS. No
role renders any of them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 12:07:57 +02:00
kevinveenbirkenbachandClaude Opus 5 575a5a87ac refactor(layout): sort shipped scripts by language and split the unit suite
Every script a role ships now lives under `files/<language>/`, and the unit
suite is split the same way: `tests/unit/python/` beside new sibling trees for
the other languages. `tests/lint/filesystem/test_role_files_layout.py` keeps it
that way; directories whose shape is dictated from outside carry a `.nocheck`
naming the constraint (Docker build contexts, WordPress mu-plugins, the Joomla
extension, Odoo addons, Playwright suites, the `files/test/test.sh` harness
that `test-e2e-cli` resolves generically).

This cannot be applied in halves. Moving a file and repointing what names it
are the same change: 14 references resolve only at deploy time, so the tree is
green either before or after, never between.

Those 14 were found by static resolution over every reference class, not by the
test suite -- a full `make quality-high` was green while all of them were
broken:

  * 7 `src:` values built from a role variable holding a bare filename
    (svc-bkp-remote-2-local, decidim, espocrm, friendica, joomla, pixelfed x2).
    The variable doubles as the destination name inside the container, so the
    language segment belongs at the call site, not in the variable.
  * `sys-service/tasks/04_base.yml` resolved every consumer's systemd script
    through `first_found(..., errors='strict')` over `files/script.{sh,py}`.
    18 roles ship exactly that and have no template fallback: the play would
    have died on each.
  * `sys-front-inj-javascript` fell back to `<role>/files/javascript.js`; the
    interface now reads `files/javascript/javascript.js`.
  * `sys-front-inj-logout/templates/head_sub.j2` built the cache-busting mtime
    path through `path_absolute(... 'files', ...)`.
  * 4 `ansible.builtin.script:` invocations naming a bare command
    (pull.py, prune.sh, set_postgres_superuser_password.sh, ganesha-serve.sh).
  * `dev-python/files/shell/install.sh` pointed at a sibling role's script.

Depth-dependent paths inside the moved trees were bumped accordingly: 59
`parents[N]` computations, 12 relative climbs, and the per-role recover CLI,
whose path `cli/administration/recover/recoverers.py` still assembled from the
pre-move location.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 01:31:09 +02:00
kevinveenbirkenbachandClaude Opus 5 642055a20a style(e2e): drop the timeout argument isVisible discards
Playwright declares the timeout option on locator.isVisible() and isHidden() as deprecated and ignored: both return immediately and never wait. Passing resolveTimeout(10_000) to one therefore changes nothing at runtime and only tells the reader a wait happened that never did. That reading is what let 7538b8a0b conclude two genuine call sites were fine, and what hid the false negative b97cf8888 had to fix.

Sixty call sites across fifty-eight role-local specs lose the argument and nothing else. Every added line in this commit is a bare isVisible(); a grep for added lines that are not one returns zero, so a behaviour change cannot have entered here. Files whose diff also alters waiting semantics are deliberately not part of this commit.

A repository lint added in b97cf8888 keeps the construct out. It matches isVisible and isHidden only, because isChecked, isDisabled, isEditable and isEnabled take a real timeout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 13:49:18 +02:00
kevinveenbirkenbachandClaude Opus 5 f09a05e69c fix(matomo/e2e): drop the unused resolveTimeout import
The spec navigates through gotoOnion and carries no timeout literals of its
own, so the import had no consumer and failed eslint's no-unused-vars, which
took the whole CI lint job down.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 06:47:15 +02:00
kevinveenbirkenbachandClaude Fable 5 d88f272f9f Merge branch 'feature/swarm-nfs' into feature/svc-net-tor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:31:08 +02:00
kevinveenbirkenbachandClaude Opus 4.8 07c6ad8e7c test(e2e): scale every nav click + timeout literal for onion targets
Over Tor the redirect chain behind a login/logout/OIDC click routinely exceeds Playwright's fixed 30s action timeout, so the click throws before the flow's scaled waits run. Sweep all role Playwright suites and the shared persona helpers: give every navigation/auth-transition click an onion-scaled timeout (.click({ timeout: resolveTimeout(30_000) })) and route the remaining raw millisecond literals (timeout options, waitForTimeout, sleep-promise setTimeout, positional wait args) through resolveTimeout. Local UI clicks (menus, toggles, modal dismissals, DOM clicks inside evaluate) stay untouched.

Two new lints keep it that way: test_no_raw_timeout_literals forbids raw numeric timeout literals in Playwright specs (opt-out: nocheck: raw-timeout), test_persona_clicks_scaled requires every click in the shared persona auth flows to carry a resolveTimeout timeout (opt-out: nocheck: unscaled-click).

Verified: node --check on all 133 changed JS files, all 13 playwright lints green, make autoformat clean, make test green (2745+196+163+4 tests OK).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:35:53 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4c91df5de7 test(e2e): forbid raw page.goto, migrate all specs to Tor-resilient gotoOnion
New lint test_no_raw_goto.py forbids raw `page.goto(...)` in role playwright specs + persona flows (the gotoOnion wrapper in personas/utils is exempt) so navigation retries transient Tor-transport errors over onion. Migrates every raw call across ~75 roles (343 sites) to `gotoOnion(page, ...)` and teaches gotoOnion to detect relative onion URLs (`isOnionCanonical`). gotoOnion is a behaviour-identical drop-in on clearnet (single attempt, same return). Verified: no-raw-goto lint green, eslint 0 errors, make test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:59:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 35dee215c8 test(playwright): route all timeouts through resolveTimeout for onion-compat
Add the global TIMEOUT_FACTOR and a shared timeouts.js helper: resolveTimeout(base) scales by TIMEOUT_FACTOR and, over a .onion target, an extra multiplier, so Tor's per-request latency no longer trips hard-coded timeouts. Migrate every raw timeout in the specs (timeout:/timeout=/setTimeout/waitForTimeout) to resolveTimeout, stage the helper in all three staging paths, and add test_no_raw_timeouts to forbid raw numeric timeouts. Clearnet behaviour is unchanged (factor 1: resolveTimeout(N) === N).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 10:38:06 +02:00
kevinveenbirkenbachandClaude Opus 4.8 bd0c70af17 feat(matomo): configurable tracking scope (sub default / root)
Add a per-deployment Matomo tracking scope, defaulting to per-subdomain:

- services.matomo.tracking.scope 'sub' (default) registers one Matomo site per
  consumer subdomain with an isolated cookie; 'root' keeps the prior behaviour
  of one shared site per registrable base domain with cross-subdomain linking.
- sys-front-inj-matomo derives matomo_site_domain (full host vs registrable
  base) and the shared matomo_site_url from it; getSitesId / addSite and the
  tracking snippet (setCookieDomain / setDomains / enableCrossDomainLinking) all
  follow the scope.
- The matomo Playwright env emits MATOMO_TRACKING_SCOPE; the SitesManager test
  matches the consumer's full host in 'sub' and its base domain in 'root'.
- variants.yml variant 0 sets scope=root so the matrix exercises both flavours
  (every other deploy runs the sub default).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:32:51 +02:00
kevinveenbirkenbachandClaude Opus 4.8 edb52b3c51 refactor(matomo): split playwright spec into logical files + shared module
Break the single 457-line playwright.spec.js into topic files discovered by the
harness testMatch "**/*.@(spec|test).js", mirroring the pihole / gitea layout:

- _shared.js            env vars, the setup hook, loginAsAdmin, attachDiagnostics,
                        hostOf / baseDomainOf and the MATOMO_TARGET_ROLES_JSON list
- playwright.spec.js    CSP + canonical-domain smoke (filename kept; harness asserts it)
- auth.spec.js          admin login/logout and biber denied at the admin surface
- tracking.spec.js      SitesManager site registration and per-consumer _paq injection
- persona-flows.spec.js guest / biber / admin universal-logout flows

Test logic is unchanged: same titles, selectors, regexes, timeouts and
assertions (verified by title parity and an adversarial diff against HEAD). The
only behavioural delta is that the extracted loginAsAdmin waits for the username
field to be visible before filling, which the SitesManager login previously
skipped (hardening only). The duplicated admin-login form-fill and the old
beforeEach are now the shared loginAsAdmin and setupMatomoPage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 16:37:56 +02:00
kevinveenbirkenbachandClaude Opus 4.8 833db8fd81 test(matomo): match SitesManager sites by base domain, not full host
The matomo injection registers one shared SitesManager site per registrable base
domain (addSite urls=proto://base_domain), so a subdomained consumer such as
logout.infinito.example is tracked under the infinito.example site, not its own.
The SitesManager test asserted the full canonical host as a substring of a site
URL, which never matched the base-domain site and failed in swarm.

Reduce the consumer's canonical_domain to its base domain (mirroring the regex in
sys-front-inj-matomo/vars/main.yml) and match it against each site's hostname
exactly. The exact match also drops the substring false positive, where a site at
evil-infinito.example would have satisfied infinito.example. Per-consumer tracker
presence stays covered by the separate per-role injection test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 16:37:56 +02:00
kevinveenbirkenbachandClaude Opus 4.8 0fcec0c4ae fix(web-app-matomo): authenticate the SitesManager e2e probe with token_auth
Matomo 5.x rejects the SitesManager.getAllSites API call with 401 when only the browser session cookie is sent; it requires token_auth. Surface the admin API token into the Playwright env (MATOMO_API_TOKEN) and pass it in the request POST body so it stays out of the URL/logs. Mode-agnostic test/env change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:35:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 24d4ab8e4a refactor(sso)\!: collapse oauth2 + oidc into unified services.sso block (req 21)
Implements docs/requirements/021-sso-flavor-migration.md atomically:

* Schema: every role's meta/services.yml replaces the parallel
  `oauth2:` / `oidc:` top-level blocks with a single `sso:` block
  carrying `enabled`, `shared`, and a `flavor` discriminator
  (`oidc` | `oauth2` | `saml`; default `oidc`). Flavor-specific
  sub-keys live under `sso.<flavor>.*` (`sso.oauth2.{origin,acl,
  allowed_groups}`, `sso.oidc.plugin`).
* Provider role: roles/web-app-oauth2-proxy/ is deleted in its
  entirety; its 5 sidecar templates move under
  roles/web-app-keycloak/templates/sso_proxy/, and the per-consumer
  rendering task lives at roles/web-app-keycloak/tasks/sso_proxy.yml.
  web-app-keycloak now declares `provides: sso`.
* Runtime: new resolver utils/roles/applications/services/sso.py
  centralises the SSO predicates (`is_enabled`, `is_proxy_gated`,
  `is_oidc_native`) and the oauth2 sub-values
  (`oauth2_origin_{host,port}`, `oauth2_acl`, `oauth2_allowed_groups`).
  Three lookup plugins consume the resolver: `sso` (single-app
  predicate access), `sso_proxy_consumers` (enumerates oauth2-flavored
  consumers), `sso_oidc_plugin` (renamed from `oidc_flavor`, Nextcloud
  plugin selector). sys-stk-backend, sys-svc-compose, sys-svc-proxy
  and plugins/filter/compose_volumes.py all gate on
  `lookup('sso', ..., 'is_proxy_gated')` instead of manually combining
  `enabled` and `flavor`.
* Identifier renames repo-wide: `services.<entity>.ports.local.oauth2`
  → `ports.local.sso`; `PORT_BANDS.local.oauth2` → `PORT_BANDS.local.sso`;
  `OAUTH2_PROXY_*` env/Jinja vars → `SSO_PROXY_*`;
  `oauth2_proxy_cookie_secret` → `sso_proxy_cookie_secret`;
  `OAUTH2_SERVICE_ENABLED` + `OIDC_SERVICE_ENABLED` collapse into
  `SSO_SERVICE_ENABLED`; Playwright gating helpers
  (isServiceEnabled/skipUnlessServiceEnabled/requireService/safeIsEnabled/
  safeSkipUnlessEnabled) move from `oidc`/`oauth2` to `sso`.
* Dual-block resolution: per req's Background-Table, web-app-bookwyrm
  is `flavor: oauth2`; web-app-gitea and web-app-friendica keep
  `flavor: oauth2` with their existing `oauth2.acl.blacklist`
  (/user/login) and `oauth2.acl.whitelist` (federation + discovery
  endpoints) — preserving the operative oauth2-proxy gating.
* Tests: the two mutual-exclusion guards
  (tests/integration/iam/oauth2_oidc/test_mutual_exclusive.py and
  test_acl_mutual_exclusive.py) are deleted per Decision #10;
  test_oauth2_contract.py is renamed and rewritten to
  test_sso_contract.py, asserting that `flavor: oauth2` roles carry
  `oauth2.origin.{host,port}` plus `ports.local.sso`; test_sso.py
  (web-app integration) now requires the unified `sso:` block with an
  explicit flavor or a `# nocheck: sso` opt-out; new
  tests/lint/repository/no_legacy_sso_paths/ guards the source tree
  against re-introducing the legacy strings.
* Docs: req 21 itself has every AC checked off, and reqs 006, 008,
  009, 013, 015, 016, 019, 020 plus 10+ role READMEs and
  docs/contributing/actions/testing/suppression.md are updated to the
  unified SSO naming.

Empirically validated end-to-end against the validation app set
(web-app-keycloak, web-app-nextcloud, web-app-bookwyrm, web-app-gitea,
web-app-friendica, web-app-dashboard) in matrix variants 0 and 1; v2
also passes the 4 target roles' Playwright after a documented
pre-existing KC_HOSTNAME-drift workaround (separate concern, not
caused by this refactor).

BREAKING CHANGE: every consumer of `services.oauth2.*`,
`services.oidc.*`, `OAUTH2_PROXY_*`, `OAUTH2_SERVICE_ENABLED`,
`OIDC_SERVICE_ENABLED`, `oauth2_proxy_cookie_secret`,
`oauth2_proxy_application_id`, `ports.local.oauth2`, the
`web-app-oauth2-proxy` role, or the legacy gating helper service names
(`oidc`/`oauth2`) must migrate to the unified `sso` shape documented
in docs/requirements/021-sso-flavor-migration.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 12:59:59 +02:00
kevinveenbirkenbachandClaude Opus 4.7 f24b2d903b style(playwright,csp): strip explanatory comments per minimal-comments rule
Removes prose block comments, JSDoc headers, and multi-line nocheck
rationale paragraphs added in the previous two commits — keeps only
the bare nocheck markers (which carry actual exception semantics
the lint test enforces) and the unchanged function/test bodies. No
behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 01:09:02 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b6a41941ec test(test-e2e-playwright): assert injected assets load + CSP allows them across css/logout/matomo
Adds the shared `assertInjectedAssetLoadsWithoutCspBlock(page, opts)`
helper in personas/utils/csp.js. It installs the CSP violation
observer, navigates the consumer URL with `waitUntil: networkidle`,
and asserts both:

  1. the browser observed a successful 2xx/3xx response for the
     requested resourceType(s) from one of the injector hosts, AND
  2. no `securitypolicyviolation` event named one of those hosts as
     its `blockedURI`.

Closes the loophole where the markup-only checks would pass even
when the consumer's CSP `style-src` / `script-src` did not whitelist
the injector host — the `<link>` / `<script>` was in the HTML but
the browser silently dropped the load.

Wires the helper into the three consumer-injection provider specs:

- web-svc-css: replaces the HTML-regex link check with the helper
  (`stylesheet` resource type, css.* + cdn.* hosts).
- web-svc-logout: extends the existing `logout.js`-in-HTML check with
  the helper (`script` resource type, cdn.* host). Adds CDN_BASE_URL
  to the env template since logout.js is served from the CDN.
- web-app-matomo: extends the per-consumer tracker test with the
  helper (`script` resource type, matomo canonical host) before the
  existing `_paq` / `matomo.php` markup tokens, so the matomo.js
  async fetch after DOMContentLoaded is verified end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 01:09:02 +02:00
kevinveenbirkenbachandClaude Opus 4.7 6fa7bd2f57 feat(playwright): expose canonical_url in roles_with_service; add web-svc-logout injection spec; parameterize matomo tracker check
- plugins/lookup/roles_with_service.py: resolve url.base via the tls
  sublookup and return canonical_url alongside id/canonical_domain.
- web-app-matomo/files/playwright/playwright.spec.js: parameterised
  matomo-tracker injection assertion per consumer over
  MATOMO_TARGET_ROLES_JSON, using target.canonical_url.
- web-svc-logout: new files/playwright/playwright.spec.js + templates/
  playwright.env.j2; checks universal-logout injection on every consumer
  declared in LOGOUT_TARGET_ROLES_JSON. meta/services.yml gains nocheck
  markers explaining why this provider has no user-facing surface to
  assert on.
- tests/unit/plugins/lookup/test_roles_with_service.py: cover the new
  canonical_url field via a stubbed tls sublookup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:24:56 +02:00
kevinveenbirkenbachandClaude Opus 4.7 c2c532c8e6 refactor(playwright): centralize dashboard-tile mechanics in web-app-dashboard SPOT; move specs into files/playwright/ subdir
Architecture
============
- New SPOT helper `roles/web-app-dashboard/files/playwright/dashboard-card-flow.js` (`runDashboardCardScenario(page, context, target)`): per `roles_with_service('dashboard')` target, asserts tile visible → click loads in `#main iframe` (outer URL gets `?iframe=` + iframe src matches canonical_domain) → header "Tab" button pops the embed into a fresh browser tab.
- `web-app-dashboard/files/playwright/playwright.spec.js` parametrised loop calls the helper for every `DASHBOARD_TARGET_ROLES_JSON` entry — the single owner of dashboard-tile mechanics across the whole codebase.

Spec relocation
===============
- 71 `roles/<role>/files/playwright.spec.js` → `roles/<role>/files/playwright/playwright.spec.js` (`git mv`, history preserved). odoo + pixelfed show as D+A in `git status` because of substantial content rewrites; coverage retained.
- New SPOT constant `ROLE_FILE_PLAYWRIGHT_SPEC` in `utils/roles/mapping.py`, registered as optional on `ROLE_TYPE_APPLICATION` (disallowed elsewhere, mirrors `ROLE_FILE_META_VARIANTS` pattern).
- `roles/test-e2e-playwright/tasks/run_one.yml`: staging task replaced with `with_fileglob` over `files/playwright/*.js` — roles can ship companion helpers (like `dashboard-card-flow.js`) next to the spec without further wiring.

Spec migrations (no more dashboard-iframe path in app specs)
============================================================
13 specs rewritten so the persona/SSO journey starts at the app URL directly, never through the dashboard tile or iframe:
* fider, mailu, mattermost, nextcloud, odoo, taiga: iframe-wrap removed (no more `frameLocator("#main iframe")`)
* bigbluebutton, discourse, kix, matrix, openwebui, peertube: dashboard goto preamble removed
* pixelfed: ~270 LOC of iframe-aware helpers/login/logout collapsed to direct-URL flow

Title cleanup
=============
- 64 specs: `"<persona>: dashboard → app → universal logout"` / `"administrator: dashboard → prometheus → app → universal logout"` → `"<persona>: app → universal logout"`.
- 13 specs: `"<persona>: dashboard to <app> ..."` → `"<persona>: <app> ..."`.

Dead-code cleanup
=================
- `dashboardBaseUrl` consts + beforeEach assertions removed from 11 specs (no functional uses; personas helper handles env validation).
- `DASHBOARD_BASE_URL=` env-template lines removed from fider, mattermost, odoo, taiga, pixelfed, kix.
- `DASHBOARD_SERVICE_ENABLED=` env-template lines removed (pixelfed retained for the still-gated spec).
- Stale "loads inside the iframe" / "FrameLocator parameter" comments cleaned in fider, odoo, prometheus, nextcloud.

Tooling
=======
- 5 lint Python files + 1 integration test now use `from utils.roles.mapping import ROLE_FILE_PLAYWRIGHT_SPEC`.
- `eslint.config.js` glob pattern + `roles/test-e2e-playwright/meta/main.yml` description + `roles/test-e2e-playwright/README.md` updated to new path.
- 25 markdown/docstring references in `docs/`, `roles/**/README.md`, `roles/**/TODO.md`, persona-helper docstrings updated to new path.
- `docs/contributing/artefact/files/role/playwright.specs.js.md`: file-placement section now documents the `files/playwright/` convention + `ROLE_FILE_PLAYWRIGHT_SPEC` SPOT constant; dashboard SPOT delegation explicit at 3 places.

Net diff: 124 files, +1142/-1564 (−422 LOC).
Integration suite: 183/183 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:24:55 +02:00