From 184b4831034bb1d8d79d1a403b3adc5169b5cb68 Mon Sep 17 00:00:00 2001 From: Travis Garrison Date: Thu, 30 Jul 2026 13:46:46 +0400 Subject: [PATCH] refactor(tests): move PluginTestCase to Tests\\Support namespace - Relocate PluginTestCase from App\\Libraries\\Plugins to Tests\\Support - Update all references in AGENTS.md, README.md, and test files - Add TestModulePlugin fixture for module registration tests - Add PluginModuleRegistrationTest covering namespace registration - Add plugin_data_helper.js for zero-JS plugin form field collection Signed-off-by: Travis Garrison --- AGENTS.md | 2 +- app/Plugins/README.md | 4 +- public/js/plugin_data_helper.js | 35 ++++ .../Plugins/PluginModuleRegistrationTest.php | 162 ++++++++++++++++++ tests/Support/TestModulePlugin.php | 79 +++++++++ 5 files changed, 279 insertions(+), 3 deletions(-) create mode 100644 public/js/plugin_data_helper.js create mode 100644 tests/Libraries/Plugins/PluginModuleRegistrationTest.php create mode 100644 tests/Support/TestModulePlugin.php diff --git a/AGENTS.md b/AGENTS.md index 37df9164c..b87d8265c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,7 +130,7 @@ app/Plugins/{PluginName}/ └── {PluginName}Test.php # namespace App\Plugins\{PluginName}\Tests ``` -**Base class:** Extend `App\Libraries\Plugins\PluginTestCase` (registers plugin namespaces in `setUp`). Pure-PHP tests may extend `CodeIgniter\Test\CIUnitTestCase` directly. +**Base class:** Extend `Tests\Support\PluginTestCase` (registers plugin namespaces in `setUp`). Pure-PHP tests may extend `CodeIgniter\Test\CIUnitTestCase` directly. ```bash vendor/bin/phpunit --testsuite Plugins # plugins only diff --git a/app/Plugins/README.md b/app/Plugins/README.md index 727f33970..6cd4b2f95 100644 --- a/app/Plugins/README.md +++ b/app/Plugins/README.md @@ -833,7 +833,7 @@ This resolves via the existing `App\` → `app/` PSR-4 mapping in `composer.json ### Base Class -Extend `App\Libraries\Plugins\PluginTestCase` instead of `CIUnitTestCase` directly. `PluginTestCase` calls `PluginManager::registerAllNamespaces()` in `setUp()` so plugin class namespaces resolve without the `pre_system` hook that normally fires during a real request. Tests that exercise only pure PHP logic (no plugin class loading) may extend `CIUnitTestCase` directly. +Extend `Tests\Support\PluginTestCase` instead of `CIUnitTestCase` directly. `PluginTestCase` calls `PluginManager::registerAllNamespaces()` in `setUp()` so plugin class namespaces resolve without the `pre_system` hook that normally fires during a real request. Tests that exercise only pure PHP logic (no plugin class loading) may extend `CIUnitTestCase` directly. ### Example @@ -842,8 +842,8 @@ Extend `App\Libraries\Plugins\PluginTestCase` instead of `CIUnitTestCase` direct namespace App\Plugins\ExamplePlugin\Tests; -use App\Libraries\Plugins\PluginTestCase; use App\Plugins\ExamplePlugin\ExamplePlugin; +use Tests\Support\PluginTestCase; class ExamplePluginTest extends PluginTestCase { diff --git a/public/js/plugin_data_helper.js b/public/js/plugin_data_helper.js new file mode 100644 index 000000000..212c90b96 --- /dev/null +++ b/public/js/plugin_data_helper.js @@ -0,0 +1,35 @@ +/** + * Collects all [data-plugin-field] inputs on the page and injects them as a + * plugin_data JSON hidden field into any form marked with data-plugin-form + * before submission. Plugin devs only need data-plugin-field="pluginid_varname" + * on their inputs — no JS required in plugin partials. + */ +$(function() { + $('form[data-plugin-form]').on('submit', function() { + var pluginData = {}; + + $('[data-plugin-field]').each(function() { + var key = $(this).data('plugin-field'); + var $el = $(this); + var val; + + if ($el.is(':checkbox')) { + // bootstrap-toggle stores state on the original hidden input via .prop('checked') + val = $el.prop('checked'); + } else { + val = $el.val(); + } + + pluginData[key] = val; + }); + + var $existing = $(this).find('input[name="plugin_data"]'); + if ($existing.length) { + $existing.val(JSON.stringify(pluginData)); + } else { + $(this).append( + $('').attr({ type: 'hidden', name: 'plugin_data' }).val(JSON.stringify(pluginData)) + ); + } + }); +}); diff --git a/tests/Libraries/Plugins/PluginModuleRegistrationTest.php b/tests/Libraries/Plugins/PluginModuleRegistrationTest.php new file mode 100644 index 000000000..23f6aea24 --- /dev/null +++ b/tests/Libraries/Plugins/PluginModuleRegistrationTest.php @@ -0,0 +1,162 @@ +plugin = new TestModulePlugin(); + $this->configModel = new PluginConfig(); + $this->cleanupTestRows(); + } + + protected function tearDown(): void + { + $this->cleanupTestRows(); + parent::tearDown(); + } + + private function cleanupTestRows(): void + { + $db = Database::connect(); + $db->table('permissions')->like('permission_id', 'testmodule', 'after')->delete(); + $db->table('modules')->like('module_id', 'testmodule', 'after')->delete(); + $db->table('plugin_config')->where('plugin_id', 'testmodule')->delete(); + } + + private function managerWithPlugin(): PluginManager + { + $manager = new PluginManager(); + $property = new ReflectionProperty(PluginManager::class, 'plugins'); + $property->setAccessible(true); + $property->setValue($manager, ['testmodule' => $this->plugin]); + return $manager; + } + + public function testRegisterModuleWritesRows(): void + { + $this->assertTrue($this->plugin->publicRegisterModule('testmodule')); + + $db = Database::connect(); + $this->assertSame(1, $db->table('modules')->where('module_id', 'testmodule')->countAllResults()); + $this->assertSame(1, $db->table('permissions')->where('permission_id', 'testmodule')->countAllResults()); + $this->assertSame(1, $db->table('grants')->where('permission_id', 'testmodule')->where('person_id', 1)->countAllResults()); + } + + public function testRegisterModuleRejectsUnprefixedId(): void + { + $this->assertFalse($this->plugin->publicRegisterModule('unrelated')); + $this->assertSame(0, Database::connect()->table('modules')->where('module_id', 'unrelated')->countAllResults()); + } + + public function testRegisterSubPermissionRejectsUnprefixedId(): void + { + $this->plugin->publicRegisterModule('testmodule'); + $this->assertFalse($this->plugin->publicRegisterSubPermission('unrelated_extra', 'testmodule')); + $this->assertSame(0, Database::connect()->table('permissions')->where('permission_id', 'unrelated_extra')->countAllResults()); + } + + public function testDoubleRegisterIsIdempotent(): void + { + $this->plugin->publicRegisterModule('testmodule'); + $this->plugin->publicRegisterModule('testmodule'); + + $this->assertSame(1, Database::connect()->table('modules')->where('module_id', 'testmodule')->countAllResults()); + } + + public function testUnregisterModuleDeletesRows(): void + { + $this->plugin->publicRegisterModule('testmodule'); + $this->plugin->publicRegisterSubPermission('testmodule_extra', 'testmodule'); + + $this->plugin->publicUnregisterModule('testmodule'); + + $db = Database::connect(); + $this->assertSame(0, $db->table('modules')->where('module_id', 'testmodule')->countAllResults()); + $this->assertSame(0, $db->table('permissions')->where('module_id', 'testmodule')->countAllResults()); + } + + public function testUnregisterSubPermissionDeletesRow(): void + { + $this->plugin->publicRegisterModule('testmodule'); + $this->plugin->publicRegisterSubPermission('testmodule_extra', 'testmodule'); + + $this->plugin->publicUnregisterSubPermission('testmodule_extra'); + + $this->assertSame(0, Database::connect()->table('permissions')->where('permission_id', 'testmodule_extra')->countAllResults()); + } + + public function testUninstallAutoUnregistersByConvention(): void + { + $manager = $this->managerWithPlugin(); + $manager->enablePlugin('testmodule'); + + $db = Database::connect(); + $this->assertSame(1, $db->table('modules')->where('module_id', 'testmodule')->countAllResults()); + $this->assertSame(1, $db->table('permissions')->where('permission_id', 'testmodule_extra')->countAllResults()); + + $this->assertTrue($manager->uninstallPlugin('testmodule')); + + $this->assertSame(0, $db->table('modules')->where('module_id', 'testmodule')->countAllResults()); + $this->assertSame(0, $db->table('permissions')->like('permission_id', 'testmodule', 'after')->countAllResults()); + $this->assertSame('0', $this->configModel->getValue('testmodule', 'installed')); + } + + public function testUninstallCleansLegacyInstallWithNoRegistrationRecord(): void + { + // Simulate a plugin installed before automatic cleanup existed: + // module rows present, installed=1, nothing else recorded anywhere. + $db = Database::connect(); + $db->table('modules')->insert([ + 'module_id' => 'testmodule', + 'name_lang_key' => 'module_testmodule', + 'desc_lang_key' => 'module_testmodule_desc', + 'sort' => 500, + ]); + $db->table('permissions')->insert([ + 'permission_id' => 'testmodule', + 'module_id' => 'testmodule', + ]); + $this->configModel->setValue('testmodule', 'installed', '1', true); + + $manager = $this->managerWithPlugin(); + $this->assertTrue($manager->uninstallPlugin('testmodule')); + + $this->assertSame(0, $db->table('modules')->where('module_id', 'testmodule')->countAllResults()); + $this->assertSame(0, $db->table('permissions')->where('module_id', 'testmodule')->countAllResults()); + } + + public function testUninstallDoesNotTouchOtherModules(): void + { + $manager = $this->managerWithPlugin(); + $manager->enablePlugin('testmodule'); + + $db = Database::connect(); + $coreModulesBefore = $db->table('modules')->countAllResults(); + + $manager->uninstallPlugin('testmodule'); + + $this->assertSame($coreModulesBefore - 1, $db->table('modules')->countAllResults()); + $this->assertSame(1, $db->table('modules')->where('module_id', 'sales')->countAllResults()); + } +} diff --git a/tests/Support/TestModulePlugin.php b/tests/Support/TestModulePlugin.php new file mode 100644 index 000000000..7cf6b05c6 --- /dev/null +++ b/tests/Support/TestModulePlugin.php @@ -0,0 +1,79 @@ +registerModule('testmodule', 500, 'office'); + return $ok && $this->registerSubPermission('testmodule_extra', 'testmodule'); + } + + public function uninstall(): bool + { + return true; + } + + protected function log(string $level, string $message): void + { + // No-op in tests: results come from PHPUnit, not log files. + } + + public function publicRegisterModule(string $module_id): bool + { + return $this->registerModule($module_id); + } + + public function publicUnregisterModule(string $module_id): bool + { + return $this->unregisterModule($module_id); + } + + public function publicRegisterSubPermission(string $permission_id, string $module_id): bool + { + return $this->registerSubPermission($permission_id, $module_id); + } + + public function publicUnregisterSubPermission(string $permission_id): bool + { + return $this->unregisterSubPermission($permission_id); + } +}