Skip to content

Organisation merge repoints nothing — repointBySelfOrganisation() probes a magic accessor, so every object is skipped while the source org is tombstoned #490

Description

@rubenvdlinde

Summary

MergeOrganisatieService::repointBySelfOrganisation() reads the owning organisation with a method_exists() probe:

// lib/Service/MergeOrganisatieService.php:450
if (method_exists($entity, 'getOrganisation') === true) { $owningOrganisation = ...; }
...
if ($owningOrganisation !== $source) { continue; }

$entity is an OCA\OpenRegister\Db\ObjectEntity. getOrganisation() is an @method docblock served by Entity::__call(), so the probe is false, $owningOrganisation stays null, and the next line continues every object.

Measured

Live, against the server's own Entity.php:

subject method_exists is_callable
ObjectEntity::getOrganisation false true
ObjectEntity::getObject — concrete control true true

Consequence

repointBySelfOrganisation() always returns 0. Meanwhile tombstoneSource() still tombstones the source organisation, so contract and compliancy objects are left pointing at @self.organisation = <tombstoned org>. The merge reports success having moved nothing.

The parity check between dry-run and execute cannot catch this: both arms are equally broken, so they agree. This is the same shape as an A/B whose two arms share the defect.

Also in this repo, lower rank

ReviewService.php:371 and IntakeService.php:278 — same probe on getUuid, no working fallback (is_array() is false for an ObjectEntity), so both return null. submit() returns uuid: null to the client and writes ['uuid' => null] to the audit log.

Seven further getUuid probes in this repo (OrganizationContactSyncJob:221, MigrateContactsToNc:386, ModerationService:344, ReviewAggregateService:279, FederationService:659, EolSyncService:463) are dead code with no runtime effectObjectEntity::getObject() always injects the uuid under id, so their $data['id'] fallback already holds. Worth tidying, not worth prioritising.

Fix

is_callable() is not a membership test on a __call class — measured, it is true for any name and the call then raises BadFunctionCallException. So swapping the probe yields an always-true guard, and the call must be made exception-safe in the same edit.

The most robust route here is to read @self.organisation from jsonSerialize(), which is genuinely concrete.

Please reshape the double in the same PR

Whatever test currently covers the merge will be green because its double declares the accessor concretely — that is what happened in every other confirmed instance of this bug. A fix without reshaping the double proves nothing. Write down how many tests you expect to go red when the fix is reverted, then check.

decidesk/tests/Stubs/Db/ObjectEntity.php is the fleet's reference double: it extends Entity, declares only genuinely concrete methods, and documents why.

Fleet context and the full 18-repo table: fleet-board/findings/method-exists-sweep.md.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions