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": [
{
"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"
}
]

View File

@ -32,11 +32,6 @@ abstract class BasePlugin implements PluginInterface
protected string $iconSrc;
/**
* @var array<string,string>
*/
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<mixed>|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<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

View File

@ -26,7 +26,7 @@ class Manifest extends ManifestObject
* @var array<string,string>
*/
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',

View File

@ -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<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 */
$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);
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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
{

View File

@ -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
{

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"
}