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/Unit/Settings/CmdbPersonDataVisibilityTest.php b/tests/Unit/Settings/CmdbPersonDataVisibilityTest.php index ccb8d6f2..487a3197 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. diff --git a/tests/bootstrap-unit.php b/tests/bootstrap-unit.php index 3ba8c636..763f89fd 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 09bd3bc3..9d39d07e 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'; +}