From 5947044367c444daaf428d110ce2e7fce6341e97 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Thu, 15 Oct 2026 19:12:05 +0200 Subject: [PATCH 1/2] fix(example-sets): a set may require a register OpenRegister ships only as a mock A fresh instance refused the zuiddrecht set: brp and kvk are mock-only descriptors, and the real-configuration re-import correctly refuses a mock. A requires entry now names the descriptor it wants: a slug for the real one, {"register": "", "mock": true} for the mock, imported through reimportMock() under openregister.mock and recorded for remove as before. A plain slug never falls back to the mock. zuiddrecht declares brp and kvk as mock requirements. --- .../ExampleSet/ExampleSetCatalogue.php | 41 +++- .../ExampleSet/ExampleSetPrerequisites.php | 5 +- .../ExampleSet/ExampleSetRegisters.php | 24 ++- lib/Settings/example-sets/zuiddrecht.json | 10 +- openspec/changes/example-sets/design.md | 8 + .../example-sets/specs/example-sets/spec.md | 12 +- openspec/changes/example-sets/tasks.md | 1 + .../ExampleSet/ExampleSetCatalogueTest.php | 2 +- .../ExampleSetMockRegisterImportTest.php | 176 ++++++++++++++++++ .../Fake/FakeExampleSetRegisters.php | 11 +- 10 files changed, 270 insertions(+), 20 deletions(-) create mode 100644 tests/Unit/Service/ExampleSet/ExampleSetMockRegisterImportTest.php diff --git a/lib/Service/ExampleSet/ExampleSetCatalogue.php b/lib/Service/ExampleSet/ExampleSetCatalogue.php index cc6976ab72..b96c175c34 100644 --- a/lib/Service/ExampleSet/ExampleSetCatalogue.php +++ b/lib/Service/ExampleSet/ExampleSetCatalogue.php @@ -116,7 +116,7 @@ public function sets(): array { * @param string $set The set id. * * @return array `set`, `title`, `timezone`, `cast` (people, organisations and groups by key, - * plus `entries`, all three merged), `requires` (OpenRegister register slugs), + * plus `entries`, all three merged), `requires` (OpenRegister register slug to `{mock}`), * `fragments` (each with `app`, `file`, `objects`, `changes` and `groups`) and * `skipped` (named apps that are not enabled). * @@ -161,7 +161,7 @@ public function gather(string $set): array { 'title' => (string) ($castFile['title'] ?? $set), 'timezone' => (string) ($castFile['timezone'] ?? 'UTC'), 'cast' => $this->cast(cast: $castFile['cast'], file: $castFile['@file'], set: $set), - 'requires' => array_values(array_unique(array_map('strval', (array) ($castFile['requires'] ?? [])))), + 'requires' => $this->requiredRegisters(declared: (array) ($castFile['requires'] ?? []), file: $castFile['@file']), 'organisations' => $this->organisationEntities(declared: (array) ($castFile['cast']['openregisterOrganisations'] ?? []), file: $castFile['@file']), 'fragments' => [], 'skipped' => $ordered['skipped'], @@ -388,6 +388,43 @@ private function userPreferences(string $app, string $file, array $declared): ar return $entries; }//end userPreferences() + /** + * The OpenRegister registers the cast requires, keyed by slug. + * + * An entry is a slug, imported from OpenRegister's real descriptor, or + * `{"register": "", "mock": true}`, imported from its mock (sample) + * descriptor. + * + * @param array $declared The declared requirements. + * @param string $file The cast file, for messages. + * + * @return array Slug to how it is imported. + * + * @throws ExampleSetException When an entry names no register. + * + * @spec openspec/changes/example-sets/specs/example-sets/spec.md#requirement-a-set-imports-the-openregister-registers-it-requires-req-exs-009 + */ + private function requiredRegisters(array $declared, string $file): array { + $registers = []; + foreach ($declared as $index => $entry) { + if (is_array($entry) === false) { + $entry = ['register' => $entry]; + } + + $slug = $entry['register'] ?? ''; + $mock = $entry['mock'] ?? false; + if (is_string($slug) === false || $slug === '' || is_bool($mock) === false) { + throw new ExampleSetException( + $file.': requires '.$index.' needs a register slug, or {"register": "", "mock": true}.' + ); + } + + $registers[$slug] = ['mock' => $mock]; + } + + return $registers; + }//end requiredRegisters() + /** * The OpenRegister organisations the cast needs, each with a key, a name and a slug. * diff --git a/lib/Service/ExampleSet/ExampleSetPrerequisites.php b/lib/Service/ExampleSet/ExampleSetPrerequisites.php index 371139d501..cee9c5d817 100644 --- a/lib/Service/ExampleSet/ExampleSetPrerequisites.php +++ b/lib/Service/ExampleSet/ExampleSetPrerequisites.php @@ -114,13 +114,14 @@ public function context(array $plan, DateTimeImmutable $today): array { */ private function ensureRegisters(array $plan, array &$record): array { $result = ['refusals' => [], 'outcome' => []]; - foreach ((array) ($plan['requires'] ?? []) as $slug) { + foreach ((array) ($plan['requires'] ?? []) as $slug => $required) { + $slug = (string) $slug; $result['outcome'][$slug] = 'kept'; if ($this->registers->exists(slug: $slug) === true) { continue; } - $import = $this->registers->import(slug: $slug); + $import = $this->registers->import(slug: $slug, mock: (($required['mock'] ?? false) === true)); if ($import['jobs'] !== []) { $record['registers'][$slug] = array_merge(($record['registers'][$slug] ?? []), $import['jobs']); $this->record->write(set: (string) $plan['set'], record: $record); diff --git a/lib/Service/ExampleSet/ExampleSetRegisters.php b/lib/Service/ExampleSet/ExampleSetRegisters.php index f38c0b3be9..a01f1d84b1 100644 --- a/lib/Service/ExampleSet/ExampleSetRegisters.php +++ b/lib/Service/ExampleSet/ExampleSetRegisters.php @@ -6,7 +6,8 @@ * The OpenRegister registers an example set needs, named in the cast file's * `requires` list. A required register that is not on the instance is * imported from OpenRegister's own descriptor, the same way - * `POST /api/register-descriptors/openregister/{slug}/import` does. The + * `POST /api/register-descriptors/openregister/{slug}/import` does, or from + * its mock descriptor when the set requires the mock. The * import jobs that import created are returned, so the set can remove exactly * those objects again and leave an import somebody else ran alone. * @@ -90,24 +91,27 @@ public function exists(string $slug): bool { }//end exists() /** - * Import a register from OpenRegister's own descriptor. + * Import a register from OpenRegister's own descriptor, or from its mock (sample) descriptor. * * @param string $slug The register slug. + * @param bool $mock Import the mock descriptor that declares the slug instead of the real one. * * @return array{error: string, jobs: array} Why it failed, or ''; and * the import jobs it created. * * @spec openspec/changes/example-sets/specs/example-sets/spec.md#requirement-a-set-imports-the-openregister-registers-it-requires-req-exs-009 */ - public function import(string $slug): array { + public function import(string $slug, bool $mock=false): array { try { - $before = $this->jobIds(); - $result = $this->container->get(RegisterDescriptorService::class)->reimport( - appId: self::APP, - slug: $slug, - via: 'example set install' - ); - $after = $this->jobIds(); + $before = $this->jobIds(); + $descriptors = $this->container->get(RegisterDescriptorService::class); + if ($mock === true) { + $result = $descriptors->reimportMock(appId: self::APP, slug: $slug); + } else { + $result = $descriptors->reimport(appId: self::APP, slug: $slug, via: 'example set install'); + } + + $after = $this->jobIds(); } catch (Throwable $e) { return ['error' => $e->getMessage(), 'jobs' => []]; } diff --git a/lib/Settings/example-sets/zuiddrecht.json b/lib/Settings/example-sets/zuiddrecht.json index 12b4099938..a9997bebb8 100644 --- a/lib/Settings/example-sets/zuiddrecht.json +++ b/lib/Settings/example-sets/zuiddrecht.json @@ -4,8 +4,14 @@ "$comment": "The cast of the Zuiddrecht example set, and OpenRegister's own fragment: a BRP row per cast person with a BSN and a KVK row and branch per business. Installed with `occ openregister:example-set:install zuiddrecht`; every other app adds its part in its own lib/Settings/example-sets/zuiddrecht.json and refers to cast members with {{cast:key.field}} and to other apps' objects only through a lookup on a business key (BSN, KVK number). Zuiddrecht is fictitious. BSNs are 11-proof numbers from the BRP test block 999990000-999999999 and appear nowhere else in the fleet's registers; KVK numbers are fictitious; municipality code 9999 belongs to no municipality.", "timezone": "Europe/Amsterdam", "requires": [ - "brp", - "kvk" + { + "register": "brp", + "mock": true + }, + { + "register": "kvk", + "mock": true + } ], "apps": [ "openregister", diff --git a/openspec/changes/example-sets/design.md b/openspec/changes/example-sets/design.md index d062ed2112..ed5710a91a 100644 --- a/openspec/changes/example-sets/design.md +++ b/openspec/changes/example-sets/design.md @@ -152,6 +152,14 @@ the Woo journey depends on them. That is a separate portaliq PR. elevation call-site set stays pinned (ADR-099 rule 9). A register that was already there is never imported or removed. The register and schema definitions stay; only the objects of the import go. + Amended 15 Oct: `brp` and `kvk` are mock-only descriptors, and the + real-configuration re-import (#4618) correctly refuses a mock, so a fresh + instance refused the set. A `requires` entry now says which descriptor it + wants: a slug for the real one, `{"register": "", "mock": true}` for + the mock, imported through `RegisterDescriptorService::reimportMock()` + under `openregister.mock`. The choice is the set's, not a command flag, so + the nightly reset runs the same command as a hand install and a plain slug + never quietly falls back to sample data. 8. **Changes to objects the set did not create.** A sign-in mode lives on the portal row that the site install made, so it cannot be a new object. A fragment's `changes` find one object by business key and `set` or `append` diff --git a/openspec/changes/example-sets/specs/example-sets/spec.md b/openspec/changes/example-sets/specs/example-sets/spec.md index c2fa1fe32e..42417adf16 100644 --- a/openspec/changes/example-sets/specs/example-sets/spec.md +++ b/openspec/changes/example-sets/specs/example-sets/spec.md @@ -130,7 +130,7 @@ After writing, the loader SHALL read every object it created in this run back fr ### Requirement: A set imports the OpenRegister registers it requires (REQ-EXS-009) -The cast file MAY name OpenRegister registers in `requires` (for example `["brp", "kvk"]`). Before the check, the loader SHALL import every required register that is not on the instance from OpenRegister's own register descriptor, the way the descriptor import endpoint does, and SHALL record the import jobs that import created. A required register that is already on the instance SHALL be used as it is and SHALL NOT be recorded. A second run SHALL import nothing. Remove SHALL soft-delete exactly the objects of the recorded import jobs, after the set's own objects, and SHALL NOT touch an import it did not make. A required register that cannot be imported SHALL refuse the set. +The cast file MAY name OpenRegister registers in `requires`. An entry SHALL be either a register slug (for example `"publication"`), imported from OpenRegister's real register descriptor, or `{"register": "", "mock": true}`, imported from OpenRegister's mock (sample) descriptor that declares the slug, the way `occ openregister:descriptors:list --app=openregister --import= --mock` does. An example set is sample data, so it MAY require a register OpenRegister ships only as a mock (zuiddrecht requires `brp` and `kvk` that way). An entry that names no register slug SHALL refuse the set when it is gathered. Before the check, the loader SHALL import every required register that is not on the instance from the descriptor its entry names, and SHALL record the import jobs that import created. A required register that is already on the instance SHALL be used as it is and SHALL NOT be recorded. A second run SHALL import nothing. Remove SHALL soft-delete exactly the objects of the recorded import jobs, after the set's own objects, and SHALL NOT touch an import it did not make. A required register that cannot be imported SHALL refuse the set. #### Scenario: A missing register is imported once and removed again - **GIVEN** a set that requires `brp` and an instance without it @@ -142,6 +142,16 @@ The cast file MAY name OpenRegister registers in `requires` (for example `["brp" - **WHEN** the set is installed and removed - **THEN** nothing SHALL be imported and remove SHALL leave every import of `brp` alone +#### Scenario: A register shipped only as a mock is imported from the mock +- **GIVEN** a set whose `requires` holds `{"register": "brp", "mock": true}` and a fresh instance without `brp` +- **WHEN** `occ openregister:example-set:install --check` runs +- **THEN** `brp` SHALL be imported from OpenRegister's mock `brp` descriptor under the `openregister.mock` configuration id, reported as `imported`, and recorded so remove deletes its objects + +#### Scenario: A plain slug never falls back to a mock +- **GIVEN** a set whose `requires` holds `"brp"` and OpenRegister ships `brp` only as a mock +- **WHEN** the set is checked +- **THEN** it SHALL be refused naming `brp`, and SHALL NOT import the mock + #### Scenario: A register that cannot be imported refuses the set - **GIVEN** a set that requires a register OpenRegister ships no descriptor for - **WHEN** the set is installed diff --git a/openspec/changes/example-sets/tasks.md b/openspec/changes/example-sets/tasks.md index 862c510e8e..77642ba7ef 100644 --- a/openspec/changes/example-sets/tasks.md +++ b/openspec/changes/example-sets/tasks.md @@ -48,6 +48,7 @@ - [x] 3.10 Remove from occ: files by node, archival and held objects, reasons in the output (`ExampleSetRemoveLiveTest`, `ExampleSetStoreTest`) - [x] 3.11 Partial remove then reinstall: kept publications follow the theme written again, a further install writes nothing, remove is whole (`ExampleSetReferencesTest`) - [x] 3.12 User preferences written, kept, restored, left when changed, and refused for a foreign account, an administrator and a secret (`ExampleSetUserPreferencesTest`) +- [x] 3.13 A required register shipped only as a mock is declared `{"register": "", "mock": true}` and imported from the mock; a plain slug never falls back to it; the shipped zuiddrecht requirements import on a fresh instance (`ExampleSetMockRegisterImportTest`) - [x] 3.6 Required registers imported once and removed only when imported; admin refused; changes made, proved and reverted exactly; JSON text fields ## 4. Verification diff --git a/tests/Unit/Service/ExampleSet/ExampleSetCatalogueTest.php b/tests/Unit/Service/ExampleSet/ExampleSetCatalogueTest.php index 5e96e1b406..283edc7bb6 100644 --- a/tests/Unit/Service/ExampleSet/ExampleSetCatalogueTest.php +++ b/tests/Unit/Service/ExampleSet/ExampleSetCatalogueTest.php @@ -93,7 +93,7 @@ public function testTheShippedZuiddrechtSetIsWhole(): void { $plan = $this->catalogue->gather(set: 'zuiddrecht'); $this->assertCount(16, $plan['cast']['people']); - $this->assertSame(['brp', 'kvk'], $plan['requires']); + $this->assertSame(['brp' => ['mock' => true], 'kvk' => ['mock' => true]], $plan['requires']); $this->assertSame(['vergunningen'], array_values(array_diff($plan['cast']['people']['marleen']['groups'], ['medewerkers']))); $this->assertSame('Teamleider', $plan['cast']['people']['marleen']['function']); $this->assertNotContains('admin', array_column($plan['cast']['groups'], 'gid')); diff --git a/tests/Unit/Service/ExampleSet/ExampleSetMockRegisterImportTest.php b/tests/Unit/Service/ExampleSet/ExampleSetMockRegisterImportTest.php new file mode 100644 index 0000000000..55ef713ce3 --- /dev/null +++ b/tests/Unit/Service/ExampleSet/ExampleSetMockRegisterImportTest.php @@ -0,0 +1,176 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Tests\Unit\Service\ExampleSet; + +use OCA\OpenRegister\AppHost\AppContainerLocator; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Service\AppRegisterImportRunner; +use OCA\OpenRegister\Service\Configuration\AppImportJobRecorder; +use OCA\OpenRegister\Service\ConfigurationService; +use OCA\OpenRegister\Service\CountedRegisterImport; +use OCA\OpenRegister\Service\ExampleSet\ExampleSetException; +use OCA\OpenRegister\Service\ExampleSet\ExampleSetRegisters; +use OCA\OpenRegister\Service\RegisterDescriptorService; +use OCA\OpenRegister\Service\ShippedBaseline\ShippedAuthorizationDecision; +use OCP\App\IAppManager; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; + +/** + * @covers \OCA\OpenRegister\Service\ExampleSet\ExampleSetCatalogue + * @covers \OCA\OpenRegister\Service\ExampleSet\ExampleSetPrerequisites + * @covers \OCA\OpenRegister\Service\ExampleSet\ExampleSetRegisters + * + * @spec openspec/changes/example-sets/specs/example-sets/spec.md#requirement-a-set-imports-the-openregister-registers-it-requires-req-exs-009 + */ +class ExampleSetMockRegisterImportTest extends ExampleSetTestCase { + /** @var array Registers on the fake instance, slug to version. */ + private array $instance = []; + + /** @var array>> Recorded import jobs per configuration id. */ + private array $recorded = []; + + /** @var array The configuration ids the imports ran under. */ + private array $importedAs = []; + + /** + * The real registers service over OpenRegister's own lib/Settings. + * + * @return ExampleSetRegisters + */ + private function realRegisters(): ExampleSetRegisters { + $appManager = $this->createMock(IAppManager::class); + $appManager->method('getAppPath')->willReturn(dirname(__DIR__, 4)); + + $registerMapper = $this->createMock(RegisterMapper::class); + $registerMapper->method('findAll')->willReturnCallback(function (): array { + $registers = []; + foreach ($this->instance as $slug => $version) { + $register = new Register(); + $register->setSlug($slug); + $register->setVersion($version); + $registers[] = $register; + } + + return $registers; + }); + + $configuration = $this->createMock(ConfigurationService::class); + $configuration->method('importFromApp')->willReturnCallback( + function (string $appId, array $data, string $version, bool $force = false): array { + $this->importedAs[] = $appId; + foreach (array_keys((array) ($data['components']['registers'] ?? [])) as $slug) { + $this->instance[strtolower((string) $slug)] = $version; + } + + $this->recorded[$appId][] = ['jobId' => 'job-'.count($this->importedAs), 'version' => $version, 'created' => 1, 'importedAt' => '']; + + return ['objects' => array_keys((array) ($data['components']['objects'] ?? [])), 'skipped' => []]; + } + ); + + $descriptors = new RegisterDescriptorService( + $appManager, + $registerMapper, + $configuration, + $this->createMock(LoggerInterface::class), + new CountedRegisterImport( + registerMapper: $registerMapper, + schemaMapper: $this->createMock(SchemaMapper::class), + decision: new ShippedAuthorizationDecision() + ), + new AppRegisterImportRunner( + appManager: $appManager, + containers: $this->createMock(AppContainerLocator::class), + configurationService: $configuration + ) + ); + + $jobs = $this->createMock(AppImportJobRecorder::class); + $jobs->method('jobs')->willReturnCallback(fn (string $appId): array => ($this->recorded[$appId] ?? [])); + + $container = $this->createMock(ContainerInterface::class); + $container->method('get')->willReturnCallback(fn (string $id): object => match ($id) { + RegisterDescriptorService::class => $descriptors, + AppImportJobRecorder::class => $jobs, + }); + + return new ExampleSetRegisters(container: $container); + } + + public function testEveryRegisterTheShippedZuiddrechtSetRequiresImportsOnAFreshInstance(): void { + $this->apps['openregister'] = dirname(__DIR__, 4); + $plan = $this->catalogue->gather(set: 'zuiddrecht'); + $registers = $this->realRegisters(); + + $this->assertSame(['brp', 'kvk'], array_keys($plan['requires'])); + foreach ($plan['requires'] as $slug => $required) { + $import = $registers->import(slug: $slug, mock: $required['mock']); + + $this->assertSame('', $import['error'], $slug.' imports from what the set declares'); + $this->assertSame([['appId' => 'openregister.mock', 'jobId' => 'job-'.count($this->importedAs)]], $import['jobs']); + $this->assertArrayHasKey($slug, $this->instance); + } + + $this->assertSame(['openregister.mock', 'openregister.mock'], $this->importedAs); + } + + public function testARegisterShippedOnlyAsAMockIsNotImportedAsTheRealConfiguration(): void { + $import = $this->realRegisters()->import(slug: 'brp'); + + $this->assertStringContainsString('declares no register "brp"', $import['error']); + $this->assertSame([], $import['jobs']); + $this->assertSame([], $this->importedAs); + } + + public function testARequirementWithoutARegisterSlugRefusesTheSet(): void { + $cast = $this->castFile(); + $cast['requires'] = [['mock' => true]]; + $this->app(app: 'openregister', fragment: $cast); + + $this->expectException(ExampleSetException::class); + $this->expectExceptionMessage('requires 0 needs a register slug, or {"register": "", "mock": true}.'); + $this->catalogue->gather(set: 'demo'); + } + + public function testAMockRequirementIsImportedFromTheMockAndRecorded(): void { + $this->store->offer(register: 'brp', schema: 'person'); + $this->registers->mockImportable['brp'] = true; + $cast = $this->castFile(objects: [['key' => 'brp-sanne', 'register' => 'brp', 'schema' => 'person', 'data' => ['bsn' => '{{cast:sanne.bsn}}']]]); + $cast['requires'] = [['register' => 'brp', 'mock' => true]]; + $this->app(app: 'openregister', fragment: $cast); + + $report = $this->installer->install(plan: $this->catalogue->gather(set: 'demo'), today: $this->today()); + + $this->assertTrue($report['ok'], (string) json_encode($report)); + $this->assertSame(['brp' => 'imported'], $report['registers']); + $this->assertSame(1, $this->registers->imports); + $this->assertSame(1, $this->remover->remove(set: 'demo')['removed']['register imports']); + } +} diff --git a/tests/Unit/Service/ExampleSet/Fake/FakeExampleSetRegisters.php b/tests/Unit/Service/ExampleSet/Fake/FakeExampleSetRegisters.php index f515cd2ff8..2a94a17bff 100644 --- a/tests/Unit/Service/ExampleSet/Fake/FakeExampleSetRegisters.php +++ b/tests/Unit/Service/ExampleSet/Fake/FakeExampleSetRegisters.php @@ -32,6 +32,9 @@ class FakeExampleSetRegisters extends ExampleSetRegisters { /** @var array Register slugs OpenRegister ships a descriptor for. */ public array $importable = []; + /** @var array Register slugs OpenRegister ships a mock descriptor for. */ + public array $mockImportable = []; + /** @var array Live import job ids, mapped to their register. */ public array $jobs = []; @@ -51,9 +54,13 @@ public function exists(string $slug): bool { return isset($this->present[$slug]); } - public function import(string $slug): array { + public function import(string $slug, bool $mock=false): array { $this->imports++; - if (isset($this->importable[$slug]) === false) { + if ($mock === true && isset($this->mockImportable[$slug]) === false) { + return ['error' => 'openregister ships no mock descriptor declaring register "'.$slug.'".', 'jobs' => []]; + } + + if ($mock === false && isset($this->importable[$slug]) === false) { return ['error' => 'openregister ships no descriptor declaring register "'.$slug.'".', 'jobs' => []]; } From b637c7c7f1021fb8f0a628edfa4abe1f34230069 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Thu, 15 Oct 2026 20:37:33 +0200 Subject: [PATCH 2/2] refactor(example-sets): a mock import is its own method, not a flag --- .../ExampleSet/ExampleSetPrerequisites.php | 5 +- .../ExampleSet/ExampleSetRegisters.php | 52 ++++++++++++++----- .../ExampleSetMockRegisterImportTest.php | 3 +- .../Fake/FakeExampleSetRegisters.php | 15 ++++-- 4 files changed, 56 insertions(+), 19 deletions(-) diff --git a/lib/Service/ExampleSet/ExampleSetPrerequisites.php b/lib/Service/ExampleSet/ExampleSetPrerequisites.php index cee9c5d817..a391d5c7ba 100644 --- a/lib/Service/ExampleSet/ExampleSetPrerequisites.php +++ b/lib/Service/ExampleSet/ExampleSetPrerequisites.php @@ -121,7 +121,10 @@ private function ensureRegisters(array $plan, array &$record): array { continue; } - $import = $this->registers->import(slug: $slug, mock: (($required['mock'] ?? false) === true)); + $import = match (($required['mock'] ?? false) === true) { + true => $this->registers->importMock(slug: $slug), + false => $this->registers->import(slug: $slug), + }; if ($import['jobs'] !== []) { $record['registers'][$slug] = array_merge(($record['registers'][$slug] ?? []), $import['jobs']); $this->record->write(set: (string) $plan['set'], record: $record); diff --git a/lib/Service/ExampleSet/ExampleSetRegisters.php b/lib/Service/ExampleSet/ExampleSetRegisters.php index a01f1d84b1..211681e7c6 100644 --- a/lib/Service/ExampleSet/ExampleSetRegisters.php +++ b/lib/Service/ExampleSet/ExampleSetRegisters.php @@ -91,27 +91,53 @@ public function exists(string $slug): bool { }//end exists() /** - * Import a register from OpenRegister's own descriptor, or from its mock (sample) descriptor. + * Import a register from OpenRegister's own descriptor. * * @param string $slug The register slug. - * @param bool $mock Import the mock descriptor that declares the slug instead of the real one. * * @return array{error: string, jobs: array} Why it failed, or ''; and * the import jobs it created. * * @spec openspec/changes/example-sets/specs/example-sets/spec.md#requirement-a-set-imports-the-openregister-registers-it-requires-req-exs-009 */ - public function import(string $slug, bool $mock=false): array { - try { - $before = $this->jobIds(); - $descriptors = $this->container->get(RegisterDescriptorService::class); - if ($mock === true) { - $result = $descriptors->reimportMock(appId: self::APP, slug: $slug); - } else { - $result = $descriptors->reimport(appId: self::APP, slug: $slug, via: 'example set install'); - } + public function import(string $slug): array { + return $this->recordedImport( + import: fn (RegisterDescriptorService $descriptors): array => $descriptors->reimport( + appId: self::APP, + slug: $slug, + via: 'example set install' + ) + ); + }//end import() - $after = $this->jobIds(); + /** + * Import a register from OpenRegister's mock (sample) descriptor that declares the slug. + * + * @param string $slug The register slug. + * + * @return array{error: string, jobs: array} Why it failed, or ''; and + * the import jobs it created. + * + * @spec openspec/changes/example-sets/specs/example-sets/spec.md#requirement-a-set-imports-the-openregister-registers-it-requires-req-exs-009 + */ + public function importMock(string $slug): array { + return $this->recordedImport( + import: fn (RegisterDescriptorService $descriptors): array => $descriptors->reimportMock(appId: self::APP, slug: $slug) + ); + }//end importMock() + + /** + * Run one descriptor import and return why it failed and the import jobs it created. + * + * @param callable(RegisterDescriptorService): array $import The import. + * + * @return array{error: string, jobs: array} + */ + private function recordedImport(callable $import): array { + try { + $before = $this->jobIds(); + $result = $import($this->container->get(RegisterDescriptorService::class)); + $after = $this->jobIds(); } catch (Throwable $e) { return ['error' => $e->getMessage(), 'jobs' => []]; } @@ -129,7 +155,7 @@ public function import(string $slug, bool $mock=false): array { } return ['error' => $error, 'jobs' => $jobs]; - }//end import() + }//end recordedImport() /** * Soft-delete the objects one import job created, and forget the job when that went cleanly. diff --git a/tests/Unit/Service/ExampleSet/ExampleSetMockRegisterImportTest.php b/tests/Unit/Service/ExampleSet/ExampleSetMockRegisterImportTest.php index 55ef713ce3..05ea1782fb 100644 --- a/tests/Unit/Service/ExampleSet/ExampleSetMockRegisterImportTest.php +++ b/tests/Unit/Service/ExampleSet/ExampleSetMockRegisterImportTest.php @@ -131,7 +131,8 @@ public function testEveryRegisterTheShippedZuiddrechtSetRequiresImportsOnAFreshI $this->assertSame(['brp', 'kvk'], array_keys($plan['requires'])); foreach ($plan['requires'] as $slug => $required) { - $import = $registers->import(slug: $slug, mock: $required['mock']); + $this->assertTrue($required['mock'], $slug.' is required as a mock'); + $import = $registers->importMock(slug: $slug); $this->assertSame('', $import['error'], $slug.' imports from what the set declares'); $this->assertSame([['appId' => 'openregister.mock', 'jobId' => 'job-'.count($this->importedAs)]], $import['jobs']); diff --git a/tests/Unit/Service/ExampleSet/Fake/FakeExampleSetRegisters.php b/tests/Unit/Service/ExampleSet/Fake/FakeExampleSetRegisters.php index 2a94a17bff..e221af7f22 100644 --- a/tests/Unit/Service/ExampleSet/Fake/FakeExampleSetRegisters.php +++ b/tests/Unit/Service/ExampleSet/Fake/FakeExampleSetRegisters.php @@ -54,13 +54,20 @@ public function exists(string $slug): bool { return isset($this->present[$slug]); } - public function import(string $slug, bool $mock=false): array { - $this->imports++; - if ($mock === true && isset($this->mockImportable[$slug]) === false) { + public function importMock(string $slug): array { + if (isset($this->mockImportable[$slug]) === false) { + $this->imports++; return ['error' => 'openregister ships no mock descriptor declaring register "'.$slug.'".', 'jobs' => []]; } - if ($mock === false && isset($this->importable[$slug]) === false) { + $this->importable[$slug] = true; + + return $this->import(slug: $slug); + } + + public function import(string $slug): array { + $this->imports++; + if (isset($this->importable[$slug]) === false) { return ['error' => 'openregister ships no descriptor declaring register "'.$slug.'".', 'jobs' => []]; }