You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(merge): organisation merge re-points nothing — probe a magic accessor with property_exists (#491)
* fix(merge): organisation merge re-points nothing — probe a magic accessor with property_exists
MergeOrganisatieService::repointBySelfOrganisation() decided whether an object
was owned by the source organisation with method_exists($entity,
'getOrganisation'). OpenRegister's ObjectEntity declares that accessor only as
an @method docblock tag over protected ?string $organisation, so it is served
by OCP\AppFramework\Db\Entity::__call() and the probe is always false. The next
line skipped every object, so contract and compliancy were never re-pointed
while tombstoneSource() still retired the source organisation — leaving live
objects owned by an organisation that no longer exists. Dry-run and execute
agreed only because both arms were equally broken.
The instrument is property_exists(), which is what Entity::getter() itself
decides on. is_callable() is not a membership test on a __call class — it is
true for every name, so a probe swap would make the branch unconditionally
true and move the failure into a runtime BadFunctionCallException. The
accessor call is wrapped and the result type-checked in the same edit.
The same probe in ReviewService::entityUuid() and IntakeService::entityUuid()
made both return null for every real save, because saveObject() returns an
object and the is_array() fallback cannot rescue it — so submit() answered
uuid: null to the client and wrote uuid: null to the audit log.
Why the suite was green: tests/Stubs/Db/ObjectEntity declared getOrganisation()
concretely, which inverted the exact predicate under test. The merge suite now
builds a faithful double — a concrete subclass of the stub, which extends the
real Entity, with organisation as a property reached through __call — and one
test asserts that premise so the fixture cannot drift back. The stub no longer
declares getOrganisation()/setOrganisation() and carries a warning about what
adding an accessor there costs.
Reverting only the merge probe turns 6 tests red; reverting only the two
entityUuid probes turns 2 red. Both predictions were written before the revert
and matched exactly. 667 unit tests pass; phpcs, phpmd, psalm and phpstan clean.
Also corrects a stale class docblock: it credited the @self.organisation write
path to SaveObject::applyCallerSuppliedFields(), a method that exists nowhere
in OpenRegister. The real acceptance path is SaveObject::setSelfMetadata().
Closes#490
* fix(tests): keep the ObjectEntity stub free-standing so it loads under both bootstraps
The previous commit made the stub extend OCP\AppFramework\Db\Entity. That is
fine under tests/bootstrap-unit.php, which registers an OCP autoloader, but
tests/bootstrap.php require_once's every file in tests/Stubs/ BEFORE
Nextcloud's lib/base.php — deliberately, so the stub wins over the real
OpenRegister class during mock generation. At that point no OCP class is
resolvable, so the whole suite died in the bootstrap with
Error in bootstrap script: Class "OCP\AppFramework\Db\Entity" not found
on both PHPUnit cells. The local unit run could not see it because
phpunit-unit.xml uses the other bootstrap.
The stub now mirrors Entity's __call/getter/setter triple instead of
inheriting it, so it has no load-time dependency at all. The semantics that
the fix turns on are reproduced exactly: get*/set* resolve through
property_exists(), anything else raises BadFunctionCallException.
Verified by replaying the exact failing bootstrap step — vendor/autoload.php
plus the tests/Stubs glob, with no Nextcloud and no OCP autoloader. The
committed version fatals there; this version loads clean. The revert
prediction is unchanged: reverting the merge probe still turns exactly the
same 6 tests red.
0 commit comments