feat(settings): add canonical PUT /api/settings (settings#update) - #464
Merged
Conversation
`PUT /api/settings` answered **405 Method Not Allowed** on the dev instance,
not 500: softwarecatalog does not call `\OCA\OpenRegister\AppHost\Routes::standard()`
and declared no `settings#update`, so the PUT verb simply had no route.
Because the app ships its own `SettingsController`, AppHost's
`aliasControllerUnlessLeafDefinesIt()` skips the generic controller and the
leaf owes every `settings#` method itself.
Strict addition — nothing removed:
- `SettingsController::update()` takes the former `create()` body verbatim.
It writes exactly the three sections `index()` reads back via
`SettingsService::getAllSettings()`: `configuration`/`selectedRegister`,
`userGroups.{generic,organizationAdmin,superUser}` and `emailSettings`.
It is deliberately NOT a catch-all: the ~35 other `getXConfig`/`updateXConfig`
pairs, email templates, ArchiMate import/export, progress and stats keep
their own endpoints untouched.
- `create()` becomes `return $this->update();` and keeps its own
`@NoCSRFRequired` tag — NC middleware only evaluates attributes on the
dispatched method, so delegation cannot inherit the posture.
- `appinfo/routes.php` gains `settings#update` PUT `/api/settings`, before the
SPA `/{path}` catch-all. Route and method land together so the method is
never unreachable (gate-14).
Auth: `create()` declares `@NoCSRFRequired` and deliberately not
`@NoAdminRequired`, so NC requires an administrator. `update()` mirrors that
exactly; net privilege change is zero. A test pins the parity in both
directions and asserts `update()` can never gain `@NoAdminRequired`/`@PublicPage`.
Tests: 17 new assertions-heavy cases across two files, covering per-method
existence (item, not container) with a positive control, the evaluated route
table, the write behaviour, the non-catch-all guarantee, the 400 group-validation
path, exception mapping, and that `create()` delegates rather than duplicates.
New statements are 17/17 covered.
Contributor
Quality Report — ConductionNL/softwarecatalog @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue-demi | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 128/128 | |||
| npm | ✅ | ✅ 718/718 | |||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-08 16:26 UTC
Download the full PDF report from the workflow artifacts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is a 405 / missing-route case, not the fleet-wide 500
The fleet brief claimed
PUT /api/settings"resolves to a nonexistent method" in 12 apps. That is not what softwarecatalog does. Measured on the running dev instance (2026-08-08,curl -s -o /tmp/out -w "%{http_code}",-u admin:admin):/apps/softwarecatalog/api/settingssettings#indexsettings#create405, not 500.
appinfo/routes.phpdoes not call\OCA\OpenRegister\AppHost\Routes::standard()and declared nosettings#update, so the PUT verb simply had no route — the router rejected it before any controller was reached. And because the app ships its ownlib/Controller/SettingsController.php, AppHost'sBootstrap::aliasControllerUnlessLeafDefinesIt()skips the generic controller, so the leaf owes everysettings#method itself.Did the canonical shape actually fit?
Yes, cleanly — better than expected for a ~70-method controller.
create()'s body was already decomposed into exactly three private helpers, and those three correspond one-for-one to whatindex()reads back. The move was verbatim.What
update()writesExactly the three sections
index()surfaces throughSettingsService::getAllSettings():configuration/selectedRegisterSettingsService::updateSettings()userGroups.{generic,organizationAdmin,superUser}validateGroups()→ matching setter (400 on invalid)emailSettingsSettingsService::updateEmailSettings()Surfaces deliberately NOT absorbed
update()is not a catch-all. Every one of these keeps its own URL and its own handler, untouched:getGeneralConfig/updateGeneralConfig,getSyncConfig/updateSyncConfiggetArchiMateConfig/updateArchiMateConfig,getEmailConfig/updateEmailConfiggetAmefConfig/updateAmefConfig,getVoorzieningenConfig/updateVoorzieningenConfiggetUserGroupsConfig/updateUserGroupsConfig,getCronjobConfig/updateCronjobConfiggetEolSyncConfig/updateEolSyncConfig, email templates, ArchiMate import/export/download, progress streaming, stats, object countsA dedicated test (
testUpdateIsNotACatchAllForOtherSettingsSurfaces) postscatalogLocation/syncTimeWindow/cronjobs/eolSyncand asserts nothing is persisted.Auth decision
create()declares@NoCSRFRequiredand deliberately not@NoAdminRequired— so NC's security middleware requires an administrator.update()mirrors that exactly. Net privilege change: zero.create()was read.create()keeps its own@NoCSRFRequiredeven though it now delegates — the middleware evaluates attributes on the dispatched method, so delegation does not inherit them.create()had no in-body guard, so nothing needed to move.testUpdateHasIdenticalAuthPostureToCreatepins parity in both directions and additionally assertsupdate()can never gain@NoAdminRequired/@PublicPage, in docblock or attribute form.Can-fail proof
Both halves were removed with the Edit tool (not
git stash/git checkout --) and shown RED.1.
update()removed →EXIT=22. Route line removed →
EXIT=13. Both restored →
EXIT=0Full unit suite:
Tests: 512, Assertions: 1814, Deprecations: 2, Skipped: 25—EXIT=0. The 2 deprecations are pre-existing (the 17 new tests run clean).Tests
tests/Unit/Controller/SettingsControllerCanonicalWriteTest.phpandtests/Unit/SettingsRouteTableTest.php:$inspected > 0, route table > 50 entries,settings#routes matched) so a scan that matched nothing cannot report green.appinfo/routes.php— a commented-out entry would satisfy a grep but fails here.settings#route asserted to target an existing public method (gate-14 in miniature)./{path}catch-all.update()writes all three sections; returns the identical payloadcreate()used to; 400 on invalid groups; 500 mapping;create()delegates structurally (return $this->update();, and does not containupdateConfigSettings).Coverage: new statements 17/17 covered, none uncovered. Repo line coverage measured at 13.37% (pcov, 4239/31712) vs the committed CI baseline of 13.01 — the ratchet moves up, not down.
Gates
run-hydra-gates.sh --scope-to-diff --base origin/development(gates taken fromConductionNL/.github@origin/main; the local.githubcheckout was 77 commits behind and was not used). Verdicts read from stdout, not the exit byte:Zero FAILs. 57 gates green. Including gate-1 spdx, gate-5 route-auth, gate-9 semantic-auth, gate-14 route-reachability, gate-16 spec-coverage, gate-46 spec-anchor-existence, gate-47 security-change-has-tests, gate-48 csrf-cochange, gate-64 apphost-autoload-prelude.
3 applicable gates did not run (structural skips, honestly reported — not passes): gate-19 e2e-coverage, gate-62 store-plane, gate-63 settings-surface. All three skipped because this diff touches no spec file and no manifest, which is their subject matter.
The
@specanchor…method-decomposition/spec.md#requirement-settingscontroller-settings-crud-endpoints-req-decomp-013was verified against the real gate-46 checker, with a positive control proving the checker reports a bogus anchor.A full-repo (unscoped) gate run shows 17 pre-existing failures (a11y debt, 291 spec scenarios missing
@e2e, etc.). None are attributable to this change: gate-46's 4 findings are all inSbomImportService/SbomParserService/suiteWizard.js, andsettings#updatedoes not appear in gate-25's contract-coverage log.Also clean:
php -l, PHPCS (phpcs.xmlscopeslibonly) and Psalm on the changed controller.Not merged
Left open for the orchestrator.