diff --git a/composer.json b/composer.json index afb2557..9b2612f 100644 --- a/composer.json +++ b/composer.json @@ -16,12 +16,12 @@ ], "require": { "php": ">=8.2", - "ext-mbstring": "*" + "ext-mbstring": "*", + "nimbuscms/nimbus": "dev-main" }, "require-dev": { "phpstan/phpstan": "^2.2", - "phpunit/phpunit": "^11.0", - "nimbuscms/nimbus": "dev-main" + "phpunit/phpunit": "^11.0" }, "repositories": [ { diff --git a/src/MarkdownPlugin.php b/src/MarkdownPlugin.php index b1d31a5..7e970fc 100644 --- a/src/MarkdownPlugin.php +++ b/src/MarkdownPlugin.php @@ -23,6 +23,6 @@ final class MarkdownPlugin implements Plugin public function register(PluginContext $context): void { - $context->fieldTypes()->register(new MarkdownFieldType(), self::ID); + $context->fieldTypes()->register(new MarkdownFieldType()); } } diff --git a/tests/MarkdownFieldTypeTest.php b/tests/MarkdownFieldTypeTest.php index 454acd5..1b964a8 100644 --- a/tests/MarkdownFieldTypeTest.php +++ b/tests/MarkdownFieldTypeTest.php @@ -34,7 +34,7 @@ private function field(array $options = [], bool $required = false): Field public function test_the_plugin_registers_its_field_type(): void { $registry = new FieldTypeRegistry(); - (new MarkdownPlugin())->register(new PluginContext($registry)); + (new MarkdownPlugin())->register(new PluginContext($registry, MarkdownPlugin::ID)); self::assertTrue($registry->has('markdown')); self::assertSame('markdown', $registry->get('markdown')->type()); @@ -44,7 +44,7 @@ public function test_the_plugin_registers_its_field_type(): void public function test_the_type_appears_in_the_field_picker(): void { $registry = new FieldTypeRegistry(); - (new MarkdownPlugin())->register(new PluginContext($registry)); + (new MarkdownPlugin())->register(new PluginContext($registry, MarkdownPlugin::ID)); self::assertArrayHasKey('markdown', $registry->choices()); self::assertSame('Markdown', $registry->choices()['markdown']); @@ -62,10 +62,10 @@ public function test_the_plugin_id_matches_the_composer_manifest(): void public function test_registering_twice_is_rejected_by_core(): void { $registry = new FieldTypeRegistry(); - (new MarkdownPlugin())->register(new PluginContext($registry)); + (new MarkdownPlugin())->register(new PluginContext($registry, MarkdownPlugin::ID)); $this->expectException(\Nimbus\Content\DuplicateFieldType::class); - (new MarkdownPlugin())->register(new PluginContext($registry)); + (new MarkdownPlugin())->register(new PluginContext($registry, MarkdownPlugin::ID)); } // -------------------------------------------------------- normalization @@ -125,7 +125,7 @@ public function test_max_length_counts_characters_not_bytes(): void public function test_required_empty_is_handled_by_core_not_here(): void { $registry = new FieldTypeRegistry(); - (new MarkdownPlugin())->register(new PluginContext($registry)); + (new MarkdownPlugin())->register(new PluginContext($registry, MarkdownPlugin::ID)); $collection = new Collection(1, 'posts', 'Posts', '#', '', [$this->field(required: true)], ['kind' => 'collection']); $errors = (new Validator($registry))->validate($collection, ['body' => $this->type->normalize('')]); @@ -137,7 +137,7 @@ public function test_required_empty_is_handled_by_core_not_here(): void public function test_a_valid_required_value_passes_through_core_validation(): void { $registry = new FieldTypeRegistry(); - (new MarkdownPlugin())->register(new PluginContext($registry)); + (new MarkdownPlugin())->register(new PluginContext($registry, MarkdownPlugin::ID)); $collection = new Collection(1, 'posts', 'Posts', '#', '', [$this->field(required: true)], ['kind' => 'collection']); $errors = (new Validator($registry))->validate($collection, ['body' => $this->type->normalize('# Hello')]); diff --git a/tests/PackageIntegrationTest.php b/tests/PackageIntegrationTest.php new file mode 100644 index 0000000..5015fab --- /dev/null +++ b/tests/PackageIntegrationTest.php @@ -0,0 +1,192 @@ +installedJson = tempnam(sys_get_temp_dir(), 'nb-installed-') ?: ''; + } + + protected function tearDown(): void + { + @unlink($this->installedJson); + } + + /** @return array this package's actual composer manifest */ + private function manifest(): array + { + $manifest = json_decode((string) file_get_contents(__DIR__ . '/../composer.json'), true); + self::assertIsArray($manifest); + + return $manifest; + } + + /** + * An installed.json describing this package exactly as Composer would, + * straight from the real manifest. + */ + private function installedAs(): string + { + $manifest = $this->manifest(); + file_put_contents($this->installedJson, json_encode([ + 'packages' => [[ + 'name' => $manifest['name'], + 'type' => $manifest['type'], + 'extra' => $manifest['extra'], + ]], + ], JSON_THROW_ON_ERROR)); + + return $this->installedJson; + } + + // ------------------------------------------------------- the manifest + + public function test_the_package_declares_nimbus_as_a_runtime_dependency(): void + { + $manifest = $this->manifest(); + + // The plugin's production classes implement Nimbus interfaces, so core + // is a runtime requirement. In require-dev, Composer would happily + // install this package without a compatible Nimbus present. + self::assertArrayHasKey('nimbuscms/nimbus', $manifest['require']); + self::assertArrayNotHasKey('nimbuscms/nimbus', $manifest['require-dev'] ?? []); + } + + public function test_the_package_is_typed_as_a_nimbus_plugin(): void + { + self::assertSame('nimbuscms-plugin', $this->manifest()['type']); + } + + // -------------------------------------------------- discovery to registry + + public function test_composer_discovery_registers_the_field_type(): void + { + $registry = new FieldTypeRegistry(); + $loader = new PluginLoader($this->installedAs()); + $diagnostics = $loader->load($registry); + + self::assertSame([], $diagnostics, 'a correctly installed package must load cleanly'); + self::assertSame( + [MarkdownPlugin::ID => $this->manifest()['name']], + $loader->registered(), + ); + + // Registered into the *shared* registry, under this plugin's id. + self::assertTrue($registry->has('markdown')); + self::assertSame(MarkdownPlugin::ID, $registry->providerOf('markdown')); + self::assertArrayHasKey('markdown', $registry->choices()); + } + + public function test_core_field_types_are_untouched_by_installation(): void + { + $registry = new FieldTypeRegistry(); + (new PluginLoader($this->installedAs()))->load($registry); + + foreach (['text', 'textarea', 'number', 'boolean', 'relation'] as $core) { + self::assertSame('core', $registry->providerOf($core)); + } + } + + // ---------------------------------------------------------- disabling + + public function test_disabling_the_package_leaves_the_type_unregistered(): void + { + $registry = new FieldTypeRegistry(); + $loader = new PluginLoader($this->installedAs(), [MarkdownPlugin::ID => false]); + $diagnostics = $loader->load($registry); + + self::assertSame([], $loader->registered()); + self::assertFalse($registry->has('markdown')); + self::assertCount(1, $diagnostics); + self::assertSame(PluginDiagnostic::DISABLED, $diagnostics[0]->reason); + self::assertFalse($diagnostics[0]->isFailure(), 'disabled is a choice, not a fault'); + } + + public function test_with_the_package_disabled_writes_are_blocked_and_content_is_kept(): void + { + $registry = new FieldTypeRegistry(); + (new PluginLoader($this->installedAs(), [MarkdownPlugin::ID => false]))->load($registry); + + $field = new Field('body', 'Body', 'markdown'); + $stored = "# Still here\n\nWith **bold** text."; + + // Write paths refuse the type outright... + try { + $registry->get('markdown'); + self::fail('write paths must not resolve an unavailable type'); + } catch (UnknownFieldType $e) { + self::assertSame('markdown', $e->type); + } + + // ...while the admin degrades: the source is shown, never rewritten, + // and saving is refused until the package is back. + $fallback = $registry->forDisplay('markdown'); + self::assertSame($stored, $fallback->normalize($stored), 'stored source must survive byte for byte'); + self::assertNotNull($fallback->validate($field, $stored), 'saving must be blocked'); + self::assertStringContainsString('Still here', $fallback->renderInput($field, $stored)); + self::assertStringContainsString('markdown', $fallback->renderInput($field, $stored)); + } + + public function test_re_enabling_restores_the_field_type(): void + { + $path = $this->installedAs(); + + $disabled = new FieldTypeRegistry(); + (new PluginLoader($path, [MarkdownPlugin::ID => false]))->load($disabled); + self::assertFalse($disabled->has('markdown')); + + $enabled = new FieldTypeRegistry(); + (new PluginLoader($path, [MarkdownPlugin::ID => true]))->load($enabled); + + self::assertTrue($enabled->has('markdown'), 'flipping the switch back is all it takes'); + self::assertSame('markdown', $enabled->get('markdown')->type()); + } + + // ----------------------------------------------------------- conflicts + + public function test_a_second_package_cannot_take_this_plugins_id(): void + { + $manifest = $this->manifest(); + file_put_contents($this->installedJson, json_encode(['packages' => [ + ['name' => $manifest['name'], 'type' => $manifest['type'], 'extra' => $manifest['extra']], + ['name' => 'squatter/markdown', 'type' => 'nimbuscms-plugin', 'extra' => $manifest['extra']], + ]], JSON_THROW_ON_ERROR)); + + $registry = new FieldTypeRegistry(); + $loader = new PluginLoader($this->installedJson); + $diagnostics = $loader->load($registry); + + self::assertSame([MarkdownPlugin::ID => $manifest['name']], $loader->registered()); + self::assertCount(1, $diagnostics); + self::assertSame(PluginDiagnostic::DUPLICATE_ID, $diagnostics[0]->reason); + self::assertSame('squatter/markdown', $diagnostics[0]->package); + } +}