From adaf375d58439b6ea7e95d834dd32260369dbb35 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Fri, 2 Oct 2026 06:15:39 +0200 Subject: [PATCH 1/2] fix(tests): load the ObjectCreatedEvent stub after Nextcloud boots The stub extends OCP\EventDispatcher\Event and sat one level under tests/Stubs, so the early stub glob in tests/bootstrap.php required it before lib/base.php made OCP resolvable. Every CI PHPUnit leg died in the bootstrap with Class "OCP\EventDispatcher\Event" not found before any test ran. Move it two levels deep, out of the glob's reach, and load it after the boot when OCP resolves and the real OpenRegister event does not. --- .../Event/ObjectCreatedEvent.php | 0 tests/bootstrap-unit.php | 2 +- tests/bootstrap.php | 14 ++++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) rename tests/Stubs/{ => OpenRegister}/Event/ObjectCreatedEvent.php (100%) diff --git a/tests/Stubs/Event/ObjectCreatedEvent.php b/tests/Stubs/OpenRegister/Event/ObjectCreatedEvent.php similarity index 100% rename from tests/Stubs/Event/ObjectCreatedEvent.php rename to tests/Stubs/OpenRegister/Event/ObjectCreatedEvent.php diff --git a/tests/bootstrap-unit.php b/tests/bootstrap-unit.php index 3ba8c6368..763f89fd0 100644 --- a/tests/bootstrap-unit.php +++ b/tests/bootstrap-unit.php @@ -58,7 +58,7 @@ 'OCA\\OpenRegister\\Db\\' => __DIR__ . '/Stubs/Db/', 'OCA\\OpenRegister\\Service\\' => __DIR__ . '/Stubs/Service/', // A copy of OpenRegister's ObjectCreatedEvent, so listener tests construct the real shape. - 'OCA\\OpenRegister\\Event\\' => __DIR__ . '/Stubs/Event/', + 'OCA\\OpenRegister\\Event\\' => __DIR__ . '/Stubs/OpenRegister/Event/', ]; foreach ($prefixMap as $prefix => $dir) { diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 09bd3bc33..9d39d07ed 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -189,3 +189,17 @@ function stackiq_nc_root_is_installed(string $ncRoot): bool } unset($integriqStubEvent); + +// OpenRegister's ObjectCreatedEvent, deferred for the same reason. It extends +// OCP\EventDispatcher\Event, and nextcloud/ocp ships no autoload section, so +// OCP only becomes resolvable once lib/base.php has run above. Loading it in +// the early tests/Stubs glob killed every CI PHPUnit leg in this bootstrap +// with `Class "OCP\EventDispatcher\Event" not found` before a single test ran. +// It sits two directories deep so that glob cannot reach it. When the real +// OpenRegister is enabled, class_exists() loads the real event and the stub +// stays out of the way. +if (class_exists('\\OCP\\EventDispatcher\\Event') === true + && class_exists('\\OCA\\OpenRegister\\Event\\ObjectCreatedEvent') === false +) { + require_once __DIR__ . '/Stubs/OpenRegister/Event/ObjectCreatedEvent.php'; +} From 754aaf2aeecedcbde97c7bf67feb42e036ee3cc0 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Fri, 2 Oct 2026 06:32:41 +0200 Subject: [PATCH 2/2] test(cmdb-import): pin the owner-privacy guarantee as it stands after publication field rules With the bootstrap fixed, CI ran stackiq's unit tests for the first time in a while and found two merged PRs disagreeing: #1209's test said usage may have no public read rule, while #1206 gives usage a public rule conditional on publicationDate. Owners stay private either way: contactPerson has no public rule, the import never sets a usage's publicationDate, and usage.businessOwner, technicalOwner and contactPerson carry authenticated-only property rules. The test now pins exactly that, and fails if any of those person properties is made public (control run: businessOwner set to public fails it). --- .../Settings/CmdbPersonDataVisibilityTest.php | 61 ++++++++++++++----- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/tests/Unit/Settings/CmdbPersonDataVisibilityTest.php b/tests/Unit/Settings/CmdbPersonDataVisibilityTest.php index ccb8d6f25..487a3197e 100644 --- a/tests/Unit/Settings/CmdbPersonDataVisibilityTest.php +++ b/tests/Unit/Settings/CmdbPersonDataVisibilityTest.php @@ -6,10 +6,11 @@ * The import links the application owner as a `contactPerson` through * `usage.businessOwner`. Anonymous visitors (OpenCatalogi search, Portaliq, * the OpenRegister objects API) must not see that person. This test pins the - * register configuration that guarantees it: neither `usage` nor - * `contactPerson` has a public read rule in the merged register (base plus - * every register.d fragment), and a published `module` only refers to them - * by relation, so a public search hit carries at most ids. The rig check of + * register configuration that guarantees it, in the merged register (base plus + * every register.d fragment): `contactPerson` has no public read rule, a + * `usage` is public only once its publicationDate has passed and its person + * properties never are, and a published `module` only refers to them by + * relation, so a public search hit carries at most ids. The rig check of * the running stack is the API test in * tests/e2e/spec-coverage/cmdb-import.spec.ts. * @@ -73,22 +74,52 @@ private static function isPublic(mixed $rule): bool { }//end isPublic() /** - * Neither usage nor contactPerson can be read anonymously. + * A contactPerson can never be read anonymously. * * @return void */ - public function testUsageAndContactPersonHaveNoPublicReadRule(): void { - $schemas = $this->mergedRegister()['components']['schemas']; - - foreach (['usage', 'contactPerson'] as $schema) { - $read = ($schemas[$schema]['authorization']['read'] ?? null); - $this->assertIsArray($read, $schema . ' must have an explicit read rule; without one OpenRegister does not restrict reads'); - $this->assertNotEmpty($read, $schema); - foreach ($read as $rule) { - $this->assertFalse(self::isPublic(rule: $rule), $schema . ' has a public read rule: imported owners would be readable anonymously'); + public function testContactPersonHasNoPublicReadRule(): void { + $read = ($this->mergedRegister()['components']['schemas']['contactPerson']['authorization']['read'] ?? null); + $this->assertIsArray($read, 'contactPerson must have an explicit read rule; without one OpenRegister does not restrict reads'); + $this->assertNotEmpty($read, 'contactPerson'); + foreach ($read as $rule) { + $this->assertFalse(self::isPublic(rule: $rule), 'contactPerson has a public read rule: imported owners would be readable anonymously'); + } + }//end testContactPersonHasNoPublicReadRule() + + /** + * A usage is public only once published, and never with its owners. + * + * Publication-field-rules (stackiq #1206) makes a usage readable to the public + * group once its publicationDate has passed, so OpenCatalogi can show which + * applications an organisation uses. The import never sets a usage's + * publicationDate, and the properties that name a person carry their own read + * rule without the public group, which OpenRegister enforces on the body, + * relations, `@self` and facets alike. + * + * @return void + */ + public function testAUsageIsPublicOnlyWhenPublishedAndNeverWithItsOwners(): void { + $usage = $this->mergedRegister()['components']['schemas']['usage']; + $read = ($usage['authorization']['read'] ?? null); + $this->assertIsArray($read, 'usage must have an explicit read rule; without one OpenRegister does not restrict reads'); + $this->assertNotEmpty($read, 'usage'); + foreach ($read as $rule) { + if (self::isPublic(rule: $rule) === true) { + $this->assertIsArray($rule, 'a bare public read rule on usage would publish every usage'); + $this->assertArrayHasKey('publicationDate', ($rule['match'] ?? []), 'the public read rule on usage must be conditional on publicationDate'); + } + } + + foreach (['businessOwner', 'technicalOwner', 'contactPerson'] as $property) { + $propertyRead = ($usage['properties'][$property]['authorization']['read'] ?? null); + $this->assertIsArray($propertyRead, 'usage.' . $property . ' names a person and needs its own read rule'); + $this->assertNotEmpty($propertyRead, 'usage.' . $property); + foreach ($propertyRead as $rule) { + $this->assertFalse(self::isPublic(rule: $rule), 'usage.' . $property . ' is publicly readable: imported owners would be visible anonymously'); } } - }//end testUsageAndContactPersonHaveNoPublicReadRule() + }//end testAUsageIsPublicOnlyWhenPublishedAndNeverWithItsOwners() /** * A published module refers to its contact person and usages by relation only, and holds no person field.