The shared OAuth-credential writer stored the RAW value via IAppConfig::setValueString(..., sensitive: true), but the integration apps (gitlab/openproject/suitecrm/zammad) read back via getValueString(lazy) + ICrypto->decrypt and expect an ICrypto ciphertext. The mismatch made decrypt throw 'Authenticated ciphertext could not be decoded' while rendering the GitLab Personal section, which 500'd the shared connected-accounts page and cascaded into the integration_discourse spec timing out on the missing #discourse_prefs mount. Encrypt each value with ICrypto and store it lazy + non-sensitive, mirroring the apps' own setEncryptedAppValue.
Verified in-container: getAdminClientId() now succeeds, a fresh variant-3 provision no longer returns a 500, and the integration_discourse spec passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The gitlab/openproject/suitecrm/zammad integration tasks gated their partner-running check on 'container exec <partner> true' (rc==0), which returns rc 127 on partner images lacking a true binary, wrongly skipping the integration. Switch to 'container inspect -f {{.State.Running}}' gated on stdout-trimmed == 'true' — the same probe already shipped for the mattermost integration in 8023e4b8b. inspect never enters the container, so the in-container PATH lookup that broke the old probe is irrelevant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The nextcloud->mattermost OAuth provisioning failed because the partner-running probe used 'container exec <mm> true' (rc 127 — no true binary in the mattermost image), which gated provisioning off and left the OAuth app unprovisioned (spec failed on oauth_instance_url=null). Replace it with 'container inspect -f {{.State.Running}}' gated on stdout-trimmed == true, provision the OAuth app via a controller-run script using a short-lived admin PAT, and rewrite the spec as a behavioral connect-redirect check.
Mattermost personal access tokens are now enabled only when the consumer web-app-nextcloud is deployed (services.mattermost.user_access_token_enabled renders from 'web-app-nextcloud' in group_names) instead of being hardcoded on.
Verified: probe live in the DiD stack (inspect running -> rc0, missing -> graceful skip); the user-access-token chain renders True/False via a real Templar (nextcloud present -> True, absent -> False); service-flag integration tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
matomo: force-restart the swarm service after patching config.ini.php DB creds (the compose-up handler is a no-op in swarm), extracted into 10_resolve_host_cid. nextcloud: gate the redis PONG wait on the engine being local. prometheus: config 0444 (swarm config objects are root) and gate the host-monitor binds plus privileged cadvisor/node-exporter to compose only. dashboard: probe the public icon URL in swarm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The integration_zammad Connect flow reached the partner /oauth/authorize with a malformed client_id, so Zammad's Doorkeeper rejected the unknown client UID and 302-redirected to its homepage (the spec then captured the partner root, not the authorize page). The encrypted-appconfig helper stored the credentials via setValueString(..., lazy: true) WITHOUT sensitive: true, which mangled the client_id on read. Store it sensitive so the provisioned client_id round-trips intact and the OAuth authorize carries the correct UID.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nextcloud runs pretty-URLs (front_controller_active), so each integration app emits its oauth-redirect WITHOUT /index.php at runtime. The partner OAuth apps were registered with only the /index.php variant, so exact-match validation rejected the real callback and the per-user connect silently failed while the deploy and Playwright specs went green. Register both variants (the no-/index.php one first). Build the list on the consumer side (Doorkeeper newline-join for gitlab, native JSON callback_urls array for mattermost, Doorkeeper for openproject) to avoid newline-in-argv.
SuiteCRM is unaffected (OAuth2 password grant, no redirect_uri); the Nextcloud->OpenProject client callback is an OpenProject URL and is left untouched. Also drop the Doorkeeper>=5 hashed-secret fallback in the gitlab provisioner so a missing plaintext secret fails loudly.
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>
The personal-security enable control for twofactor_nextcloud_notification
only renders once the provider is activatable, which needs a SECOND active
session to receive the approval notification — impossible in a single-session
E2E (the provider state is correctly false there). The addon hook already
proves registration via occ twofactorauth:state; the browser-observable
equivalent is that the provider state element is surfaced, so assert
toBeAttached, not toBeVisible. Verified in-container: nextcloud v1 suite
31 passed, 0 failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OnlyOffice declares fileslibreofficeedit (and richdocuments) incompatible and
disables them on install, so the LibreOffice editor's E2E spec failed wherever
OnlyOffice was co-deployed. Enable fileslibreofficeedit only when the OnlyOffice
addon is off (addons.onlyoffice.enabled), and add the required service-coupled
addon integration hook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mariadb.enabled is a static literal in meta/services.yml, so variants must not
pin it (test_variants_no_static_override). Regenerate variants.yml overriding
only the dynamic flag per service (mariadb keeps shared-only). With mariadb
correctly always-on and flowise+openwebui sharing ollama, the bond<1 partners
repack into 4 bins -> 5 variants (v0 coupled baseline + 4 partner bins), each
under the 73GB/37GB host budget.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test_order now only pulls bond>=1 (tightly-coupled, same-host) services into a
role's closure; bond<1 partners deploy on their own host and are excluded, so a
later variant may enable them without variant 0 having to list them.
New test_bond_baseline enforces the placement: a bond>=1 dynamic flag must be
pinned true in variant 0 (the host baseline); a bond<1 flag must be pinned true
in at least one variant. Escape: '# nocheck: bond-baseline'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An addon's enabled flag must defer to the owning role's service flag
(lookup config services.<x>.enabled), a literal true/false, or api lookups —
never a raw '<role>' in group_names test. group_names belongs in
meta/services.yml (the SPOT); addons read the resolved flag so one source
decides partner presence. New lint scans roles/*/meta/addons/*.yml (87 files)
with a '# nocheck: addon-enabled-group-names' escape.
deck.yml is the one legitimate exception (anti-dependency on web-app-taiga,
which has no nextcloud service flag) and is nochecked accordingly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
integration_moodle has no Nextcloud-33-compatible release on the app store
(absent for nc 30/32/33) and integration_suitecrm requires nc <= 24, so both
bridges cannot work on the deployed Nextcloud 33. Set enabled/shared to false
(nocheck: dynamic-flag, like minio) so neither partner is pulled or bridged,
with a TODO to re-enable once a compatible plugin ships. Verified against the
Nextcloud app store nc-33 compatibility list; all other bridges (discourse,
matrix, peertube, mattermost, mastodon, zammad, openai, openproject, gitlab,
github, google, jira, bbb, xwiki) have real nc-33 releases and stay enabled.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Declare the 14 services that consuming roles reference but no role provided
(bigbluebutton, xwiki, openproject, gitlab, mattermost, matrix, zammad,
openwebui, flowise, peertube, moodle, suitecrm, redis, memcached) as shared
providers: set enabled/shared/bond on each entity so the service registry
registers them and the dangling references resolve.
Add test_shared_service_provider.py (lint): fail when a referenced shared
service has no provider and its name matches a role's entity/provider that does
not register it (suppressable via '# nocheck: shared-provider').
Guard the 9 web-app shared services with the run-once 01_core.yml pattern
(main.yml includes 01_core.yml only when run_once_<role> is undefined; the core
flags itself via utils/once/flag.yml) so they deploy once when pulled in by
multiple consumers. svc-db-redis/memcached are template-only (no runtime tasks).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`bond` is a 0..1 factor for how strongly a service is tied to the overall
integration: bond: 1 = always required, bond: 0 = omittable. Groundwork for
tooling that later scores service importance and decides what can be dropped.
Named `bond` (not `bound`) to avoid clashing with the resource bounds/limits.
Add test_service_bond.py (lint): every enabled + shared service must declare a
valid `bond` (number in 0..1). Seed `bond: 1` as a placeholder on all 638
enabled+shared service entries across 88 roles, to be refined later.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mbake line-wrap of the long configvalue jinja expression. Cosmetic only;
the rendered value is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Leftover 'if (popup) await popup.close()' referenced a variable removed
together with the connect-button/popup OAuth flow -> eslint no-undef ->
lint-javascript red -> make test red. make test is green again. No change
to the spec's assertions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OpenProject's integration_openproject renders no 'Connect to OpenProject'
button on connected-accounts (unlike discourse/gitlab/mattermost); the
connection happens via the Files smart-picker / the OpenProject side. The
spec's connect-button + OAuth-authorize popup flow tested non-existent UI.
Assert the real provisioned bidirectional coupling instead, read from the
admin initial-state config: openproject_instance_url (partner host),
openproject_client_id, openproject_client_secret, authorization_method=oauth2.
NOTE: NOT yet verified green live -- the compose-playwright verify run was
OOM-killed (exit 137) before finishing. High confidence by logic only: in the
prior run this spec already passed the instance-URL check (same JSON.parse
path), failing only on the now-removed connect-button assertion, and all
asserted fields are confirmed present in the decoded admin config. Re-run a
scoped verify to confirm before relying on it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
integration_gitlab/integration_mattermost read client_id/client_secret via
crypto->decrypt(appConfig->getValueString(lazy:true)). Provisioning set them
with 'config:app:set --sensitive', the wrong scheme -> "Authenticated
ciphertext could not be decoded" -> the connected-accounts admin page 500s,
cascading onto the discourse/matrix/mattermost addon specs.
Add set_encrypted_appconfig.php (crypto->encrypt + appConfig setValueString
lazy -- the exact scheme the apps' setEncryptedAppValue uses) and route both
provisioning hooks through it. Verified live: gitlab client_id is now an
encrypted blob and connected-accounts renders again.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- discourse-activity-pub, integration_matrix specs: drop unused catch
binding and useless json init (eslint no-unused-vars/no-useless-assignment)
- openwebui playwright.env.j2: order OLLAMA_SERVICE_ENABLED after
OIDC_ISSUER_URL (env keys must be ASC, test_alphabetical_sort)
- test_addon_env_flags.py: autoformat
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The drawio/files_bpm/external/fileslibreofficeedit/whiteboard addon specs
asserted guessed routes/selectors that do not exist on the live stack.
Verified against the running Nextcloud (make compose-playwright):
- drawio, fileslibreofficeedit: no standalone app route -> assert the app
is registered+enabled in OC.appswebroots (Files file-action apps)
- files_bpm: modeler surface is #bpmn-app, not #canvas.bpmn-canvas
- external: admin section is #external + #add_external_site
- whiteboard: backend URL is injected in #initial-state-whiteboard-url
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- svc-ai-ollama variant-0: preload smollm2:135m (smallest instruct model for CI)
- integration_openai meta: Service URL -> {base}/api for the OpenWebUI backend
- web-app-openwebui: enable_api_keys config flag (services.yml + vars) rendered into
ENABLE_API_KEYS so sk- API keys authenticate against the OpenAI-compatible API
- integration_openai spec: drop the DOM-gated skip, assert the partner backend host
(openwebui chat.ai / flowise flow.ai), distinct from Nextcloud
- openwebui: OLLAMA_SERVICE_ENABLED playwright flag + test-ollama-chat.js asserting
the preloaded model answers a chat completion over OpenWebUI -> Ollama
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the DOM-gated skip; assert the xwiki admin instances table renders (route not
404), no 'no wikis registered' placeholder, and a configured instance-url input
whose host is the XWiki partner, not Nextcloud.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the generic Files-app check with the app's own settings/admin/whiteboard
section and assert the collab-backend URL field is present and non-blank, proving
the plugin_configuration reached the running app.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Keep the deterministic config coupling (OCS spreed signaling capability + the Talk
admin TURN server pinned to the web-svc-coturn partner host, STUN server, signaling
host) with a best-effort live 'Test this server' check; drop the comment blocks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assert the admin section renders, the wopi_url field carries a real partner URL
whose host is distinct from Nextcloud, and the Collabora connection-failure banner
stays absent, proving the connector reached the partner WOPI server.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assert the recognize admin Vue component (#recognize) and its own Face-recognition
section render, not the generic settings shell.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the config-presence check with a real round trip: upload a valid .docx,
open it, and assert the ONLYOFFICE editor iframe is served by the web-svc-onlyoffice
partner host, shows no JWT/download error, and renders its editor surface.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assert the metadata app is registered in OC.appswebroots and its own frontend
bundle (apps/metadata/...) is injected into the Files page, proving the provider is
loaded and coupled, not just listed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the best-effort early-return; assert the admin instance URL is the partner
(not Nextcloud) and the per-user connect drives the partner /oauth/authorize with
the provisioned client_id + response_type=code.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the DOM-gated skip; assert the admin panel exposes the provisioned partner
instances list (distinct from Nextcloud) and the per-user connect drives the OAuth
authorize on the partner instance with the provisioned client_id + response_type=code.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assert the groupfolders admin Vue app (#groupfolders-wrapper/#groupfolders-root)
mounts, not the generic settings shell, proving the app is installed and enabled.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Target the app's own settings/admin/fileslibreofficeedit route (not 404) and assert
its LibreOffice editor admin panel and document-editor server URL field render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Open the Files New menu and assert the 'New link (.URL)' and 'New link (.webloc)'
entries the app injects are present, proving its frontend bundle loaded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Target the app's own apps/files_bpm/ route (not the generic Files app) and assert
it resolves (not 404) and the #canvas.bpmn-canvas modeler surface renders.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Target the app's own settings/admin/external section and assert its description
text and 'New site' action render, proving the external app's section and template
are served by the enabled app (not the generic additional-settings shell).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app is backend-only (no user route); assert via the Provisioning OCS registry
that event_update_notification is in the enabled-apps list and not in the disabled
list, so a disabled/absent app fails.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the DOM-gated skips; assert the settings/user/epubviewer section resolves
(not 404), the #reader-personal panel renders, and the #EpubEnable checkbox
reflects the persisted user config.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the DOM-gated skips; assert the apps/drawio/ route resolves (not 404) and the
drawio-specific editor surface mounts, so a disabled/broken app fails.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Navigate to apps/bbb/ and assert the cloud_bbb app shell + its own room-management
surface render, in addition to the admin api.url/api.secret partner coupling.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Push the OpenProject OAuth credentials (authorization_method, openproject_client_id,
openproject_client_secret) into Nextcloud before the NC OAuth2 client step so the
forward connect is always provisioned. The reverse-direction Nextcloud OAuth2 client
relies on occ oauth2:add-client, which Nextcloud 33 does not ship, so it is now
failed_when: false with its dependents in a guarded block — the deploy completes
instead of aborting before the Playwright suite runs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The integration_openproject / integration_gitlab provisioning and the user_ldap
config probe read their state with tasks that always run (no when:) and
failed_when: false, so their .stdout is always defined; 'X.stdout | default('') |
trim' is identical to 'X.stdout | trim'. Remove the dead default('').
Genuinely-guarding defaults are kept: the s01 config-id fallback, the skippable
ldap auto-create stdout/stderr, the regex_search None guards, and the outer
default(..., true) fresh-or-persisted secret fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OpenProject only returns a fresh OAuth client secret when the application is first
created; on a re-deploy where the app already exists but Nextcloud never persisted
the secret (e.g. an earlier deploy failed downstream at the storage step), the
secret is unrecoverable and the provisioning assert tripped, aborting the play
before the Playwright suite ran.
ensure_nextcloud_storage.rb now destroys and recreates the OAuth application when
it exists but Nextcloud holds no secret (gated to the first call via the absent NC
client id), so a fresh secret is always emitted. The provision task passes the
persisted-secret read as NC_HAS_OP_SECRET only on that first call so the second
(client-register) call never destroys the freshly created app. Verified
in-container: the recovery path emits a fresh openproject_client_secret.
Also drops the redundant | default('') on the always-set
nextcloud_op_client_secret_current.stdout in this file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drive the real bridge: pin the per-user #moodle-url to the deployed partner
Moodle host (distinct from Nextcloud), persist it, then Connect and assert the
POST /apps/integration_moodle/get-token response is a genuine partner verdict
(200 with user_name minted by the partner webservice, or 401 partner-refused) —
proving the request reached the partner Moodle, not a local stub. bridges:
[moodle] means the deployment gate skips it when the partner is not deployed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assert the admin oauth_instance_url points at the partner Mattermost host (not
Nextcloud) and the admin panel exposes the provisioned OAuth client, and the
per-user connect drives an OAuth authorize redirect to that partner host with
the provisioned client_id + response_type=code. Backed by the
integration_mattermost_provision hook; bridges: [mattermost] with the partner in
the deploy set means this runs live and fails if provisioning or the connect is
broken.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the DOM-gated skip; assert the admin Matrix panel renders, the configured
homeserver is the deployed partner (distinct from Nextcloud), and the per-user
connect drives a real session against the partner homeserver (a browser
/_matrix/client/ call to the partner, or the nextcloud integration_matrix
connect round-trip). bridges: [matrix] with the partner in the deploy set means
this runs live and fails on a broken coupling instead of skipping on the DOM.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>