From e2a90def88a50e88a4cdedd442397f28a43ce914 Mon Sep 17 00:00:00 2001 From: Yassine Doghri Date: Tue, 28 May 2024 15:57:04 +0000 Subject: [PATCH] test(plugins): add test cases for loading manifest data --- .devcontainer/devcontainer.json | 6 +- modules/Plugins/Core/BasePlugin.php | 33 +--- modules/Plugins/Manifest/Manifest.php | 2 +- modules/Plugins/Manifest/ManifestObject.php | 37 ++++- modules/Plugins/Manifest/Person.php | 4 +- .../Seeds/FakeSinglePodcastApiSeeder.php | 4 +- tests/modules/Api/Rest/V1/EpisodeTest.php | 4 +- tests/modules/Api/Rest/V1/PodcastTest.php | 4 +- tests/modules/Plugins/ManifestTest.php | 67 ++++++++ .../Mocks/manifests/manifest-empty.json | 1 + .../manifests/manifest-full-invalid.json | 142 +++++++++++++++++ .../Mocks/manifests/manifest-full-valid.json | 143 ++++++++++++++++++ .../Mocks/manifests/manifest-required.json | 4 + .../plugins/acme/hello-universe/Plugin.php | 11 ++ .../plugins/acme/hello-universe/manifest.json | 4 + .../Mocks/plugins/acme/hello-world/Plugin.php | 11 ++ .../plugins/acme/hello-world/manifest.json | 4 + 17 files changed, 436 insertions(+), 45 deletions(-) rename {app => tests/_support}/Database/Seeds/FakeSinglePodcastApiSeeder.php (99%) create mode 100644 tests/modules/Plugins/ManifestTest.php create mode 100644 tests/modules/Plugins/Mocks/manifests/manifest-empty.json create mode 100644 tests/modules/Plugins/Mocks/manifests/manifest-full-invalid.json create mode 100644 tests/modules/Plugins/Mocks/manifests/manifest-full-valid.json create mode 100644 tests/modules/Plugins/Mocks/manifests/manifest-required.json create mode 100644 tests/modules/Plugins/Mocks/plugins/acme/hello-universe/Plugin.php create mode 100644 tests/modules/Plugins/Mocks/plugins/acme/hello-universe/manifest.json create mode 100644 tests/modules/Plugins/Mocks/plugins/acme/hello-world/Plugin.php create mode 100644 tests/modules/Plugins/Mocks/plugins/acme/hello-world/manifest.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d57408cb..1287d027 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -33,7 +33,11 @@ }, "json.schemas": [ { - "fileMatch": ["plugins/**/manifest.json"], + "fileMatch": [ + "plugins/**/manifest.json", + "tests/modules/Plugins/Mocks/manifests/*.json", + "tests/modules/Plugins/Mocks/plugins/**/manifest.json" + ], "url": "/workspaces/castopod/modules/Plugins/Manifest/manifest.schema.json" } ] diff --git a/modules/Plugins/Core/BasePlugin.php b/modules/Plugins/Core/BasePlugin.php index 09911a8c..211ed0b8 100644 --- a/modules/Plugins/Core/BasePlugin.php +++ b/modules/Plugins/Core/BasePlugin.php @@ -32,11 +32,6 @@ abstract class BasePlugin implements PluginInterface protected string $iconSrc; - /** - * @var array - */ - protected array $errors = []; - protected PluginStatus $status; protected Manifest $manifest; @@ -52,29 +47,11 @@ abstract class BasePlugin implements PluginInterface // TODO: cache manifest data $manifestPath = $directory . '/manifest.json'; - $manifestContents = @file_get_contents($manifestPath); - if (! $manifestContents) { - $manifestContents = '{}'; - $this->errors['manifest'] = lang('Plugins.errors.manifestMissing', [ - 'manifestPath' => $manifestPath, - ]); - } + $this->manifest = new Manifest($this->key); + $this->manifest->loadFromFile($manifestPath); - /** @var array|null $manifestData */ - $manifestData = json_decode($manifestContents, true); - - if ($manifestData === null) { - $manifestData = []; - $this->errors['manifest'] = lang('Plugins.errors.manifestJsonInvalid', [ - 'manifestPath' => $manifestPath, - ]); - } - - $this->manifest = new Manifest($this->key, $manifestData); - $this->errors = [...$this->errors, ...Manifest::getPluginErrors($this->key)]; - - if ($this->errors !== []) { + if ($this->getManifestErrors() !== []) { $this->status = PluginStatus::INVALID; } else { $this->status = get_plugin_option($this->key, 'active') ? PluginStatus::ACTIVE : PluginStatus::INACTIVE; @@ -121,9 +98,9 @@ abstract class BasePlugin implements PluginInterface /** * @return array */ - final public function getErrors(): array + final public function getManifestErrors(): array { - return $this->errors; + return Manifest::getPluginErrors($this->key); } final public function isHookDeclared(string $name): bool diff --git a/modules/Plugins/Manifest/Manifest.php b/modules/Plugins/Manifest/Manifest.php index 09dcbf98..eb8932e3 100644 --- a/modules/Plugins/Manifest/Manifest.php +++ b/modules/Plugins/Manifest/Manifest.php @@ -26,7 +26,7 @@ class Manifest extends ManifestObject * @var array */ protected const VALIDATION_RULES = [ - 'name' => 'required|max_length[128]', + 'name' => 'required|max_length[128]|regex_match[/^[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9]([_.-]?[a-z0-9]+)*$/]', 'version' => 'required|regex_match[/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/]', 'description' => 'permit_empty|max_length[256]', 'authors' => 'permit_empty|is_list', diff --git a/modules/Plugins/Manifest/ManifestObject.php b/modules/Plugins/Manifest/ManifestObject.php index 3dc26a47..c6a58b98 100644 --- a/modules/Plugins/Manifest/ManifestObject.php +++ b/modules/Plugins/Manifest/ManifestObject.php @@ -21,16 +21,10 @@ abstract class ManifestObject */ protected static array $errors = []; - /** - * @param mixed[] $data - */ public function __construct( protected readonly string $pluginKey, - private readonly array $data, ) { self::$errors[$pluginKey] = []; - - $this->load(); } public function __get(string $name): mixed @@ -47,14 +41,41 @@ abstract class ManifestObject return property_exists($this, $property); } - public function load(): void + public function loadFromFile(string $manifestPath): void + { + $manifestContents = @file_get_contents($manifestPath); + + if (! $manifestContents) { + $manifestContents = '{}'; + $this->addError('manifest', lang('Plugins.errors.manifestMissing', [ + 'manifestPath' => $manifestPath, + ])); + } + + /** @var array|null $manifestData */ + $manifestData = json_decode($manifestContents, true); + + if ($manifestData === null) { + $manifestData = []; + $this->addError('manifest', lang('Plugins.errors.manifestJsonInvalid', [ + 'manifestPath' => $manifestPath, + ])); + } + + $this->loadData($manifestData); + } + + /** + * @param array $data + */ + public function loadData(array $data): void { /** @var Validation $validation */ $validation = service('validation'); $validation->setRules($this::VALIDATION_RULES); - if (! $validation->run($this->data)) { + if (! $validation->run($data)) { foreach ($validation->getErrors() as $key => $message) { $this->addError($key, $message); } diff --git a/modules/Plugins/Manifest/Person.php b/modules/Plugins/Manifest/Person.php index 14514c46..3a4a0927 100644 --- a/modules/Plugins/Manifest/Person.php +++ b/modules/Plugins/Manifest/Person.php @@ -35,7 +35,7 @@ class Person extends ManifestObject protected ?URI $url = null; - public function __construct(string $pluginKey, array|string $data) + public function loadData(array|string $data): void { if (is_string($data)) { $result = preg_match(self::AUTHOR_STRING_PATTERN, $data, $matches); @@ -51,6 +51,6 @@ class Person extends ManifestObject ]; } - parent::__construct($pluginKey, $data); + parent::loadData($data); } } diff --git a/app/Database/Seeds/FakeSinglePodcastApiSeeder.php b/tests/_support/Database/Seeds/FakeSinglePodcastApiSeeder.php similarity index 99% rename from app/Database/Seeds/FakeSinglePodcastApiSeeder.php rename to tests/_support/Database/Seeds/FakeSinglePodcastApiSeeder.php index e07bf66b..a5bc51ef 100644 --- a/app/Database/Seeds/FakeSinglePodcastApiSeeder.php +++ b/tests/_support/Database/Seeds/FakeSinglePodcastApiSeeder.php @@ -2,8 +2,10 @@ declare(strict_types=1); -namespace App\Database\Seeds; +namespace Tests\Support\Database\Seeds; +use App\Database\Seeds\AppSeeder; +use App\Database\Seeds\DevSeeder; use CodeIgniter\Database\Seeder; class FakeSinglePodcastApiSeeder extends Seeder diff --git a/tests/modules/Api/Rest/V1/EpisodeTest.php b/tests/modules/Api/Rest/V1/EpisodeTest.php index ff366a4d..2dba8fed 100644 --- a/tests/modules/Api/Rest/V1/EpisodeTest.php +++ b/tests/modules/Api/Rest/V1/EpisodeTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace modules\Api\Rest\V1; +namespace Tests\Modules\Api\Rest\V1; -use App\Database\Seeds\FakeSinglePodcastApiSeeder; use CodeIgniter\Database\Seeder; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; use CodeIgniter\Test\FeatureTestTrait; +use Tests\Support\Database\Seeds\FakeSinglePodcastApiSeeder; class EpisodeTest extends CIUnitTestCase { diff --git a/tests/modules/Api/Rest/V1/PodcastTest.php b/tests/modules/Api/Rest/V1/PodcastTest.php index a70e5544..b014913a 100644 --- a/tests/modules/Api/Rest/V1/PodcastTest.php +++ b/tests/modules/Api/Rest/V1/PodcastTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace modules\Api\Rest\V1; +namespace Tests\Modules\Api\Rest\V1; -use App\Database\Seeds\FakeSinglePodcastApiSeeder; use CodeIgniter\Database\Seeder; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; use CodeIgniter\Test\FeatureTestTrait; +use Tests\Support\Database\Seeds\FakeSinglePodcastApiSeeder; class PodcastTest extends CIUnitTestCase { diff --git a/tests/modules/Plugins/ManifestTest.php b/tests/modules/Plugins/ManifestTest.php new file mode 100644 index 00000000..3c3fd5f8 --- /dev/null +++ b/tests/modules/Plugins/ManifestTest.php @@ -0,0 +1,67 @@ +assertNotEquals($manifest->name, 'acme/hello-world'); + $this->assertNotEquals($manifest->version, '1.0.0'); + + $manifest->loadFromFile(TESTPATH . 'modules/Plugins/Mocks/manifests/manifest-required.json'); + + // no errors + $this->assertEmpty($manifest->getPluginErrors('acme/hello-world')); + + // properties have been set + $this->assertEquals($manifest->name, 'acme/hello-world'); + $this->assertEquals($manifest->version, '1.0.0'); + } + + public function testLoadEmptyData(): void + { + $manifest = new Manifest('acme/hello-world'); + + $manifest->loadFromFile(TESTPATH . 'modules/Plugins/Mocks/manifests/manifest-empty.json'); + + $errors = $manifest->getPluginErrors('acme/hello-world'); + + $this->assertCount(2, $errors); + + // missing required name and version + $this->assertArrayHasKey('name', $errors); + $this->assertArrayHasKey('version', $errors); + } + + public function testLoadValidData(): void + { + $manifest = new Manifest('acme/hello-world'); + + $manifest->loadFromFile(TESTPATH . 'modules/Plugins/Mocks/manifests/manifest-full-valid.json'); + + // no errors + $this->assertEmpty($manifest->getPluginErrors('acme/hello-world')); + } + + public function testLoadInvalidData(): void + { + $manifest = new Manifest('acme/hello-world'); + + $manifest->loadFromFile(TESTPATH . 'modules/Plugins/Mocks/manifests/manifest-full-invalid.json'); + + // errors + $this->assertNotEmpty($manifest->getPluginErrors('acme/hello-world')); + } +} diff --git a/tests/modules/Plugins/Mocks/manifests/manifest-empty.json b/tests/modules/Plugins/Mocks/manifests/manifest-empty.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/tests/modules/Plugins/Mocks/manifests/manifest-empty.json @@ -0,0 +1 @@ +{} diff --git a/tests/modules/Plugins/Mocks/manifests/manifest-full-invalid.json b/tests/modules/Plugins/Mocks/manifests/manifest-full-invalid.json new file mode 100644 index 00000000..8678bed9 --- /dev/null +++ b/tests/modules/Plugins/Mocks/manifests/manifest-full-invalid.json @@ -0,0 +1,142 @@ +{ + "name": "acme/hello-world", + "description": true, + "version": "1.0.0", + "authors": [ + { + "name": "Acme Corporation", + "email": "acme@example.com", + "url": "https://example.com/" + } + ], + "homepage": "https://example.com/", + "license": ["MIT", "AGPLv3"], + "keywords": ["seo", "analytics"], + "hooks": ["rssAfterChannel"], + "settings": { + "general": [ + { + "type": "radio-group", + "key": "name", + "label": "Name", + "options": [ + { "label": "Foo", "value": "foo", "hint": "This is a hint." }, + { "label": "Bar", "value": "bar" } + ] + }, + { + "type": "email", + "key": "email", + "label": "Email" + }, + { + "type": "url", + "key": "url", + "label": "Your website URL" + }, + { + "type": "toggler", + "key": "toggler", + "label": "Toggle this?" + }, + { + "type": "number", + "key": "number", + "label": "Number" + }, + { + "type": "datetime", + "key": "datetime", + "label": "Enter a date", + "optional": true + }, + { + "type": "select", + "key": "select", + "label": "Select something", + "options": [ + { + "label": "Foo", + "value": "foo" + }, + { + "label": "Bar", + "value": "bar" + }, + { + "label": "Baz", + "value": "baz" + } + ] + }, + { + "type": "select-multiple", + "key": "select-multiple", + "label": "Select multiple things", + "options": [ + { + "label": "Foo", + "value": "foo" + }, + { + "label": "Bar", + "value": "bar" + }, + { + "label": "Baz", + "value": "baz" + } + ] + }, + { + "type": "radio-group", + "key": "radio-group", + "label": "Radio Group", + "helper": "This is a helper.", + "options": [ + { + "label": "Foo", + "value": "foo" + }, + { + "label": "Bar", + "value": "bar" + }, + { + "label": "Baz", + "value": "baz" + } + ] + }, + { + "type": "textarea", + "key": "texting", + "label": "Your text", + "hint": "This is a hint." + }, + { + "type": "markdown", + "key": "hello", + "label": "Name Podcast", + "hint": "This is a hint.", + "optional": true + } + ], + "podcast": [ + { + "type": "text", + "key": "name", + "label": "Name Podcast", + "hint": "This is a hint." + } + ], + "episode": [ + { + "type": "text", + "key": "name", + "label": "Name Episode", + "helper": "This is a helper." + } + ] + } +} diff --git a/tests/modules/Plugins/Mocks/manifests/manifest-full-valid.json b/tests/modules/Plugins/Mocks/manifests/manifest-full-valid.json new file mode 100644 index 00000000..28434aa4 --- /dev/null +++ b/tests/modules/Plugins/Mocks/manifests/manifest-full-valid.json @@ -0,0 +1,143 @@ +{ + "name": "acme/hello-world", + "description": "A Castopod plugin to add a hello world greeting to your RSS feed!", + "version": "1.0.0", + "authors": [ + { + "name": "Acme Corporation", + "email": "acme@example.com", + "url": "https://example.com/" + } + ], + "homepage": "https://example.com/", + "license": "MIT", + "keywords": ["seo", "analytics"], + "hooks": ["rssAfterChannel"], + "settings": { + "general": [ + { + "type": "radio-group", + "key": "name", + "label": "Name", + "options": [ + { "label": "Foo", "value": "foo", "hint": "This is a hint." }, + { "label": "Bar", "value": "bar" }, + { "label": "Baz", "value": "baz" } + ] + }, + { + "type": "email", + "key": "email", + "label": "Email" + }, + { + "type": "url", + "key": "url", + "label": "Your website URL" + }, + { + "type": "toggler", + "key": "toggler", + "label": "Toggle this?" + }, + { + "type": "number", + "key": "number", + "label": "Number" + }, + { + "type": "datetime", + "key": "datetime", + "label": "Enter a date", + "optional": true + }, + { + "type": "select", + "key": "select", + "label": "Select something", + "options": [ + { + "label": "Foo", + "value": "foo" + }, + { + "label": "Bar", + "value": "bar" + }, + { + "label": "Baz", + "value": "baz" + } + ] + }, + { + "type": "select-multiple", + "key": "select-multiple", + "label": "Select multiple things", + "options": [ + { + "label": "Foo", + "value": "foo" + }, + { + "label": "Bar", + "value": "bar" + }, + { + "label": "Baz", + "value": "baz" + } + ] + }, + { + "type": "radio-group", + "key": "radio-group", + "label": "Radio Group", + "helper": "This is a helper.", + "options": [ + { + "label": "Foo", + "value": "foo" + }, + { + "label": "Bar", + "value": "bar" + }, + { + "label": "Baz", + "value": "baz" + } + ] + }, + { + "type": "textarea", + "key": "texting", + "label": "Your text", + "hint": "This is a hint." + }, + { + "type": "markdown", + "key": "hello", + "label": "Name Podcast", + "hint": "This is a hint.", + "optional": true + } + ], + "podcast": [ + { + "type": "text", + "key": "name", + "label": "Name Podcast", + "hint": "This is a hint." + } + ], + "episode": [ + { + "type": "text", + "key": "name", + "label": "Name Episode", + "helper": "This is a helper." + } + ] + } +} diff --git a/tests/modules/Plugins/Mocks/manifests/manifest-required.json b/tests/modules/Plugins/Mocks/manifests/manifest-required.json new file mode 100644 index 00000000..5272f045 --- /dev/null +++ b/tests/modules/Plugins/Mocks/manifests/manifest-required.json @@ -0,0 +1,4 @@ +{ + "name": "acme/hello-world", + "version": "1.0.0" +} diff --git a/tests/modules/Plugins/Mocks/plugins/acme/hello-universe/Plugin.php b/tests/modules/Plugins/Mocks/plugins/acme/hello-universe/Plugin.php new file mode 100644 index 00000000..a94109d4 --- /dev/null +++ b/tests/modules/Plugins/Mocks/plugins/acme/hello-universe/Plugin.php @@ -0,0 +1,11 @@ +