The role restricted the WOPI hosts with `domain` and with
`--o:net.wopi_allowed_hosts`. The distroless image that #775 brought in
applies neither. `domain` was translated by the start script of the older
image, which this one does not have, and coolwsd knows no setting called
`net.wopi_allowed_hosts`. Its startup log listed that key among the
overrides and no trusted WOPI host, so the server ran with the policy the
image ships instead of the one the role declares.
The allowlist now goes through `aliasgroup1`, the variable coolwsd reads
itself. It carries the origin of the Nextcloud domain as the deployment
resolves it, with scheme and port, so it also holds in a Tor variant. The
old expression was built from DOMAIN_PRIMARY and would not have matched an
onion address.
No spec opened a document, which is why this went unnoticed. The
richdocuments spec now uploads a document, opens it and waits for the
`status:` frame on the editor websocket.
Its first run on the Tor variant found a second defect. `ssl.termination`
was fixed to true, so coolwsd advertised https URLs in its discovery while
an onion vhost only serves http, and the browser never reached the editor.
The setting now follows the TLS state of the Collabora vhost.
Closes#802
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three Playwright workers run three spec files at once, and on a fresh instance the first OIDC callback is what provisions the Nextcloud account. Concurrent callbacks then race on INSERT INTO oc_users for the same uid: one wins, the losers answer HTTP 500. The exception is SQLSTATE[23000] 1062 Duplicate entry 'keycloak-<sub>' for key 'PRIMARY', raised from the sociallogin app's ProviderService through OC\User\Manager::createUser, and it is visible both in the CI rescue bundle of run 34653754175 and in the container log of a local variant-1 deploy.
The race is self-closing: once any insert wins, the window is shut for the rest of the run. It has never turned a job red, because Playwright's retries absorb it and a flaky-only run exits 0. What it costs is a rerun of the whole spec file, 1.8 minutes for fileslibreofficeedit against a 42 second first attempt, plus a flaky line in every report.
Retrying inside the helper keeps that cost to one login. The base entry point now makes a second attempt after a five second pause, which resolveTimeout stretches to twenty five over Tor, and the observed failures abort after thirty to forty two seconds against a nine hundred second budget, so the second attempt has room. A persistent failure still fails, and the first attempt's message is appended to the second rather than swallowed, which the old wrapper's bare catch did not do.
loginToStandaloneNextcloudWithRetry is gone: a helper of that name beside a base that retries is a trap, and its five callers now reach the same two attempts through the base. The other forty eight addon specs gain the retry without being touched.
Rejected alternatives, each against a written constraint: a serial globalSetup would have to live in the playwright.config.js that 87 roles share and would force ignoreHTTPSErrors into the central use block; PLAYWRIGHT_WORKERS=1 reverses the measured Tor tuning in playwright_workers.py and leaks through an unnamed include_vars into every later role; pre-creating the account collides with prevent_create_email_exists and turns a one-off flake into a permanent login refusal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Run 34421944328 Nextcloud#1 timed out "waiting for a signed-in Nextcloud
shell after the login redirect". 4164f37981 turned sso on in variant 1
while ldap stays off there, so that round logs in through sociallogin
instead of oidc_login - the first variant, on this branch or on main,
that runs sociallogin against an onion issuer. oidc_login goes through
Nextcloud's HTTP client, which honours the proxy in proxy.config.php;
sociallogin builds its own client and reads only
social_login_http_client['proxy'], so its token exchange never reached
the onion Keycloak. The template now sets the same tor SOCKS proxy for
it, inside the existing "sso on and the OIDC host is an onion" branch.
The spec only treated 5xx documents as a page failure, so a refused
callback surfaced as a 60s (x5 over tor) wait for a shell that could not
come. A 4xx on /apps/sociallogin/ or /apps/oidc_login/ now counts as
the page itself failing, reported without the query so no code leaks.
The plugin loop printed every item, including the OIDC client secret,
into the include line of a public CI log; it now labels by plugin key and
honours MASK_CREDENTIALS_IN_LOGS.
_shared.js grew past 250 lines; its page and modal helpers move to
_page.js and _shared.js re-exports them, with an identical export
surface.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The addon spec reached the per-user connected-accounts page, resolved the
"Connect to Mastodon" button and found it visible, enabled and stable, then
spent its whole 30s click timeout being refused:
<div id="firstrunwizard" class="first-run-wizard modal-mask modal-mask--opaque">
subtree intercepts pointer events
The single best-effort dismissal before the click is bounded and returns as
soon as two checks find no modal, so a wizard that appears late outlives it.
That the wizard, not the bridge, is the problem shows in the same job: the
spec passed eleven minutes later against the same stack, and passes in both
swarm rows.
The retry loop for exactly this interception already exists, written for the
user-menu click and named after it. Widen the name, take any locator, and use
it here. Nothing outside the shared module referenced the old name, so it is
renamed rather than aliased.
This does not turn the job green: it also fails the onlyoffice addon, which
is a separate cause.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
integration_mastodon asserted the provisioned oauth_instance_url resolved to
the literal "microblog.infinito.example". That value is not fixed: the addon
hook writes lookup('tls','web-app-mastodon','url.base') into it
(meta/addons/integration_mastodon.yml:12), so on an onion node the plugin
correctly received http://microblog.<onion>/ and the spec failed for being
right. The product was never wrong; the assertion was pinned to a clearnet
name.
The partner URL now reaches the spec the way every sibling bridge spec already
gets it, from the same lookup the hook uses:
playwright.env.j2 MASTODON_BASE_URL, gated on services.mastodon.enabled
_shared.js read and exported as mastodonBaseUrl
the spec compares host against host, not against a literal
web-app-fediwall already wires MASTODON_BASE_URL from this exact lookup
(templates/playwright.env.j2:12), so the name and the source are the
established ones; this adds the gate, so the variable renders empty instead of
naming an undeployed partner.
The guard in front of the comparison is not decoration: without it an empty
MASTODON_BASE_URL would reach new URL("") and abort the spec with a TypeError
instead of an assertion anyone can read.
The evidence that this is the whole difference: integration_peertube runs the
identical shape against shared.env.peertubeBaseUrl and PASSED in the same red
run, on the same onion node where integration_mastodon failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Covered by PR #380 rather than merged on its own. This branch already carries
the Tor onion-service provider across the whole role set, and #495 touches the
same nextcloud Playwright specs; landing them separately would mean resolving
the same conflicts twice, once here and once on main. Reviewing #495 as part of
#380 keeps the resolution in one place.
One conflict, in the external-sites addon spec. Nextcloud 34 renamed the admin
section from #external to #external-vue and replaced the add-site control with
a 'New site' button, so the incoming selectors win — retargeting them is what
the PR is for. This branch's resolveTimeout wrappers stay on both assertions.
make quality green on the merge result.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
waitForVisibleCandidate can report a failed document response instead of
a bare selector timeout, but it attached the listener itself, and its
first call comes after loginToStandaloneNextcloud's goto has already
consumed both the redirect and the response. Playwright delivers only
events that arrive after attachment, so the detector was structurally
blind to the one navigation it exists for.
Attach it before the goto. trackServerErrors is idempotent through its
WeakMap, so the later call is a no-op and the existing evaluation in the
wait loop is untouched.
A 500 login page now fails in ~130ms naming the endpoint, rather than
after 60s of waiting for selectors that cannot appear on an error page --
which is what pointed the recent OIDC investigation at the proxy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The external app's admin UI became a Vue app in v9.0.1 — the only
release declaring min/max-version 34, and the one CI installs. Its
template is now just <div id="external-vue"></div>, so the two hard
assertions on #external and #add_external_site (v8.0.1 markup) could
only time out. Match the mount point and the NcButton label instead;
both stay sharp, since an unmounted #external-vue has zero height and
the button exists only once the component ran.
Drop #app-content from the shared shell candidates as well: NC34 emits
that id in apps/settings/templates/help.php alone, a route no spec
visits. Dead alternatives inside an OR-list are how the login breakage
stayed invisible — #header-start__appmenu sat there long after Vue
started replacing its mount node.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nextcloud 34 moved the app links into the waffle popover, so
`a[href*="/apps/files"]` is absent until the menu is opened, and
`#header-start__appmenu` is replaced by `nav.app-menu` on mount in
33 and 34 alike. Both post-login markers were therefore gone and
every spec died in `loginToStandaloneNextcloud`, although the login
itself succeeded (POST /login 303, GET /apps/dashboard/ 200).
Match on the header chrome and the dashboard root instead — present
in both versions, absent from the guest layout, so the post-logout
assertion in test-login-biber-ldap.js stays sharp.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Scale the login helper's waitForVisibleCandidate call-sites and the Sign In / Log out clicks through resolveTimeout, and wait for network idle after logout so the slow keycloak end-session redirect chain over Tor settles before the logged-out state is probed.
Skip the Talk-admin and SeaweedFS-filer specs on onion targets (WebRTC HPB/STUN/TURN and a clearnet-only filer UI). Assert the XWiki partner host against XWIKI_BASE_URL from the env instead of a hard-coded clearnet host, so the check holds on both clearnet and onion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Provision a Doorkeeper OAuth application on the partner Zammad and write its client_id/client_secret into integration_zammad (Nextcloud-encrypted), so the per-user OAuth connect flow has the credentials the generic SPOT cannot supply. Register both the pretty-URL and /index.php oauth-redirect variants, and drop the hashed-secret fallback so a missing plaintext secret fails loudly instead of persisting an unusable hash.
Strengthen the integration_peertube spec: pin the admin 'instances' value to the exact deployed partner host (new PEERTUBE_BASE_URL) instead of merely 'not Nextcloud', and add a functional OCS search-providers check that fails when the PeerTube provider is not registered. PeerTube has no per-user OAuth (verified upstream: no client_id/oauth_instance_url config, no /oauth/authorize route), so the removed Connect->/oauth/authorize assertion tested a non-existent flow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- loginToStandaloneNextcloud signed-in-shell wait 60s -> 120s so cold
first-deploys do not time out mid-login
- export moodleBaseUrl from _shared and render MOODLE_BASE_URL /
DISCOURSE_BASE_URL (service-gated) for the moodle/discourse integration specs
- drop NEXTCLOUD_RICHDOCUMENTS_EXPECTED_WOPI_URL (richdocuments spec no longer
pins the WOPI host)
- strip narrative comments
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
NC 33's login form labels the username textbox simply "Account name"
(no "or email" suffix), so the prior regex matched nothing and every
login-via-native-form spec timed out at the 5-s default. Broaden the
pattern to accept either label shape.
The biber-LDAP scenario also failed at the logout assertion: the
universal-logout patch from sys-front-inj-logout rewrites the click
target to Keycloak's logout endpoint, which in user_ldap-only mode
ends on Keycloak's "You are logged out" info page rather than NC's
/login. Replace the negative shell-absence check with a positive
poll on the Keycloak logout URL that matches what the universal-logout
patch actually does.
meta/services.yml — switch `lookup('config', application_id, ...)` to the
spelled-out role id `'web-app-nextcloud'`. `application_id` is undefined
when meta/services.yml is rendered (the applications registry is built
before any role-vars binding), which made the lookup return the raw
unrendered Jinja string instead of the resolved oidc.enabled value, and
in turn flipped legacy_login_mask.enabled to false in variant 1 where
it should have been true. The matching `twofactor_*` plugin entries in
the same file already use the hardcoded form for the same reason.
files/playwright/_shared.js — anchor `nextcloudCredentialSubmitPattern`
to `^(sign in|log in)$` so it matches only the credential-form submit
button. With auth.webauthn.enabled=true (the post-fix variant-1 state)
Nextcloud renders an additional `Log in with a device` button next to
the credential `Log in`, which the previous unanchored pattern matched
too — strict-mode violation, test fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ESLint reported 2 errors after the playwright-spec split refactors:
- `playwright/no-wait-for-navigation` rule "not found" at the inline
disable directive in web-app-fediwall/_shared.js, because the
playwright plugin block was scoped to `playwright.spec.js` only.
- `enterNextcloudLoginThroughVisibleEntryPoint` undefined in the
web-app-nextcloud `_shared.js` module.exports — a refactor
(daefcf146) intentionally dropped the helper but left the export
reference behind.
Widen the playwright plugin glob to `roles/**/files/playwright/**/*.js`
so the same rule set applies to the now-common per-test submodules
(_shared.js, test-*.js splits), and remove the dangling export.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the Talk env vars, settings-text scraping helpers, spreed
test-button verifier, and the former `verifyNextcloudTalkAdminSettings`
flow out of `_shared.js` into `test-talk-admin-settings.js` so the gate
flag, env-presence guard, and assertion logic live in a single module.
Drop the redundant `verifyNextcloudTalkAdminSettings` calls from the
admin OIDC and native login specs — the dedicated Talk test now owns
that coverage. Add `talk: true` / `talk: false` variant pins for the
new dynamic `services.talk.enabled` flag so coverage stays complete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the taiga/pixelfed pattern: `playwright.spec.js` shrinks from a
834-LOC monolith to a 28-LOC orchestrator that wires `beforeEach` and
require()s one test module per scenario; all shared env, locator,
modal, login/logout and Talk-admin verification logic moves to
`_shared.js`. Each test stays atomar and individually inspectable via
its filename.
Login coverage is now persona × flavor instead of one monolithic
"nextcloud login" that implicitly assumed OIDC:
- test-login-admin-oidc.js skip \!oidcEnabled
- test-login-admin-native.js skip oidcEnabled (uses
NEXTCLOUD_DIRECT_LOGIN_PASSWORD
via the shared
login helper)
- test-login-biber-oidc.js skip \!oidcEnabled
- test-login-biber-ldap.js skip oidcEnabled || \!ldapEnabled (biber is
not autoprovisioned
in pure-native NC)
Persona scenarios (guest / biber / administrator post-login flows) keep
delegating to the shared `runGuestFlow` / `runBiberFlow` / `runAdminFlow`
helpers and stay universal across all variants.
Also drops an unused helper (`enterNextcloudLoginThroughVisibleEntryPoint`)
that the deleted always-on `test-nextcloud-login.js` was the only
consumer of.