Skip to content

fix(listeners): method_exists() cannot see OpenRegister's magic accessors — the slug resolver was dead - #185

Merged
rubenvdlinde merged 1 commit into
developmentfrom
fix/method-exists-magic-accessors
Aug 11, 2026
Merged

fix(listeners): method_exists() cannot see OpenRegister's magic accessors — the slug resolver was dead#185
rubenvdlinde merged 1 commit into
developmentfrom
fix/method-exists-magic-accessors

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

What

ObjectSchemaSlugResolver guarded all three of its accessor calls with method_exists(). OpenRegister's ObjectEntity, Schema and Register extend Nextcloud's Entity, which serves every column accessor through __call() and declares it as an @method docblock only. So the probe was false for getSchema(), getRegister() and getSlug() on the real classes, schemaSlug() returned an empty string for every real entity, and isOpenBuildSchema() was false even for openbuild's own objects.

Measured on this instance: the probe is false while is_callable is true for those three, against the concrete control getObject() which is true for both.

That silently disabled the two listeners the resolver exists to serve — ProductionVersionGuardListener and AutomationCleanupListener.

Blast radius is unchanged by this PR

Both listeners sit behind ListenerSlugContract, which defaults off. Default behaviour is therefore identical. What changes is that turning the flag on now actually wakes them, instead of silently keeping them off — which is the opposite of what a rollout flag is for. Waking them remains a rollout decision and is not taken here.

Whole file, not the reported line

The file's own header already documented the neighbouring trap — that getSchemaSlug() does not exist and that the event carries ids rather than slugs — while all three of its own guards carried this one. The fix is applied at the single private seam the three call sites funnel into.

is_callable() is not a membership test on a __call class; it is true for any name. So the helper is exception-safe, because Entity raises for a column it does not have.

ProductionVersionGuardListener::extractUuid() carried the same defect on getUuid() and is fixed the same way.

Why the suite never caught it

tests/stubs/openregister-stubs.php declares these accessors concretely, for a real reason: PHPUnit 10 removed addMethods(), so a generated mock cannot configure a magic method. That stub inverts the exact predicate under test — the probe is true in the suite and false in production. The stub is left alone, because its reason holds.

The new test instead extends the real OCP\AppFramework\Db\Entity, so its accessors are reached the way production reaches them, and it carries a concrete-method control so a false result cannot come from a broken fixture.

Prediction and control

Predicted before running: reverting the guard at the shared seam turns exactly three of the seven new tests red. Observed exactly those three; the other four stayed green, including the fail-closed test that rejects a same-named schema in another register.

Suite goes from 797 to 804 tests, all green. lint, phpcs, phpmd, psalm and phpstan clean. Gates were re-run after committing, so the diff-scoped gates inspected a real three-file scope rather than an empty one.

Also found while here, deliberately not fixed

AutomationApprovalTriggerListener::schemaOf() and DocumentGenerationListener::schemaOf() carry the same probe, but fixing it alone would not wake them: getSchema() returns the numeric schema id, and both compare it against a slug literal. Routing them through this resolver is the real fix and is a rollout decision of the same kind the contract flag governs, so it is filed rather than guessed at.

…sors — the slug resolver was dead

ObjectSchemaSlugResolver guarded all three of its accessor calls with
method_exists(). OpenRegister's ObjectEntity, Schema and Register extend
Nextcloud's Entity, which serves every column accessor through __call() and
declares it as an @method docblock only. method_exists() is therefore false for
getSchema(), getRegister() and getSlug() on the real classes, so every guard took
its false branch, schemaSlug() returned '' for every real entity, and
isOpenBuildSchema() was false even for openbuild's own objects.

Measured on this instance: method_exists false / is_callable true for those three,
against the concrete control getObject() which is true for both.

That disabled the two listeners the resolver exists to serve —
ProductionVersionGuardListener and AutomationCleanupListener. Both sit behind
ListenerSlugContract, which defaults off, so default behaviour does not change
here: what changes is that enabling the flag now actually wakes them instead of
silently keeping them off. Waking them remains a rollout decision, unchanged.

The file's own header already documented the neighbouring trap (getSchemaSlug()
does not exist and the ids are not slugs) while all three of its guards carried
this one, so the fix is applied at the single private seam they funnel into
rather than at the reported line.

is_callable() is not a membership test on a __call class — it is true for any
name — so readAccessor() is exception-safe; Entity throws BadFunctionCallException
for a column it does not have. ProductionVersionGuardListener::extractUuid()
carried the same defect on getUuid() and is fixed the same way.

Why the suite never caught it: tests/stubs/openregister-stubs.php declares these
accessors concretely, for a real reason (PHPUnit 10 removed addMethods(), so
createMock()->method('getUuid') cannot configure a magic method). That stub
inverts the exact predicate under test — method_exists is true in the suite and
false in production. The new test therefore extends the real
OCP\AppFramework\Db\Entity so its accessors are reached the way production
reaches them, and carries a concrete-method control so a false cannot come from a
broken fixture.

Negative control, predicted before running: reverting the guard at readAccessor()
turns exactly 3 of the 7 new tests red. Observed exactly those 3
(testSchemaSlugResolvesThroughMagicAccessor,
testRegisterSlugResolvesThroughMagicAccessor,
testIsOpenBuildSchemaMatchesOpenBuildsOwnObject); the other 4 stayed green.

Suite 797 -> 804 tests, all green. lint/phpcs/phpmd/psalm/phpstan clean.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openbuild @ f7f4c32

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
test-l10n
check-gitignore
check-nc-floor
composer ✅ 101/101
npm ✅ 654/654
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-11 22:36 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Merging with E2E and Quality Report red. Both are pre-existing and neither is caused by this diff.

The E2E job fails on exactly two tests, and the same two fail identically on development runs 31520347084, 31459739568 and 31431360507 — same spec, same line numbers, same test ordinals, same locator timeout. They are the REQ-AUTD-008 RBAC scenarios already tracked in #173, which records that automation CRUD is admin-only at the OR layer so neither an editor nor an owner can author or enable one. openbuild's E2E has been red on development for eight consecutive runs; the most recent one failed even earlier, at Checkout additional apps, so it ran no tests at all.

Quality Report is a pure aggregator over the other jobs and carries no independent signal — it is red because E2E is.

Everything this change could plausibly affect is green: Hydra Gates pass, all four PHPUnit cells pass, phpcs, phpmd, psalm, phpstan, both Security jobs and both Vue Quality jobs pass.

Read the gates green narrowly. COVERAGE: 34 of 64 declared gates reported a result, 34 of 34 applicable ran, over a diff of 3 changed files against origin/development. That is a real verdict on this change and not a statement about the repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants