test(plugins): add test cases for loading manifest data

This commit is contained in:
Yassine Doghri 2024-05-28 15:57:04 +00:00
parent 014facd5a1
commit e2a90def88
17 changed files with 436 additions and 45 deletions

View File

@ -33,7 +33,11 @@
}, },
"json.schemas": [ "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" "url": "/workspaces/castopod/modules/Plugins/Manifest/manifest.schema.json"
} }
] ]

View File

@ -32,11 +32,6 @@ abstract class BasePlugin implements PluginInterface
protected string $iconSrc; protected string $iconSrc;
/**
* @var array<string,string>
*/
protected array $errors = [];
protected PluginStatus $status; protected PluginStatus $status;
protected Manifest $manifest; protected Manifest $manifest;
@ -52,29 +47,11 @@ abstract class BasePlugin implements PluginInterface
// TODO: cache manifest data // TODO: cache manifest data
$manifestPath = $directory . '/manifest.json'; $manifestPath = $directory . '/manifest.json';
$manifestContents = @file_get_contents($manifestPath);
if (! $manifestContents) { $this->manifest = new Manifest($this->key);
$manifestContents = '{}'; $this->manifest->loadFromFile($manifestPath);
$this->errors['manifest'] = lang('Plugins.errors.manifestMissing', [
'manifestPath' => $manifestPath,
]);
}
/** @var array<mixed>|null $manifestData */ if ($this->getManifestErrors() !== []) {
$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 !== []) {
$this->status = PluginStatus::INVALID; $this->status = PluginStatus::INVALID;
} else { } else {
$this->status = get_plugin_option($this->key, 'active') ? PluginStatus::ACTIVE : PluginStatus::INACTIVE; $this->status = get_plugin_option($this->key, 'active') ? PluginStatus::ACTIVE : PluginStatus::INACTIVE;
@ -121,9 +98,9 @@ abstract class BasePlugin implements PluginInterface
/** /**
* @return array<string,string> * @return array<string,string>
*/ */
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 final public function isHookDeclared(string $name): bool

View File

@ -26,7 +26,7 @@ class Manifest extends ManifestObject
* @var array<string,string> * @var array<string,string>
*/ */
protected const VALIDATION_RULES = [ 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-]+)*))?$/]', '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]', 'description' => 'permit_empty|max_length[256]',
'authors' => 'permit_empty|is_list', 'authors' => 'permit_empty|is_list',

View File

@ -21,16 +21,10 @@ abstract class ManifestObject
*/ */
protected static array $errors = []; protected static array $errors = [];
/**
* @param mixed[] $data
*/
public function __construct( public function __construct(
protected readonly string $pluginKey, protected readonly string $pluginKey,
private readonly array $data,
) { ) {
self::$errors[$pluginKey] = []; self::$errors[$pluginKey] = [];
$this->load();
} }
public function __get(string $name): mixed public function __get(string $name): mixed
@ -47,14 +41,41 @@ abstract class ManifestObject
return property_exists($this, $property); 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<mixed>|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<mixed> $data
*/
public function loadData(array $data): void
{ {
/** @var Validation $validation */ /** @var Validation $validation */
$validation = service('validation'); $validation = service('validation');
$validation->setRules($this::VALIDATION_RULES); $validation->setRules($this::VALIDATION_RULES);
if (! $validation->run($this->data)) { if (! $validation->run($data)) {
foreach ($validation->getErrors() as $key => $message) { foreach ($validation->getErrors() as $key => $message) {
$this->addError($key, $message); $this->addError($key, $message);
} }

View File

@ -35,7 +35,7 @@ class Person extends ManifestObject
protected ?URI $url = null; protected ?URI $url = null;
public function __construct(string $pluginKey, array|string $data) public function loadData(array|string $data): void
{ {
if (is_string($data)) { if (is_string($data)) {
$result = preg_match(self::AUTHOR_STRING_PATTERN, $data, $matches); $result = preg_match(self::AUTHOR_STRING_PATTERN, $data, $matches);
@ -51,6 +51,6 @@ class Person extends ManifestObject
]; ];
} }
parent::__construct($pluginKey, $data); parent::loadData($data);
} }
} }

View File

@ -2,8 +2,10 @@
declare(strict_types=1); 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; use CodeIgniter\Database\Seeder;
class FakeSinglePodcastApiSeeder extends Seeder class FakeSinglePodcastApiSeeder extends Seeder

View File

@ -2,13 +2,13 @@
declare(strict_types=1); 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\Database\Seeder;
use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait; use CodeIgniter\Test\DatabaseTestTrait;
use CodeIgniter\Test\FeatureTestTrait; use CodeIgniter\Test\FeatureTestTrait;
use Tests\Support\Database\Seeds\FakeSinglePodcastApiSeeder;
class EpisodeTest extends CIUnitTestCase class EpisodeTest extends CIUnitTestCase
{ {

View File

@ -2,13 +2,13 @@
declare(strict_types=1); 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\Database\Seeder;
use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait; use CodeIgniter\Test\DatabaseTestTrait;
use CodeIgniter\Test\FeatureTestTrait; use CodeIgniter\Test\FeatureTestTrait;
use Tests\Support\Database\Seeds\FakeSinglePodcastApiSeeder;
class PodcastTest extends CIUnitTestCase class PodcastTest extends CIUnitTestCase
{ {

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Tests\Modules\Api\Rest\V1;
use CodeIgniter\Test\CIUnitTestCase;
use Modules\Plugins\Manifest\Manifest;
/**
* @internal
*/
final class ManifestTest extends CIUnitTestCase
{
public function testLoadRequiredData(): void
{
$manifest = new Manifest('acme/hello-world');
// properties have not been set yet
$this->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'));
}
}

View File

@ -0,0 +1 @@
{}

View File

@ -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."
}
]
}
}

View File

@ -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."
}
]
}
}

View File

@ -0,0 +1,4 @@
{
"name": "acme/hello-world",
"version": "1.0.0"
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Tests\Modules\Plugins\Mocks\Plugins\AcmeHelloUniverse;
use Modules\Plugins\Core\BasePlugin;
class Plugin extends BasePlugin
{
}

View File

@ -0,0 +1,4 @@
{
"name": "acme/hello-universe",
"version": "1.0.0"
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Tests\Modules\Plugins\Mocks\Plugins\AcmeHelloWorld;
use Modules\Plugins\Core\BasePlugin;
class Plugin extends BasePlugin
{
}

View File

@ -0,0 +1,4 @@
{
"name": "acme/hello-world",
"version": "1.0.0"
}