From 8ee2bcfd9ac45f26054d95139781da1b9a6875a6 Mon Sep 17 00:00:00 2001 From: Sam Poyigi <6567634+sampoyigi@users.noreply.github.com> Date: Sat, 25 Jan 2025 10:35:22 +0000 Subject: [PATCH] chore: update test scripts for improved test coverage Signed-off-by: Sam Poyigi <6567634+sampoyigi@users.noreply.github.com> --- phpstan.neon.dist | 9 +- phpunit.xml.dist | 15 +- rector.php | 15 + tests/Pest.php | 18 +- tests/resources/css/style.css | 0 tests/resources/js/script.js | 0 tests/resources/models/test_settings.php | 14 + tests/resources/scss/style.scss | 4 + .../_content/test-content.blade.php | 1 + .../_layouts/default-with-lifecycle.blade.php | 12 + .../tests-theme/_layouts/default.blade.php | 12 + .../themes/tests-theme/_meta/blueprint.json | 11 - .../themes/tests-theme/_meta/fields.php | 17 + .../tests-theme/_pages/components.blade.php | 11 +- .../_pages/layout-with-lifecycle.blade.php | 13 + .../_pages/page-with-lifecycle.blade.php | 13 + .../_partials/test-partial.blade.php | 1 + .../themes/tests-theme/public/style.css | 0 .../testcomponent/default.blade.php | 1 + .../views/_mail/test_layout.blade.php | 3 + .../views/_mail/test_partial.blade.php | 3 + .../views/_partials/test-partial.blade.php | 2 +- tests/resources/views/layout.blade.php | 7 + tests/resources/views/test.blade.php | 4 +- .../views/testcontroller/index.blade.php | 0 .../view-with-exception.blade.php | 3 + .../view-with-throwable.blade.php | 3 + .../Admin/BulkActionWidgets/DeleteTest.php | 15 + .../Admin/BulkActionWidgets/StatusTest.php | 17 + .../src/Admin/Classes/AdminControllerTest.php | 318 ++++++++++-- .../Classes/BaseBulkActionWidgetTest.php | 27 +- .../Admin/Classes/BaseDashboardWidgetTest.php | 67 +++ .../src/Admin/Classes/BaseFormWidgetTest.php | 32 +- .../Admin/Classes/BaseMainMenuWidgetTest.php | 18 +- tests/src/Admin/Classes/BaseWidgetTest.php | 13 +- tests/src/Admin/Classes/FilterScopeTest.php | 47 +- tests/src/Admin/Classes/FormFieldTest.php | 195 +++++++- tests/src/Admin/Classes/FormTabsTest.php | 36 +- tests/src/Admin/Classes/MainMenuItemTest.php | 48 +- tests/src/Admin/Classes/NavigationTest.php | 197 +++++++- .../src/Admin/Classes/OnboardingStepsTest.php | 35 +- tests/src/Admin/Classes/TemplateTest.php | 15 +- tests/src/Admin/Classes/ToolbarButtonTest.php | 10 +- tests/src/Admin/Classes/WidgetsTest.php | 58 ++- .../src/Admin/DashboardWidgets/ChartsTest.php | 136 +++++- .../Admin/DashboardWidgets/StatisticsTest.php | 19 +- .../src/Admin/FormWidgets/CodeEditorTest.php | 29 +- .../src/Admin/FormWidgets/ColorPickerTest.php | 25 +- tests/src/Admin/FormWidgets/ConnectorTest.php | 215 ++++++--- tests/src/Admin/FormWidgets/DataTableTest.php | 102 +++- .../src/Admin/FormWidgets/DatePickerTest.php | 99 +++- .../Admin/FormWidgets/MarkdownEditorTest.php | 14 +- .../Admin/FormWidgets/RecordEditorTest.php | 177 +++++-- tests/src/Admin/FormWidgets/RelationTest.php | 98 +++- tests/src/Admin/FormWidgets/RepeaterTest.php | 105 ++-- .../src/Admin/FormWidgets/RichEditorTest.php | 17 +- .../Admin/FormWidgets/StatusEditorTest.php | 216 ++++++--- .../Http/Actions/CalendarControllerTest.php | 23 +- .../Admin/Http/Actions/FormControllerTest.php | 201 +++++++- .../Admin/Http/Actions/ListControllerTest.php | 89 +++- .../Admin/Http/Controllers/DashboardTest.php | 17 + .../Admin/Http/Controllers/StatusesTest.php | 83 ++++ .../Admin/Http/Requests/StatusRequestTest.php | 33 ++ tests/src/Admin/Http/Requests/StatusTest.php | 21 - .../Models/Concerns/GeneratesHashTest.php | 50 ++ .../Models/Concerns/LogsStatusHistoryTest.php | 162 +++++++ tests/src/Admin/Models/StatusHistoryTest.php | 111 +++++ tests/src/Admin/Models/StatusTest.php | 63 +++ .../StatusUpdatedNotificationTest.php | 167 +++++++ .../PermissionServiceProviderTest.php | 16 + .../Admin/Traits/ControllerHelpersTest.php | 53 ++ .../src/Admin/Traits/ControllerUtilsTest.php | 107 +++++ tests/src/Admin/Traits/FormExtendableTest.php | 38 ++ .../src/Admin/Traits/FormModelWidgetTest.php | 290 +++++++++++ tests/src/Admin/Traits/ListExtendableTest.php | 38 ++ tests/src/Admin/Traits/ValidatesFormTest.php | 184 +++++++ tests/src/Admin/Traits/WidgetMakerTest.php | 65 +++ tests/src/Admin/Widgets/CalendarTest.php | 35 +- .../Admin/Widgets/DashboardContainerTest.php | 75 ++- tests/src/Admin/Widgets/FilterTest.php | 216 ++++++++- tests/src/Admin/Widgets/FormTest.php | 393 +++++++++++++-- tests/src/Admin/Widgets/ListsTest.php | 454 ++++++++++++++++-- tests/src/Admin/Widgets/MenuTest.php | 51 +- tests/src/Admin/Widgets/TableTest.php | 85 +++- tests/src/Admin/Widgets/ToolbarTest.php | 63 ++- .../FormExtendableTestController.php | 18 + .../ListExtendableTestController.php | 20 + .../Controllers/ThemeTestController.php | 49 ++ tests/src/Fixtures/Requests/TestRequest.php | 22 + .../Fixtures/Widgets/TestDashboardWidget.php | 26 + tests/src/Fixtures/Widgets/TestFormWidget.php | 20 + tests/src/Fixtures/extension/composer.json | 17 + .../src/Fixtures/extension/src/Extension.php | 10 + tests/src/Flame/Composer/ManagerTest.php | 59 +++ tests/src/Main/Classes/MainControllerTest.php | 310 +++++++++++- tests/src/Main/Classes/MediaItemTest.php | 59 +++ tests/src/Main/Classes/MediaLibraryTest.php | 268 +++++++++++ tests/src/Main/Classes/RouteRegistrarTest.php | 31 ++ .../SupportConfigurableComponentTest.php | 96 ++++ tests/src/Main/Classes/ThemeManagerTest.php | 345 ++++++++++++- tests/src/Main/Classes/ThemeTest.php | 313 ++++++++++++ .../Main/Components/BlankComponentTest.php | 12 + tests/src/Main/Components/ViewBagTest.php | 34 ++ tests/src/Main/FormWidgets/ComponentsTest.php | 279 +++++++++++ .../src/Main/FormWidgets/MediaFinderTest.php | 272 +++++++++++ .../Main/FormWidgets/TemplateEditorTest.php | 207 ++++++++ tests/src/Main/Helpers/ImageHelperTest.php | 44 ++ .../Http/Controllers/MediaManagerTest.php | 10 + .../src/Main/Http/Controllers/ThemesTest.php | 141 ++++++ .../Http/Middleware/CheckMaintenanceTest.php | 44 ++ .../Main/Http/Requests/ThemeRequestTest.php | 55 +++ tests/src/Main/Models/ThemeTest.php | 144 ++++++ tests/src/System/Actions/ModelActionTest.php | 29 ++ .../src/System/Actions/SettingsModelTest.php | 91 ++++ .../src/System/Classes/BaseComponentTest.php | 110 +++++ .../src/System/Classes/BaseExtensionTest.php | 70 +++ .../System/Classes/ComponentManagerTest.php | 169 +++++++ .../System/Classes/ComposerManagerTest.php | 19 - .../System/Classes/ControllerActionTest.php | 60 +++ .../System/Classes/ExtensionManagerTest.php | 330 ++++++++++++- tests/src/System/Classes/FormRequestTest.php | 22 + tests/src/System/Classes/HubManagerTest.php | 191 ++++++++ .../System/Classes/LanguageManagerTest.php | 166 +++++++ tests/src/System/Classes/MailManagerTest.php | 120 ++++- tests/src/System/Classes/PackageInfoTest.php | 63 +++ .../System/Classes/PackageManifestTest.php | 162 +++++++ .../src/System/Classes/UpdateManagerTest.php | 397 ++++++++++++++- .../Console/Commands/ExtensionInstallTest.php | 74 +++ .../Console/Commands/ExtensionRefreshTest.php | 44 ++ .../Console/Commands/ExtensionRemoveTest.php | 57 +++ .../Console/Commands/IgniterDownTest.php | 23 + .../Console/Commands/IgniterInstallTest.php | 125 +++++ .../Commands/IgniterPackageDiscoverTest.php | 40 ++ .../Console/Commands/IgniterPasswdTest.php | 46 ++ .../System/Console/Commands/IgniterUpTest.php | 79 +++ .../Console/Commands/IgniterUpdateTest.php | 222 +++++++++ .../Console/Commands/IgniterUtilTest.php | 110 +++++ .../Console/Commands/LanguageInstallTest.php | 60 +++ .../Console/Commands/ThemeInstallTest.php | 62 +++ .../Console/Commands/ThemePublishTest.php | 69 +++ .../Console/Commands/ThemeRemoveTest.php | 51 ++ .../Commands/ThemeVendorPublishTest.php | 51 ++ .../src/System/DashboardWidgets/CacheTest.php | 35 ++ .../src/System/DashboardWidgets/NewsTest.php | 47 ++ .../ConsoleSubscriberTest.php | 82 ++++ tests/src/System/Fixtures/.env | 0 .../System/Fixtures/TestBladeComponent.php | 25 + tests/src/System/Fixtures/TestComponent.php | 49 ++ .../Fixtures/TestComponentWithLifecycle.php | 20 + .../Fixtures/TestExtensionSettingsModel.php | 14 + .../TestExtensionSettingsWithRulesModel.php | 26 + .../System/Fixtures/TestLivewireComponent.php | 20 + tests/src/System/Fixtures/composer.json | 22 + tests/src/System/Helpers/CacheHelperTest.php | 76 +++ tests/src/System/Helpers/MailHelperTest.php | 40 ++ tests/src/System/Helpers/SystemHelperTest.php | 110 +++++ .../Http/Controllers/AssetControllerTest.php | 20 + .../System/Http/Controllers/CountriesTest.php | 108 +++++ .../Http/Controllers/CurrenciesTest.php | 98 ++++ .../Http/Controllers/ExtensionsTest.php | 391 +++++++++++++++ .../System/Http/Controllers/LanguagesTest.php | 266 ++++++++++ .../Http/Controllers/MailLayoutsTest.php | 92 ++++ .../Http/Controllers/MailPartialsTest.php | 90 ++++ .../Http/Controllers/MailTemplatesTest.php | 118 +++++ .../Http/Controllers/RequestLogsTest.php | 25 + .../System/Http/Controllers/SettingsTest.php | 130 +++++ .../Http/Controllers/SystemLogsTest.php | 25 + .../System/Http/Controllers/UpdatesTest.php | 21 + .../Http/Middleware/CheckRequirementsTest.php | 40 ++ .../System/Http/Middleware/PoweredByTest.php | 46 +- .../Requests/AdvancedSettingsRequestTest.php | 27 ++ .../Http/Requests/CountryRequestTest.php | 27 ++ .../src/System/Http/Requests/CountryTest.php | 24 - .../Http/Requests/CurrencyRequestTest.php | 35 ++ .../src/System/Http/Requests/CurrencyTest.php | 25 - .../Requests/GeneralSettingsRequestTest.php | 31 ++ .../Http/Requests/LanguageRequestTest.php | 26 + .../src/System/Http/Requests/LanguageTest.php | 23 - .../Http/Requests/MailLayoutRequestTest.php | 27 ++ .../System/Http/Requests/MailLayoutTest.php | 50 -- .../Http/Requests/MailPartialRequestTest.php | 23 + .../System/Http/Requests/MailPartialTest.php | 45 -- .../Http/Requests/MailSettingsRequestTest.php | 44 ++ .../Http/Requests/MailTemplateRequestTest.php | 26 + .../System/Http/Requests/MailTemplateTest.php | 55 --- tests/src/System/Libraries/AssetsTest.php | 136 ++++++ tests/src/System/Libraries/CountryTest.php | 125 +++-- .../Mail/AnonymousTemplateMailableTest.php | 58 +++ .../src/System/Mail/TemplateMailableTest.php | 76 +++ .../Models/Concerns/DefaultableTest.php | 112 +++++ .../System/Models/Concerns/HasCountryTest.php | 42 ++ .../Models/Concerns/SendsMailTemplateTest.php | 87 ++++ .../System/Models/Concerns/SwitchableTest.php | 70 +++ tests/src/System/Models/CountryTest.php | 52 ++ tests/src/System/Models/CurrencyTest.php | 93 ++++ tests/src/System/Models/ExtensionTest.php | 317 ++++++++++++ tests/src/System/Models/LanguageTest.php | 158 ++++++ tests/src/System/Models/MailLayoutTest.php | 124 +++++ tests/src/System/Models/MailPartialTest.php | 102 ++++ tests/src/System/Models/MailTemplateTest.php | 123 +++++ tests/src/System/Models/MailThemeTest.php | 79 ++- .../Models/Observers/LanguageObserverTest.php | 25 + tests/src/System/Models/PageTest.php | 40 ++ tests/src/System/Models/RequestLogTest.php | 62 +++ tests/src/System/Models/SettingsTest.php | 192 ++++++++ tests/src/System/Models/TranslationTest.php | 14 + .../UpdateFoundNotificationTest.php | 67 +++ .../Providers/EventServiceProviderTest.php | 29 ++ .../ExtensionServiceProviderTest.php | 48 ++ .../ValidationServiceProviderTest.php | 24 + tests/src/System/Traits/AssetMakerTest.php | 117 +++++ .../src/System/Traits/CombinesAssetsTest.php | 88 ++++ tests/src/System/Traits/ConfigMakerTest.php | 124 +++++ .../src/System/Traits/ManagesUpdatesTest.php | 398 +++++++++++++++ .../System/Traits/PropertyContainerTest.php | 95 ++++ tests/src/System/Traits/SessionMakerTest.php | 105 ++++ tests/src/System/Traits/ViewMakerTest.php | 194 ++++++++ tests/src/TestCase.php | 11 + 218 files changed, 16701 insertions(+), 1002 deletions(-) create mode 100644 rector.php create mode 100644 tests/resources/css/style.css create mode 100644 tests/resources/js/script.js create mode 100644 tests/resources/models/test_settings.php create mode 100644 tests/resources/scss/style.scss create mode 100644 tests/resources/themes/tests-theme/_content/test-content.blade.php create mode 100644 tests/resources/themes/tests-theme/_layouts/default-with-lifecycle.blade.php delete mode 100644 tests/resources/themes/tests-theme/_meta/blueprint.json create mode 100644 tests/resources/themes/tests-theme/_meta/fields.php create mode 100644 tests/resources/themes/tests-theme/_pages/layout-with-lifecycle.blade.php create mode 100644 tests/resources/themes/tests-theme/_pages/page-with-lifecycle.blade.php create mode 100644 tests/resources/themes/tests-theme/_partials/test-partial.blade.php create mode 100644 tests/resources/themes/tests-theme/public/style.css create mode 100644 tests/resources/views/_components/testcomponent/default.blade.php create mode 100644 tests/resources/views/_mail/test_layout.blade.php create mode 100644 tests/resources/views/_mail/test_partial.blade.php create mode 100644 tests/resources/views/layout.blade.php create mode 100644 tests/resources/views/testcontroller/index.blade.php create mode 100644 tests/resources/views/testcontroller/view-with-exception.blade.php create mode 100644 tests/resources/views/testcontroller/view-with-throwable.blade.php create mode 100644 tests/src/Admin/Http/Controllers/DashboardTest.php create mode 100644 tests/src/Admin/Http/Controllers/StatusesTest.php create mode 100644 tests/src/Admin/Http/Requests/StatusRequestTest.php delete mode 100644 tests/src/Admin/Http/Requests/StatusTest.php create mode 100644 tests/src/Admin/Models/Concerns/GeneratesHashTest.php create mode 100644 tests/src/Admin/Models/Concerns/LogsStatusHistoryTest.php create mode 100644 tests/src/Admin/Models/StatusHistoryTest.php create mode 100644 tests/src/Admin/Models/StatusTest.php create mode 100644 tests/src/Admin/Notifications/StatusUpdatedNotificationTest.php create mode 100644 tests/src/Admin/Providers/PermissionServiceProviderTest.php create mode 100644 tests/src/Admin/Traits/ControllerHelpersTest.php create mode 100644 tests/src/Admin/Traits/ControllerUtilsTest.php create mode 100644 tests/src/Admin/Traits/FormExtendableTest.php create mode 100644 tests/src/Admin/Traits/FormModelWidgetTest.php create mode 100644 tests/src/Admin/Traits/ListExtendableTest.php create mode 100644 tests/src/Admin/Traits/ValidatesFormTest.php create mode 100644 tests/src/Admin/Traits/WidgetMakerTest.php create mode 100644 tests/src/Fixtures/Controllers/FormExtendableTestController.php create mode 100644 tests/src/Fixtures/Controllers/ListExtendableTestController.php create mode 100644 tests/src/Fixtures/Controllers/ThemeTestController.php create mode 100644 tests/src/Fixtures/Requests/TestRequest.php create mode 100644 tests/src/Fixtures/Widgets/TestDashboardWidget.php create mode 100644 tests/src/Fixtures/Widgets/TestFormWidget.php create mode 100644 tests/src/Fixtures/extension/composer.json create mode 100644 tests/src/Fixtures/extension/src/Extension.php create mode 100644 tests/src/Flame/Composer/ManagerTest.php create mode 100644 tests/src/Main/Classes/MediaItemTest.php create mode 100644 tests/src/Main/Classes/MediaLibraryTest.php create mode 100644 tests/src/Main/Classes/RouteRegistrarTest.php create mode 100644 tests/src/Main/Classes/SupportConfigurableComponentTest.php create mode 100644 tests/src/Main/Classes/ThemeTest.php create mode 100644 tests/src/Main/Components/BlankComponentTest.php create mode 100644 tests/src/Main/Components/ViewBagTest.php create mode 100644 tests/src/Main/FormWidgets/ComponentsTest.php create mode 100644 tests/src/Main/FormWidgets/MediaFinderTest.php create mode 100644 tests/src/Main/FormWidgets/TemplateEditorTest.php create mode 100644 tests/src/Main/Helpers/ImageHelperTest.php create mode 100644 tests/src/Main/Http/Controllers/MediaManagerTest.php create mode 100644 tests/src/Main/Http/Controllers/ThemesTest.php create mode 100644 tests/src/Main/Http/Middleware/CheckMaintenanceTest.php create mode 100644 tests/src/Main/Http/Requests/ThemeRequestTest.php create mode 100644 tests/src/Main/Models/ThemeTest.php create mode 100644 tests/src/System/Actions/ModelActionTest.php create mode 100644 tests/src/System/Actions/SettingsModelTest.php create mode 100644 tests/src/System/Classes/BaseComponentTest.php create mode 100644 tests/src/System/Classes/BaseExtensionTest.php create mode 100644 tests/src/System/Classes/ComponentManagerTest.php delete mode 100644 tests/src/System/Classes/ComposerManagerTest.php create mode 100644 tests/src/System/Classes/ControllerActionTest.php create mode 100644 tests/src/System/Classes/FormRequestTest.php create mode 100644 tests/src/System/Classes/HubManagerTest.php create mode 100644 tests/src/System/Classes/LanguageManagerTest.php create mode 100644 tests/src/System/Classes/PackageInfoTest.php create mode 100644 tests/src/System/Classes/PackageManifestTest.php create mode 100644 tests/src/System/Console/Commands/ExtensionInstallTest.php create mode 100644 tests/src/System/Console/Commands/ExtensionRefreshTest.php create mode 100644 tests/src/System/Console/Commands/ExtensionRemoveTest.php create mode 100644 tests/src/System/Console/Commands/IgniterDownTest.php create mode 100644 tests/src/System/Console/Commands/IgniterInstallTest.php create mode 100644 tests/src/System/Console/Commands/IgniterPackageDiscoverTest.php create mode 100644 tests/src/System/Console/Commands/IgniterPasswdTest.php create mode 100644 tests/src/System/Console/Commands/IgniterUpTest.php create mode 100644 tests/src/System/Console/Commands/IgniterUpdateTest.php create mode 100644 tests/src/System/Console/Commands/IgniterUtilTest.php create mode 100644 tests/src/System/Console/Commands/LanguageInstallTest.php create mode 100644 tests/src/System/Console/Commands/ThemeInstallTest.php create mode 100644 tests/src/System/Console/Commands/ThemePublishTest.php create mode 100644 tests/src/System/Console/Commands/ThemeRemoveTest.php create mode 100644 tests/src/System/Console/Commands/ThemeVendorPublishTest.php create mode 100644 tests/src/System/DashboardWidgets/CacheTest.php create mode 100644 tests/src/System/DashboardWidgets/NewsTest.php create mode 100644 tests/src/System/EventSubscribers/ConsoleSubscriberTest.php create mode 100644 tests/src/System/Fixtures/.env create mode 100644 tests/src/System/Fixtures/TestBladeComponent.php create mode 100644 tests/src/System/Fixtures/TestComponent.php create mode 100644 tests/src/System/Fixtures/TestComponentWithLifecycle.php create mode 100644 tests/src/System/Fixtures/TestExtensionSettingsModel.php create mode 100644 tests/src/System/Fixtures/TestExtensionSettingsWithRulesModel.php create mode 100644 tests/src/System/Fixtures/TestLivewireComponent.php create mode 100644 tests/src/System/Fixtures/composer.json create mode 100644 tests/src/System/Helpers/CacheHelperTest.php create mode 100644 tests/src/System/Helpers/MailHelperTest.php create mode 100644 tests/src/System/Helpers/SystemHelperTest.php create mode 100644 tests/src/System/Http/Controllers/AssetControllerTest.php create mode 100644 tests/src/System/Http/Controllers/CountriesTest.php create mode 100644 tests/src/System/Http/Controllers/CurrenciesTest.php create mode 100644 tests/src/System/Http/Controllers/ExtensionsTest.php create mode 100644 tests/src/System/Http/Controllers/LanguagesTest.php create mode 100644 tests/src/System/Http/Controllers/MailLayoutsTest.php create mode 100644 tests/src/System/Http/Controllers/MailPartialsTest.php create mode 100644 tests/src/System/Http/Controllers/MailTemplatesTest.php create mode 100644 tests/src/System/Http/Controllers/RequestLogsTest.php create mode 100644 tests/src/System/Http/Controllers/SettingsTest.php create mode 100644 tests/src/System/Http/Controllers/SystemLogsTest.php create mode 100644 tests/src/System/Http/Controllers/UpdatesTest.php create mode 100644 tests/src/System/Http/Middleware/CheckRequirementsTest.php create mode 100644 tests/src/System/Http/Requests/AdvancedSettingsRequestTest.php create mode 100644 tests/src/System/Http/Requests/CountryRequestTest.php delete mode 100644 tests/src/System/Http/Requests/CountryTest.php create mode 100644 tests/src/System/Http/Requests/CurrencyRequestTest.php delete mode 100644 tests/src/System/Http/Requests/CurrencyTest.php create mode 100644 tests/src/System/Http/Requests/GeneralSettingsRequestTest.php create mode 100644 tests/src/System/Http/Requests/LanguageRequestTest.php delete mode 100644 tests/src/System/Http/Requests/LanguageTest.php create mode 100644 tests/src/System/Http/Requests/MailLayoutRequestTest.php delete mode 100644 tests/src/System/Http/Requests/MailLayoutTest.php create mode 100644 tests/src/System/Http/Requests/MailPartialRequestTest.php delete mode 100644 tests/src/System/Http/Requests/MailPartialTest.php create mode 100644 tests/src/System/Http/Requests/MailSettingsRequestTest.php create mode 100644 tests/src/System/Http/Requests/MailTemplateRequestTest.php delete mode 100644 tests/src/System/Http/Requests/MailTemplateTest.php create mode 100644 tests/src/System/Libraries/AssetsTest.php create mode 100644 tests/src/System/Mail/AnonymousTemplateMailableTest.php create mode 100644 tests/src/System/Mail/TemplateMailableTest.php create mode 100644 tests/src/System/Models/Concerns/DefaultableTest.php create mode 100644 tests/src/System/Models/Concerns/HasCountryTest.php create mode 100644 tests/src/System/Models/Concerns/SendsMailTemplateTest.php create mode 100644 tests/src/System/Models/Concerns/SwitchableTest.php create mode 100644 tests/src/System/Models/CountryTest.php create mode 100644 tests/src/System/Models/CurrencyTest.php create mode 100644 tests/src/System/Models/ExtensionTest.php create mode 100644 tests/src/System/Models/LanguageTest.php create mode 100644 tests/src/System/Models/MailLayoutTest.php create mode 100644 tests/src/System/Models/MailPartialTest.php create mode 100644 tests/src/System/Models/MailTemplateTest.php create mode 100644 tests/src/System/Models/Observers/LanguageObserverTest.php create mode 100644 tests/src/System/Models/PageTest.php create mode 100644 tests/src/System/Models/RequestLogTest.php create mode 100644 tests/src/System/Models/SettingsTest.php create mode 100644 tests/src/System/Models/TranslationTest.php create mode 100644 tests/src/System/Notifications/UpdateFoundNotificationTest.php create mode 100644 tests/src/System/Providers/EventServiceProviderTest.php create mode 100644 tests/src/System/Providers/ExtensionServiceProviderTest.php create mode 100644 tests/src/System/Providers/ValidationServiceProviderTest.php create mode 100644 tests/src/System/Traits/AssetMakerTest.php create mode 100644 tests/src/System/Traits/CombinesAssetsTest.php create mode 100644 tests/src/System/Traits/ConfigMakerTest.php create mode 100644 tests/src/System/Traits/ManagesUpdatesTest.php create mode 100644 tests/src/System/Traits/PropertyContainerTest.php create mode 100644 tests/src/System/Traits/SessionMakerTest.php create mode 100644 tests/src/System/Traits/ViewMakerTest.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 9e72c4d7..3c20d3b8 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,13 +1,14 @@ includes: + - ./vendor/larastan/larastan/extension.neon - phpstan-baseline.neon parameters: level: 5 paths: - - src/ - - config/ - - database/ - - resources/ + - src + - config + - database + - resources treatPhpDocTypesAsCertain: false # ignoreErrors: # - '#PHPDoc tag @var#' diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 20750623..acf3770f 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,18 +1,21 @@ +> - ./tests + tests + + + + ./src + + diff --git a/rector.php b/rector.php new file mode 100644 index 00000000..76ef8bdd --- /dev/null +++ b/rector.php @@ -0,0 +1,15 @@ +withPaths([__DIR__.'/src', __DIR__.'/tests']) + ->withSkip([ + ReturnTypeFromStrictNewArrayRector::class, + ]) + ->withTypeCoverageLevel(1) + ->withDeadCodeLevel(0) + ->withCodeQualityLevel(0); diff --git a/tests/Pest.php b/tests/Pest.php index 0a152264..26b95c05 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,22 +1,20 @@ in(__DIR__.'/src'); +pest()->group('admin')->in('src/Admin'); +pest()->group('main')->in('src/Main'); +pest()->group('system')->in('src/System'); +pest()->group('flame')->in('src/Flame'); + function testThemePath() { return realpath(__DIR__.'/resources/themes/tests-theme'); } -function createRequest($uri, $routeName) +function actingAsSuperUser() { - $request = new Request([], [], [], [], [], ['REQUEST_URI' => $uri]); - - $request->setRouteResolver(function() use ($uri, $routeName, $request) { - return (new Route('GET', $uri, ['as' => $routeName]))->bind($request); - }); - - return $request; + return test()->actingAs(User::factory()->superUser()->create(), 'igniter-admin'); } diff --git a/tests/resources/css/style.css b/tests/resources/css/style.css new file mode 100644 index 00000000..e69de29b diff --git a/tests/resources/js/script.js b/tests/resources/js/script.js new file mode 100644 index 00000000..e69de29b diff --git a/tests/resources/models/test_settings.php b/tests/resources/models/test_settings.php new file mode 100644 index 00000000..cfef1c30 --- /dev/null +++ b/tests/resources/models/test_settings.php @@ -0,0 +1,14 @@ + [ + 'toolbar' => [], + 'fields' => [ + 'name' => [ + 'label' => 'Name', + 'type' => 'text', + 'span' => 'left', + ], + ], + ], +]; diff --git a/tests/resources/scss/style.scss b/tests/resources/scss/style.scss new file mode 100644 index 00000000..f0703875 --- /dev/null +++ b/tests/resources/scss/style.scss @@ -0,0 +1,4 @@ +body { + background-color: #f00; + color: #fff; +} diff --git a/tests/resources/themes/tests-theme/_content/test-content.blade.php b/tests/resources/themes/tests-theme/_content/test-content.blade.php new file mode 100644 index 00000000..583e9bec --- /dev/null +++ b/tests/resources/themes/tests-theme/_content/test-content.blade.php @@ -0,0 +1 @@ +This is a test content \ No newline at end of file diff --git a/tests/resources/themes/tests-theme/_layouts/default-with-lifecycle.blade.php b/tests/resources/themes/tests-theme/_layouts/default-with-lifecycle.blade.php new file mode 100644 index 00000000..2b7712bc --- /dev/null +++ b/tests/resources/themes/tests-theme/_layouts/default-with-lifecycle.blade.php @@ -0,0 +1,12 @@ +--- +name: Default Layout + +'[testComponentWithLifecycle]': +--- + + + +@themePage + + + diff --git a/tests/resources/themes/tests-theme/_layouts/default.blade.php b/tests/resources/themes/tests-theme/_layouts/default.blade.php index e69de29b..774447a6 100644 --- a/tests/resources/themes/tests-theme/_layouts/default.blade.php +++ b/tests/resources/themes/tests-theme/_layouts/default.blade.php @@ -0,0 +1,12 @@ +--- +name: Default Layout + +'[testComponent]': +--- + + + +@themePage + + + diff --git a/tests/resources/themes/tests-theme/_meta/blueprint.json b/tests/resources/themes/tests-theme/_meta/blueprint.json deleted file mode 100644 index 4320ee51..00000000 --- a/tests/resources/themes/tests-theme/_meta/blueprint.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "_pages": { - "nested-page": { - "title": "Nested page", - "layout": "default", - "permalink": "/nested/page/:slug" - }, - "components": { - } - } -} \ No newline at end of file diff --git a/tests/resources/themes/tests-theme/_meta/fields.php b/tests/resources/themes/tests-theme/_meta/fields.php new file mode 100644 index 00000000..3549f3cc --- /dev/null +++ b/tests/resources/themes/tests-theme/_meta/fields.php @@ -0,0 +1,17 @@ + [ + 'general' => [ + 'title' => 'lang:igniter.tests::default.theme.label_title', + 'fields' => [ + 'field1' => [ + 'label' => 'lang:igniter.tests::default.theme.label_field1', + 'type' => 'text', + 'span' => 'left', + 'cssClass' => 'flex-width', + ], + ], + ], + ], +]; diff --git a/tests/resources/themes/tests-theme/_pages/components.blade.php b/tests/resources/themes/tests-theme/_pages/components.blade.php index 13214f8f..eef95a24 100644 --- a/tests/resources/themes/tests-theme/_pages/components.blade.php +++ b/tests/resources/themes/tests-theme/_pages/components.blade.php @@ -1,3 +1,10 @@ +--- +title: Components +description: '' +permalink: /components +layout: default +'[testComponent]': [] +---
@push('scripts') @@ -16,8 +23,8 @@

This is a logged customer

@endmainauth - @partialIf('scriprts') + @partialIf('scripts') - @component('account') + @component('testComponent')
\ No newline at end of file diff --git a/tests/resources/themes/tests-theme/_pages/layout-with-lifecycle.blade.php b/tests/resources/themes/tests-theme/_pages/layout-with-lifecycle.blade.php new file mode 100644 index 00000000..e5172d3e --- /dev/null +++ b/tests/resources/themes/tests-theme/_pages/layout-with-lifecycle.blade.php @@ -0,0 +1,13 @@ +--- +title: Layout with Lifecycle +description: '' +permalink: /layout-with-lifecycle +layout: default-with-lifecycle + +'[testComponent]': +--- +
+
+

This is a logged customer

+
+
diff --git a/tests/resources/themes/tests-theme/_pages/page-with-lifecycle.blade.php b/tests/resources/themes/tests-theme/_pages/page-with-lifecycle.blade.php new file mode 100644 index 00000000..b0781d17 --- /dev/null +++ b/tests/resources/themes/tests-theme/_pages/page-with-lifecycle.blade.php @@ -0,0 +1,13 @@ +--- +title: Page with Lifecycle +description: '' +permalink: /page-with-lifecycle +layout: default + +'[testComponentWithLifecycle]': +--- +
+
+

This is a logged customer

+
+
diff --git a/tests/resources/themes/tests-theme/_partials/test-partial.blade.php b/tests/resources/themes/tests-theme/_partials/test-partial.blade.php new file mode 100644 index 00000000..f0f7b693 --- /dev/null +++ b/tests/resources/themes/tests-theme/_partials/test-partial.blade.php @@ -0,0 +1 @@ +This is a test partial content diff --git a/tests/resources/themes/tests-theme/public/style.css b/tests/resources/themes/tests-theme/public/style.css new file mode 100644 index 00000000..e69de29b diff --git a/tests/resources/views/_components/testcomponent/default.blade.php b/tests/resources/views/_components/testcomponent/default.blade.php new file mode 100644 index 00000000..251821bc --- /dev/null +++ b/tests/resources/views/_components/testcomponent/default.blade.php @@ -0,0 +1 @@ +{{ 'This is a test component partial content' }} diff --git a/tests/resources/views/_mail/test_layout.blade.php b/tests/resources/views/_mail/test_layout.blade.php new file mode 100644 index 00000000..19cc3acb --- /dev/null +++ b/tests/resources/views/_mail/test_layout.blade.php @@ -0,0 +1,3 @@ +name = 'Test Layout' +=== +This is a test layout! diff --git a/tests/resources/views/_mail/test_partial.blade.php b/tests/resources/views/_mail/test_partial.blade.php new file mode 100644 index 00000000..074dae7e --- /dev/null +++ b/tests/resources/views/_mail/test_partial.blade.php @@ -0,0 +1,3 @@ +name = 'Test Partial' +=== +This is a test partial! diff --git a/tests/resources/views/_partials/test-partial.blade.php b/tests/resources/views/_partials/test-partial.blade.php index 6b8a56f2..f0f7b693 100644 --- a/tests/resources/views/_partials/test-partial.blade.php +++ b/tests/resources/views/_partials/test-partial.blade.php @@ -1 +1 @@ -This is a test partial view \ No newline at end of file +This is a test partial content diff --git a/tests/resources/views/layout.blade.php b/tests/resources/views/layout.blade.php new file mode 100644 index 00000000..990d30be --- /dev/null +++ b/tests/resources/views/layout.blade.php @@ -0,0 +1,7 @@ + + + +{{Template::getBlock('body')}} + + + diff --git a/tests/resources/views/test.blade.php b/tests/resources/views/test.blade.php index d5c7ab6a..c5fdfb4c 100644 --- a/tests/resources/views/test.blade.php +++ b/tests/resources/views/test.blade.php @@ -1,3 +1 @@ - -Hello World! - +This is a test view content diff --git a/tests/resources/views/testcontroller/index.blade.php b/tests/resources/views/testcontroller/index.blade.php new file mode 100644 index 00000000..e69de29b diff --git a/tests/resources/views/testcontroller/view-with-exception.blade.php b/tests/resources/views/testcontroller/view-with-exception.blade.php new file mode 100644 index 00000000..03a3d46c --- /dev/null +++ b/tests/resources/views/testcontroller/view-with-exception.blade.php @@ -0,0 +1,3 @@ + diff --git a/tests/resources/views/testcontroller/view-with-throwable.blade.php b/tests/resources/views/testcontroller/view-with-throwable.blade.php new file mode 100644 index 00000000..d3988058 --- /dev/null +++ b/tests/resources/views/testcontroller/view-with-throwable.blade.php @@ -0,0 +1,3 @@ + diff --git a/tests/src/Admin/BulkActionWidgets/DeleteTest.php b/tests/src/Admin/BulkActionWidgets/DeleteTest.php index d255d5fe..f7392f90 100644 --- a/tests/src/Admin/BulkActionWidgets/DeleteTest.php +++ b/tests/src/Admin/BulkActionWidgets/DeleteTest.php @@ -23,3 +23,18 @@ expect(StatusHistory::count())->toBe(0); }); + +it('does noting when records is empty', function() { + $actionButton = new ToolbarButton('delete'); + $actionButton->displayAs('link', []); + + $controller = resolve(TestController::class); + $widget = new Delete($controller, $actionButton, []); + $widget->code = $actionButton->name; + + expect(StatusHistory::count())->toBe(0); + + $widget->handleAction([], StatusHistory::get()); + + expect(StatusHistory::count())->toBe(0); +}); diff --git a/tests/src/Admin/BulkActionWidgets/StatusTest.php b/tests/src/Admin/BulkActionWidgets/StatusTest.php index 97ccc8af..95456504 100644 --- a/tests/src/Admin/BulkActionWidgets/StatusTest.php +++ b/tests/src/Admin/BulkActionWidgets/StatusTest.php @@ -27,3 +27,20 @@ expect(StatusHistory::where($statusColumn, true)->count())->toBe(10); }); + +it('does noting when records is empty', function() { + $statusColumn = 'notify'; + + $actionButton = new ToolbarButton('status'); + $actionButton->displayAs('link', []); + + $controller = resolve(TestController::class); + $widget = new Status($controller, $actionButton, ['statusColumn' => $statusColumn]); + $widget->code = $actionButton->name; + + expect(StatusHistory::count())->toBe(0); + + $widget->handleAction(['code' => 'action.disable'], StatusHistory::get()); + + expect(StatusHistory::count())->toBe(0); +}); diff --git a/tests/src/Admin/Classes/AdminControllerTest.php b/tests/src/Admin/Classes/AdminControllerTest.php index d2d03aeb..16d8f302 100644 --- a/tests/src/Admin/Classes/AdminControllerTest.php +++ b/tests/src/Admin/Classes/AdminControllerTest.php @@ -2,89 +2,323 @@ namespace Igniter\Tests\Admin\Classes; +use Igniter\Admin\Classes\AdminController; +use Igniter\Admin\Classes\BaseWidget; +use Igniter\Admin\Widgets\Menu; +use Igniter\Admin\Widgets\Toolbar; +use Igniter\Flame\Exception\AjaxException; +use Igniter\Flame\Exception\FlashException; +use Igniter\Main\Widgets\MediaManager; use Igniter\Tests\Fixtures\Controllers\TestController; +use Igniter\User\Models\User; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Response; +use Illuminate\Validation\ValidationException; -it('has defined paths to locate layouts', function() { +beforeEach(function() { + $this->user = User::factory()->superUser()->create(); +}); + +it('defines paths correctly', function() { $controller = resolve(TestController::class); expect('igniter.admin::_layouts')->toBeIn($controller->layoutPath) ->and('igniter.tests::_layouts')->toBeIn($controller->layoutPath) - ->and('igniter.tests::')->not()->toBeIn($controller->layoutPath); + ->and('igniter.tests::')->not()->toBeIn($controller->layoutPath) + ->and('igniter.admin::')->toBeIn($controller->viewPath) + ->and('igniter.tests::testcontroller')->toBeIn($controller->viewPath) + ->and('igniter.tests::')->toBeIn($controller->viewPath) + ->and('igniter.admin::_partials')->toBeIn($controller->partialPath) + ->and('igniter.tests::_partials')->toBeIn($controller->partialPath) + ->and('igniter.tests::')->not()->toBeIn($controller->partialPath) + ->and('igniter::models/admin')->toBeIn($controller->configPath) + ->and('igniter::models/system')->toBeIn($controller->configPath) + ->and('igniter::models/main')->toBeIn($controller->configPath) + ->and('igniter.tests::models')->toBeIn($controller->configPath) + ->and('igniter.tests::')->toBeIn($controller->assetPath) + ->and('igniter::')->toBeIn($controller->assetPath) + ->and('igniter::js')->toBeIn($controller->assetPath) + ->and('igniter::css')->toBeIn($controller->assetPath); }); -it('has defined paths to locate views', function() { - $controller = resolve(TestController::class); +it('initializes toolbar, mediamanager and main menu widgets correcty', function() { + $controller = new class extends AdminController + { + }; + $this->actingAs($this->user, 'igniter-admin'); - expect('igniter.admin::')->toBeIn($controller->viewPath) - ->and('igniter.tests::testcontroller')->toBeIn($controller->viewPath) - ->and('igniter.tests::')->toBeIn($controller->viewPath); + $controller->initialize(); + + expect($controller->widgets['toolbar'])->toBeInstanceOf(Toolbar::class) + ->and($controller->widgets['mediamanager'])->toBeInstanceOf(MediaManager::class) + ->and($controller->widgets['mainmenu'])->toBeInstanceOf(Menu::class); }); -it('has defined paths to locate partials', function() { - $controller = resolve(TestController::class); +it('throws exception if user does not have permission', function() { + $user = User::factory()->create(); + $controller = new class extends AdminController + { + protected null|string|array $requiredPermissions = ['Admin.Restricted.Access']; + }; + $this->actingAs($user, 'igniter-admin'); - expect('igniter.admin::_partials')->toBeIn($controller->partialPath) - ->and('igniter.tests::_partials')->toBeIn($controller->partialPath) - ->and('igniter.tests::')->not()->toBeIn($controller->partialPath); + expect(fn() => $controller->remap('restrictedAction', ['param1', 'param2'])) + ->toThrow(FlashException::class, lang('igniter::admin.alert_user_restricted')); }); -it('has defined paths to locate model config files', function() { - $controller = resolve(TestController::class); +it('returns event response if beforeResponse event is fired', function() { + $controller = new class extends AdminController + { + }; + $controller->bindEvent('controller.beforeResponse', fn() => 'eventResponse'); - expect('igniter::models/admin')->toBeIn($controller->configPath) - ->and('igniter::models/system')->toBeIn($controller->configPath) - ->and('igniter::models/main')->toBeIn($controller->configPath) - ->and('igniter.tests::models')->toBeIn($controller->configPath); + $response = $controller->remap('index', ['param1', 'param2']); + + expect($response)->toBe('eventResponse'); +}); + +it('throws exception if action is 404', function() { + $controller = new class extends AdminController + { + }; + + expect(fn() => $controller->remap('404', ['param1', 'param2'])) + ->toThrow(FlashException::class, sprintf('Method [%s] is not found in the controller [%s]', '404', get_class($controller))); }); -it('has defined paths to locate asset files', function() { +it('throws exception if action is not found', function() { + $controller = new class extends AdminController + { + }; + + expect(fn() => $controller->remap('nonExistentAction', ['param1', 'param2'])) + ->toThrow(FlashException::class, sprintf('Method [%s] is not found in the controller [%s]', 'nonExistentAction', get_class($controller))); +}); + +it('processes handler throws exception if widget is not found', function() { + request()->headers->set('X-Requested-With', 'XMLHttpRequest'); + request()->headers->set('X-IGNITER-REQUEST-HANDLER', 'nonExistentWidget::onAjax'); $controller = resolve(TestController::class); - expect('igniter.tests::')->toBeIn($controller->assetPath) - ->and('igniter::')->toBeIn($controller->assetPath) - ->and('igniter::js')->toBeIn($controller->assetPath) - ->and('igniter::css')->toBeIn($controller->assetPath); + $this->expectException(FlashException::class); + $this->expectExceptionMessage(sprintf(lang('igniter::admin.alert_widget_not_bound_to_controller'), 'nonExistentWidget')); + + $controller->remap('index'); }); -it('can find (default) layout', function() { +it('processes handler throws exception if widget handler does not exist', function() { + request()->headers->set('X-Requested-With', 'XMLHttpRequest'); + request()->headers->set('X-IGNITER-REQUEST-HANDLER', 'testWidget::onNonExistentHandler'); $controller = resolve(TestController::class); - $viewPath = $controller->getViewPath('default', $controller->layoutPath); + $widget = new class($controller) extends BaseWidget + { + public ?string $alias = 'testWidget'; + }; + $widget->bindToController(); + + $this->expectException(FlashException::class); + $this->expectExceptionMessage(sprintf(lang('igniter::admin.alert_ajax_handler_not_found'), 'testWidget::onNonExistentHandler')); - expect($viewPath)->toEndWith('admin/_layouts/default.blade.php'); + $controller->remap('index'); }); -it('can find (edit) view', function() { +it('processes widget handler and returns response', function() { + request()->headers->set('X-Requested-With', 'XMLHttpRequest'); + request()->headers->set('X-IGNITER-REQUEST-HANDLER', 'testWidget::onHandler'); $controller = resolve(TestController::class); - $viewPath = $controller->getViewPath('edit', $controller->viewPath); + $widget = new class($controller) extends BaseWidget + { + public ?string $alias = 'testWidget'; - expect($viewPath)->toEndWith('admin/edit.blade.php'); + public function onHandler(): array + { + return ['status' => 'success']; + } + }; + $widget->bindToController(); + + $response = $controller->remap('index'); + + expect($response)->toBe(['status' => 'success']); }); -it('can find (flash) partial', function() { +it('processes specific page handler and returns handler response', function() { + $controller = new class extends AdminController + { + public function index_onAjax(): Response + { + return response(['status' => 'success']); + } + + public function index() + { + return 'index content'; + } + }; + request()->request->set('_handler', 'onAjax'); + + $response = $controller->remap('index', ['param1', 'param2']); + + expect($response->getContent())->toBe(json_encode(['status' => 'success'])); +}); + +it('processes widget generic handler and returns handler response', function() { + request()->headers->set('X-Requested-With', 'XMLHttpRequest'); + request()->headers->set('X-IGNITER-REQUEST-HANDLER', 'onHandler'); $controller = resolve(TestController::class); - $partialPath = $controller->getViewPath('flash', $controller->partialPath); + $widget = new class($controller) extends BaseWidget + { + public ?string $alias = 'testWidget'; + + public function onHandler(): Response + { + return response(['status' => 'success']); + } + }; + $widget->bindToController(); - expect($partialPath)->toEndWith('admin/_partials/flash.blade.php'); + $response = $controller->remap('index'); + + expect($response->getContent())->toBe(json_encode(['status' => 'success'])); }); -it('can find controller config file', function() { +it('returns null when no matching generic handler is found in widgets', function() { + request()->headers->set('X-Requested-With', 'XMLHttpRequest'); + request()->headers->set('X-IGNITER-REQUEST-HANDLER', 'onNonExistentHandler'); $controller = resolve(TestController::class); - $viewPath = $controller->getConfigPath('status.php'); + $widget = new class($controller) extends BaseWidget + { + public ?string $alias = 'testWidget'; + }; + $widget->bindToController(); + + $response = $controller->remap('index'); - expect($viewPath)->toEndWith('models/admin/status.php'); + expect($response)->toBeNull(); }); -it('can find asset file', function() { +it('processes handler and returns partial response', function() { + request()->request->set('_handler', 'onAjax'); + request()->headers->set('X-IGNITER-REQUEST-PARTIALS', 'test-partial'); $controller = resolve(TestController::class); - expect($controller->getAssetPath('app.js'))->toEndWith('js/app.js') - ->and($controller->getAssetPath('$/igniter/js/vendor.js'))->toEndWith('igniter/js/vendor.js'); + $response = $controller->remap('index', ['param1', 'param2']); + + expect($response['test-partial'])->toContain('This is a test partial content'); +}); + +it('processes handler and returns handler redirect response', function() { + request()->headers->set('X-Requested-With', 'XMLHttpRequest'); + request()->headers->set('X-IGNITER-REQUEST-HANDLER', 'onAjax'); + $controller = new class extends AdminController + { + public function onAjax(): RedirectResponse + { + return $this->redirect('redirected-url'); + } + + public function index() + { + return 'index content'; + } + }; + + $response = $controller->remap('index'); + + expect($response['X_IGNITER_REDIRECT'])->toContain('redirected-url'); +}); + +it('processes handler and returns handler flash message response', function() { + request()->headers->set('X-Requested-With', 'XMLHttpRequest'); + request()->headers->set('X-IGNITER-REQUEST-HANDLER', 'onAjax'); + $controller = new class extends AdminController + { + public function onAjax(): string + { + flash()->success('Test flash message'); + + return 'This is a string response'; + } + + public function index() + { + return 'index content'; + } + }; + + $response = $controller->remap('index'); + + expect($response['#notification'])->toContain('Test flash message') + ->and($response['result'])->toContain('This is a string response'); +}); + +it('processes handler and throws validation errors', function() { + request()->headers->set('X-Requested-With', 'XMLHttpRequest'); + request()->headers->set('X-IGNITER-REQUEST-HANDLER', 'onAjax'); + $controller = new class extends AdminController + { + public function onAjax(): string + { + throw ValidationException::withMessages(['name' => 'Name is required']); + } + + public function index() + { + return 'index content'; + } + }; + + $this->expectException(AjaxException::class); + $this->expectExceptionMessage('Name is required'); + + $controller->remap('index'); +}); + +it('processes handler and throws mass assignment exception', function() { + request()->headers->set('X-Requested-With', 'XMLHttpRequest'); + request()->headers->set('X-IGNITER-REQUEST-HANDLER', 'onAjax'); + $controller = new class extends AdminController + { + public function onAjax(): string + { + throw new \Illuminate\Database\Eloquent\MassAssignmentException('Mass assignment exception'); + } + + public function index() + { + return 'index content'; + } + }; + + $this->expectException(FlashException::class); + $this->expectExceptionMessage('Mass assignment exception'); + + $controller->remap('index'); }); -it('runs the requested controller action', function() { - $this->get('admin/login')->assertStatus(200); +it('remaps action and renders controller action default view', function() { + $controller = new class extends AdminController + { + public function test() {} + }; + + $response = $controller->remap('test'); + + expect($response)->toBeInstanceOf(\Illuminate\Http\Response::class) + ->and($response->getContent())->toContain('This is a test view content'); }); -it('runs the requested controller handler', function() { - $this->post('admin/login', ['_handler' => 'onLogin'])->assertSessionHas('admin_errors'); +it('remaps action and returns response', function() { + $controller = new class extends AdminController + { + public function index() + { + return 'index content'; + } + }; + + $response = $controller->remap('index'); + + expect($response)->toBeInstanceOf(\Illuminate\Http\Response::class) + ->and($response->getContent())->toBe('index content'); }); diff --git a/tests/src/Admin/Classes/BaseBulkActionWidgetTest.php b/tests/src/Admin/Classes/BaseBulkActionWidgetTest.php index f689d612..0349f288 100644 --- a/tests/src/Admin/Classes/BaseBulkActionWidgetTest.php +++ b/tests/src/Admin/Classes/BaseBulkActionWidgetTest.php @@ -14,7 +14,32 @@ $widget = new BaseBulkActionWidget($controller, $actionButton, $config); - expect($widget)->toBeInstanceOf(BaseBulkActionWidget::class); + expect($widget->code)->toBeNull() + ->and($widget->label)->toBeNull() + ->and($widget->type)->toBeNull() + ->and($widget->popupTitle)->toBeNull(); +}); + +it('returns empty array for form fields', function() { + $controller = new TestController; + $actionButton = new ToolbarButton('test-toolbar-button'); + $widget = new BaseBulkActionWidget($controller, $actionButton); + + $result = $widget->defineFormFields(); + + expect($result)->toBeArray() + ->and($result)->toBeEmpty(); +}); + +it('returns empty array for validation rules', function() { + $controller = new TestController; + $actionButton = new ToolbarButton('test-toolbar-button'); + $widget = new BaseBulkActionWidget($controller, $actionButton); + + $result = $widget->defineValidationRules(); + + expect($result)->toBeArray() + ->and($result)->toBeEmpty(); }); it('returns the action button', function() { diff --git a/tests/src/Admin/Classes/BaseDashboardWidgetTest.php b/tests/src/Admin/Classes/BaseDashboardWidgetTest.php index b667902a..c81f4ad1 100644 --- a/tests/src/Admin/Classes/BaseDashboardWidgetTest.php +++ b/tests/src/Admin/Classes/BaseDashboardWidgetTest.php @@ -1,10 +1,77 @@ '2023-01-01', + 'endDate' => '2023-12-31', + 'otherProperty' => 'value', + ]); + + $result = $widget->getPropertiesToSave(); + + expect($result)->toBeArray() + ->and($result)->toHaveKey('otherProperty') + ->and($result)->not->toHaveKey('startDate') + ->and($result)->not->toHaveKey('endDate'); +}); + +it('returns validation rules and attributes', function() { + $controller = new TestController; + $widget = new class($controller, [ + 'startDate' => '2023-01-01', + 'endDate' => '2023-12-31', + 'otherProperty' => 'value', + ]) extends BaseDashboardWidget + { + public function defineProperties(): array + { + return [ + 'property1' => ['validationRule' => 'required', 'label' => 'Property 1'], + 'property2' => ['validationRule' => 'numeric', 'label' => 'Property 2'], + ]; + } + }; + + [$rules, $attributes] = $widget->getPropertyRules(); + + expect($rules)->toBeArray() + ->and($rules)->toHaveKey('property1') + ->and($rules['property1'])->toBe('required') + ->and($rules)->toHaveKey('property2') + ->and($rules['property2'])->toBe('numeric') + ->and($attributes)->toBeArray() + ->and($attributes)->toHaveKey('property1') + ->and($attributes['property1'])->toBe('Property 1') + ->and($attributes)->toHaveKey('property2') + ->and($attributes['property2'])->toBe('Property 2'); +}); + +it('returns empty rules and attributes if no properties defined', function() { + $controller = new TestController; + $widget = new class($controller, []) extends BaseDashboardWidget + { + public function defineProperties(): array + { + return []; + } + }; + + [$rules, $attributes] = $widget->getPropertyRules(); + + expect($rules)->toBeArray() + ->and($rules)->toBeEmpty() + ->and($attributes)->toBeArray() + ->and($attributes)->toBeEmpty(); +}); + it('can get width', function() { $widget = new BaseDashboardWidget(new TestController, ['width' => 300]); expect($widget->getWidth())->toBe(300); diff --git a/tests/src/Admin/Classes/BaseFormWidgetTest.php b/tests/src/Admin/Classes/BaseFormWidgetTest.php index 07b53ac8..7479cb2a 100644 --- a/tests/src/Admin/Classes/BaseFormWidgetTest.php +++ b/tests/src/Admin/Classes/BaseFormWidgetTest.php @@ -7,9 +7,39 @@ use Igniter\Admin\Classes\FormField; use Igniter\Tests\Fixtures\Models\TestModel; +it('constructs correctly', function() { + $formField = new FormField('testField', 'Test Field'); + $widget = new BaseFormWidget(new AdminController, $formField, []); + + expect($widget->model)->toBeNull() + ->and($widget->data)->toBeNull() + ->and($widget->sessionKey)->toBeNull() + ->and($widget->previewMode)->toBeFalse() + ->and($widget->showLabels)->toBeTrue() + ->and($widget->config)->toBeArray(); +}); + +it('returns unique id with suffix', function() { + $formField = new FormField('testField', 'Test Field'); + $formField->fieldName = 'testField'; + $widget = new BaseFormWidget(new AdminController, $formField, []); + + $id = $widget->getId('suffix'); + + expect($id)->toBe('baseformwidget-suffix-testField'); +}); + +it('returns unique id without suffix', function() { + $formField = new FormField('testField', 'Test Field'); + $widget = new BaseFormWidget(new AdminController, $formField, []); + + $id = $widget->getId(); + + expect($id)->toBe('baseformwidget-testField'); +}); + it('can get save value', function() { $formField = new FormField('testField', 'Test Field'); - $formField->displayAs('text'); $widget = new BaseFormWidget(new AdminController, $formField, [ 'alias' => 'test-alias', 'model' => new TestModel, diff --git a/tests/src/Admin/Classes/BaseMainMenuWidgetTest.php b/tests/src/Admin/Classes/BaseMainMenuWidgetTest.php index f2360cf1..fbf1b1c5 100644 --- a/tests/src/Admin/Classes/BaseMainMenuWidgetTest.php +++ b/tests/src/Admin/Classes/BaseMainMenuWidgetTest.php @@ -9,5 +9,21 @@ it('constructs correctly', function() { $widget = new BaseMainMenuWidget(new TestController, new MainMenuItem('test-menu-item'), []); - expect($widget)->toBeInstanceOf(BaseMainMenuWidget::class); + expect($widget->config)->toBeArray(); +}); + +it('returns unique id with suffix', function() { + $widget = new BaseMainMenuWidget(new TestController, new MainMenuItem('test-menu-item'), []); + + $id = $widget->getId('suffix'); + + expect($id)->toBe('basemainmenuwidget-suffix-menuitem-test-menu-item'); +}); + +it('returns unique id without suffix', function() { + $widget = new BaseMainMenuWidget(new TestController, new MainMenuItem('test-menu-item'), []); + + $id = $widget->getId(); + + expect($id)->toBe('basemainmenuwidget-menuitem-test-menu-item'); }); diff --git a/tests/src/Admin/Classes/BaseWidgetTest.php b/tests/src/Admin/Classes/BaseWidgetTest.php index 71a55f97..17e3edd6 100644 --- a/tests/src/Admin/Classes/BaseWidgetTest.php +++ b/tests/src/Admin/Classes/BaseWidgetTest.php @@ -33,10 +33,19 @@ ->and($this->controller->widgets['test-alias'])->toBe($this->widget) ->and($this->widget->getEventHandler('onTest'))->toBe('test-alias::onTest') ->and($this->widget->getController())->toBe($this->controller) + ->and($this->widget->hasSession('invalid-key'))->toBeFalse() ->and($this->widget->reload())->toBeArray(); }); it('can set and get config', function() { - $this->widget->setConfig(['test' => 'value']); - expect($this->widget->getConfig('test'))->toBe('value'); + $this->widget->setConfig([ + 'test' => 'value', + 'nested' => [ + 'key' => 'value', + ], + ]); + expect($this->widget->getConfig())->toBeArray() + ->and($this->widget->getConfig('test'))->toBe('value') + ->and($this->widget->getConfig('nested[key]'))->toBe('value') + ->and($this->widget->getConfig('invalid-nested[key]', 'default'))->toBe('default'); }); diff --git a/tests/src/Admin/Classes/FilterScopeTest.php b/tests/src/Admin/Classes/FilterScopeTest.php index 48477c3d..b4465ccb 100644 --- a/tests/src/Admin/Classes/FilterScopeTest.php +++ b/tests/src/Admin/Classes/FilterScopeTest.php @@ -4,16 +4,49 @@ use Igniter\Admin\Classes\FilterScope; -dataset('ids', [ - ['testScope', null, null, 'scope-testScope'], - ['testScope', 'suffix', null, 'scope-testScope-suffix'], - ['testScope', null, 'prefix', 'prefix-scope-testScope'], - ['testScope', 'suffix', 'prefix', 'prefix-scope-testScope-suffix'], -]); +it('constructs correctly', function() { + $filterScope = new FilterScope('testScopeName', 'Test Scope'); + $filterScope->displayAs('select', [ + 'options' => ['option1', 'option2'], + 'context' => ['context1', 'context2'], + 'default' => 'default', + 'conditions' => 'name = :name', + 'scope' => 'testScope', + 'cssClass' => 'css-class', + 'nameFrom' => 'nameColumn', + 'descriptionFrom' => 'descriptionColumn', + 'disabled' => true, + 'mode' => 'mode', + ]); + + expect($filterScope->scopeName)->toBe('testScopeName') + ->and($filterScope->label)->toBe('Test Scope') + ->and($filterScope->idPrefix)->toBeNull() + ->and($filterScope->nameFrom)->toBe('nameColumn') + ->and($filterScope->descriptionFrom)->toBe('descriptionColumn') + ->and($filterScope->value)->toBeNull() + ->and($filterScope->type)->toBe('select') + ->and($filterScope->options)->toBe(['option1', 'option2']) + ->and($filterScope->context)->toBe(['context1', 'context2']) + ->and($filterScope->disabled)->toBeTrue() + ->and($filterScope->defaults)->toBe('default') + ->and($filterScope->conditions)->toBe('name = :name') + ->and($filterScope->scope)->toBe('testScope') + ->and($filterScope->cssClass)->toBe('css-class') + ->and($filterScope->mode)->toBe('mode') + ->and($filterScope->minDate)->toBeNull() + ->and($filterScope->maxDate)->toBeNull() + ->and($filterScope->config)->toBeArray(); +}); it('can get id with optional prefix and suffix', function($scopeName, $suffix, $prefix, $expectedId) { $filterScope = new FilterScope($scopeName, 'Test Scope'); $filterScope->displayAs('text', ['idPrefix' => $prefix]); expect($filterScope->getId($suffix))->toBe($expectedId); -})->with('ids'); +})->with([ + ['testScope', null, null, 'scope-testScope'], + ['testScope', 'suffix', null, 'scope-testScope-suffix'], + ['testScope', null, 'prefix', 'prefix-scope-testScope'], + ['testScope', 'suffix', 'prefix', 'prefix-scope-testScope-suffix'], +]); diff --git a/tests/src/Admin/Classes/FormFieldTest.php b/tests/src/Admin/Classes/FormFieldTest.php index bde64fa5..3a1ea70c 100644 --- a/tests/src/Admin/Classes/FormFieldTest.php +++ b/tests/src/Admin/Classes/FormFieldTest.php @@ -3,6 +3,7 @@ namespace Igniter\Tests\Admin\Classes; use Igniter\Admin\Classes\FormField; +use Igniter\Flame\Database\Model; use Igniter\Tests\Fixtures\Models\TestModel; beforeEach(function() { @@ -10,11 +11,22 @@ }); it('can get name', function() { - expect($this->formField->getName())->toBe('testField'); + expect($this->formField->getName())->toBe('testField') + ->and($this->formField->getName('arrayName'))->toBe('arrayName[testField]'); }); it('can get id', function() { - expect($this->formField->getId())->toBe('field-testfield'); + $this->formField->arrayName = 'arrayName'; + + expect($this->formField->getId())->toBe('field-arrayname-testfield') + ->and($this->formField->getId('suffix'))->toBe('field-arrayname-testfield-suffix'); +}); + +it('can get id with prefix', function() { + $this->formField->arrayName = 'arrayName'; + $this->formField->idPrefix = 'idPrefix'; + + expect($this->formField->getId())->toBe('idprefix-field-arrayname-testfield'); }); it('evaluates config correctly', function() { @@ -38,9 +50,13 @@ 'default' => 'value', 'defaultFrom' => 'otherField', 'attributes' => ['class' => 'test-class'], + 'containerAttributes' => ['class' => 'test-class'], + 'valueFrom' => 'otherField', + 'extraConfig' => 'extra', ]); - expect($this->formField->commentHtml)->toBeTrue() + expect($this->formField->getConfig('extraConfig'))->toBe('extra') + ->and($this->formField->commentHtml)->toBeTrue() ->and($this->formField->placeholder)->toBe('Enter text') ->and($this->formField->dependsOn)->toBe(['otherField']) ->and($this->formField->required)->toBeTrue() @@ -50,7 +66,7 @@ ->and($this->formField->context)->toBe('create') ->and($this->formField->hidden)->toBeTrue() ->and($this->formField->path)->toBe('/path/to/partial') - ->and($this->formField->options)->toBeArray() + ->and($this->formField->options())->toBeArray() ->and($this->formField->span)->toBe('left') ->and($this->formField->size)->toBe('large') ->and($this->formField->tab)->toBe('testTab') @@ -58,12 +74,55 @@ ->and($this->formField->comment)->toBe('Test comment') ->and($this->formField->defaults)->toBe('value') ->and($this->formField->defaultFrom)->toBe('otherField') - ->and($this->formField->getAttributes())->toContain('class="test-class"'); + ->and($this->formField->hasAttribute('class'))->toBeTrue() + ->and($this->formField->hasAttribute('class', 'container'))->toBeTrue() + ->and($this->formField->getAttributes())->toContain('class="test-class"') + ->and($this->formField->getAttributes('container'))->toContain('class="test-class"'); +}); + +it('can get value from object data', function() { + $dataObject = (object)['testField' => 'test-value']; + + expect($this->formField->getValueFromData((object)[]))->toBeNull() + ->and($this->formField->getValueFromData($dataObject))->toBe('test-value'); }); -it('can get value from data', function() { +it('can get value from array data', function() { $data = ['testField' => 'test-value']; - expect($this->formField->getValueFromData($data))->toBe('test-value'); + + expect($this->formField->getValueFromData([]))->toBeNull() + ->and($this->formField->getValueFromData($data))->toBe('test-value'); +}); + +it('can get value from model relation data', function() { + $model = new class extends Model + { + public $relation = [ + 'belongsTo' => ['testRelation' => [TestModel::class]], + ]; + }; + + $relation = new TestModel; + $relation->testField = 'test-value'; + $model->setRelation('testRelation', $relation); + $this->formField->fieldName = 'testRelation[testField]'; + expect($this->formField->getValueFromData($model))->toBe('test-value'); + + $this->formField->fieldName = 'testRelation'; + $relation->testRelation = 'test-value'; + expect($this->formField->getValueFromData($model))->toBeInstanceOf(TestModel::class); +}); + +it('can get callable options', function() { + expect($this->formField->options())->toBe([]); + + $this->formField->displayAs('select', [ + 'options' => function() { + return ['option1', 'option2']; + }, + ]); + + expect($this->formField->options())->toBe(['option1', 'option2']); }); it('can get default from data', function() { @@ -72,44 +131,138 @@ expect($this->formField->getDefaultFromData($data))->toBe('default-value'); }); +it('can get default from data if no defaultField', function() { + expect($this->formField->getDefaultFromData([]))->toBeNull(); + + $this->formField->defaults = 'default-value'; + expect($this->formField->getDefaultFromData([]))->toBe('default-value'); +}); + it('can get attributes', function() { - $this->formField->displayAs('text', ['attributes' => ['class' => 'test-class']]); - expect($this->formField->getAttributes())->toBe(' class="test-class"'); + $this->formField->displayAs('switch', [ + 'attributes' => ['class' => 'test-class'], + 'readOnly' => true, + 'disabled' => true, + ]); + + $attributes = $this->formField->getAttributes(); + + expect($attributes)->toContain(' class="test-class"') + ->and($attributes)->toContain(' readonly="readonly"') + ->and($attributes)->toContain(' disabled="disabled"') + ->and($attributes)->toContain(' onclick="return false;"') + ->and($this->formField->hasAttribute('invalid-attribute'))->toBeFalse() + ->and($this->formField->hasAttribute('attribute', 'invalid-position'))->toBeFalse(); }); -it('can get attributes with trigger', function() { +it('can get trigger attributes when action is hide and position is field', function() { $this->formField->displayAs('text', [ 'trigger' => [ 'action' => 'hide', 'field' => 'otherField', 'condition' => 'value', ], - 'attributes' => ['class' => 'test-class'], + 'attributes' => ['field' => ['data-trigger' => "[name='otherField']"]], + ]); + + $attributes = $this->formField->getAttributes('field', false); + + expect($attributes['data-trigger'])->toBe("[name='otherField']") + ->and($attributes)->not->toHaveKey('data-trigger-action'); +}); + +it('can get trigger attributes when action is enable and position is container', function() { + $this->formField->displayAs('text', [ + 'trigger' => [ + 'action' => 'enable', + 'field' => 'otherField', + 'condition' => 'value', + ], ]); $attributes = $this->formField->getAttributes('container', false); + expect($attributes)->not->toHaveKey('data-trigger-action'); +}); + +it('can get trigger attributes when action is checked', function() { + $this->formField->displayAs('text', [ + 'trigger' => [ + 'action' => 'checked', + 'field' => 'otherField', + 'condition' => 'value', + ], + ]); + + $attributes = $this->formField->getAttributes('field', false); + expect($attributes['data-trigger'])->toBe("[name='otherField']") - ->and($attributes['data-trigger-action'])->toBe('hide') + ->and($attributes['data-trigger-action'])->toBe('checked') ->and($attributes['data-trigger-condition'])->toBe('value') ->and($attributes['data-trigger-closest-parent'])->toBe('form'); }); -it('can get attributes with preset', function() { +it('can get trigger attributes when action is checked and is array field', function() { + $this->formField->arrayName = 'arrayName'; + $this->formField->displayAs('text', [ + 'trigger' => [ + 'action' => 'checked', + 'field' => 'otherField', + 'condition' => 'value', + ], + ]); + + $attributes = $this->formField->getAttributes('field', false); + + expect($attributes['data-trigger'])->toBe("[name='arrayName[otherField]']") + ->and($attributes['data-trigger-action'])->toBe('checked') + ->and($attributes['data-trigger-condition'])->toBe('value') + ->and($attributes['data-trigger-closest-parent'])->toBe('form'); +}); + +it('can get preset attributes when preset is string', function() { + $this->formField->displayAs('text', [ + 'preset' => 'otherField', + ]); + + $attributes = $this->formField->getAttributes('field', false); + + expect($attributes['data-input-preset'])->toBe('[name="otherField"]') + ->and($attributes['data-input-preset-type'])->toBe('slug') + ->and($attributes['data-input-preset-closest-parent'])->toBe('form'); +}); + +it('can get preset attributes with array field', function() { + $this->formField->arrayName = 'arrayName'; $this->formField->displayAs('text', [ 'preset' => [ 'field' => 'otherField', 'type' => 'slug', + 'prefixInput' => 'prefixField', ], - 'attributes' => ['class' => 'test-class'], ]); $attributes = $this->formField->getAttributes('field', false); - expect($attributes['data-input-preset'])->toBe('[name="otherField"]') + expect($attributes['data-input-preset'])->toBe('[name="arrayName[otherField]"]') ->and($attributes['data-input-preset-type'])->toBe('slug') ->and($attributes['data-input-preset-closest-parent'])->toBe('form') - ->and($attributes['class'])->toBe('test-class'); + ->and($attributes['data-input-preset-prefix-input'])->toBe('prefixField'); +}); + +it('can get preset attributes correctly', function() { + $this->formField->displayAs('text', [ + 'preset' => [ + 'field' => 'otherField', + 'type' => 'slug', + ], + ]); + + $attributes = $this->formField->getAttributes('field', false); + + expect($attributes['data-input-preset'])->toBe('[name="otherField"]') + ->and($attributes['data-input-preset-type'])->toBe('slug') + ->and($attributes['data-input-preset-closest-parent'])->toBe('form'); }); it('can resolve model attribute', function() { @@ -119,3 +272,13 @@ expect($resolvedModel->testField)->toBe('test-value') ->and($attribute)->toBe('testField'); }); + +it('can resolve model nested attribute', function() { + $model = new TestModel; + $model->testRelation = (object)[ + 'testField' => 'test-value', + ]; + [$resolvedModel, $attribute] = $this->formField->resolveModelAttribute($model, 'testRelation[testField]'); + expect($resolvedModel->testField)->toBe('test-value') + ->and($attribute)->toBe('testField'); +}); diff --git a/tests/src/Admin/Classes/FormTabsTest.php b/tests/src/Admin/Classes/FormTabsTest.php index f1b145f7..790d275f 100644 --- a/tests/src/Admin/Classes/FormTabsTest.php +++ b/tests/src/Admin/Classes/FormTabsTest.php @@ -12,7 +12,18 @@ it('constructs correctly', function() { $formTabs = new FormTabs; - expect($formTabs->suppressTabs)->toBeTrue(); + expect($formTabs->fields)->toBe([]) + ->and($formTabs->defaultTab)->toBe('igniter::admin.form.undefined_tab') + ->and($formTabs->stretch)->toBeNull() + ->and($formTabs->suppressTabs)->toBeTrue() + ->and($formTabs->section)->toBe('outside') + ->and($formTabs->cssClass)->toBeNull() + ->and($formTabs->getIterator())->toBeIterable() + ->and($formTabs->hasFields())->toBeFalse() + ->and($formTabs->offsetSet('newField', $this->formField))->toBeNull() + ->and($formTabs->offsetExists('newField'))->toBeTrue() + ->and($formTabs->offsetUnset('newField'))->toBeNull() + ->and($formTabs->offsetGet('newField'))->toBeNull(); }); it('evaluates config correctly', function() { @@ -31,15 +42,30 @@ ->and($formTabs->cssClass)->toBe('test-class'); }); +it('adds fields to default tab', function() { + $formTabs = new FormTabs; + + $formTabs->addField('testField', $this->formField); + + expect($formTabs->fields['igniter::admin.form.undefined_tab']['testField'])->toBe($this->formField); +}); + +it('adds fields to primary tab', function() { + $formTabs = new FormTabs; + + $formTabs->addField('testField', $this->formField, 'primary'); + + expect($formTabs->fields['primary']['testField'])->toBe($this->formField); +}); + it('adds and removes field correctly', function() { $formTabs = new FormTabs; $formTabs->addField('testField', $this->formField, 'Test Tab'); - expect($formTabs->hasFields())->toBeTrue() - ->and($formTabs->getFields())->toHaveKey('Test Tab'); - $formTabs->removeField('testField'); - expect($formTabs->hasFields())->toBeFalse(); + expect($formTabs->removeField('testField'))->toBeTrue() + ->and($formTabs->removeField('invalidTest'))->toBeFalse(); + }); it('gets all fields correctly', function() { diff --git a/tests/src/Admin/Classes/MainMenuItemTest.php b/tests/src/Admin/Classes/MainMenuItemTest.php index 0d90afca..0b2d532f 100644 --- a/tests/src/Admin/Classes/MainMenuItemTest.php +++ b/tests/src/Admin/Classes/MainMenuItemTest.php @@ -47,24 +47,60 @@ }); it('sets and gets options correctly', function() { + expect($this->mainMenuItem->options())->toBe([]); $this->mainMenuItem->options(['option1', 'option2']); expect($this->mainMenuItem->options())->toBe(['option1', 'option2']); }); +it('sets and gets callable options correctly', function() { + $this->mainMenuItem->options(function() { + return ['option1', 'option2']; + }); + expect($this->mainMenuItem->options())->toBe(['option1', 'option2']); +}); + it('displays as correctly', function() { - $this->mainMenuItem->displayAs('text', ['icon' => 'test-icon']); + $this->mainMenuItem->displayAs('text', [ + 'priority' => 99, + 'anchor' => 'testAnchor', + 'options' => ['option1', 'option2'], + 'context' => ['context1', 'context2'], + 'icon' => 'test-icon', + 'path' => '/path/to/partial', + 'cssClass' => 'test-class', + 'attributes' => ['class' => 'test-class'], + 'disabled' => true, + ]); expect($this->mainMenuItem->type)->toBe('text') - ->and($this->mainMenuItem->icon)->toBe('test-icon'); + ->and($this->mainMenuItem->anchor)->toBe('testAnchor') + ->and($this->mainMenuItem->options)->toBe(['option1', 'option2']) + ->and($this->mainMenuItem->context)->toBe(['context1', 'context2']) + ->and($this->mainMenuItem->icon)->toBe('test-icon') + ->and($this->mainMenuItem->path)->toBe('/path/to/partial') + ->and($this->mainMenuItem->cssClass)->toBe('test-class') + ->and($this->mainMenuItem->attributes)->toBe(['class' => 'test-class']) + ->and($this->mainMenuItem->disabled)->toBeTrue(); }); it('gets attributes correctly', function() { - $this->mainMenuItem->attributes(['class' => 'test-class']); - expect($this->mainMenuItem->getAttributes(false))->toBe(['class' => 'test-class']); + $this->mainMenuItem->disabled = true; + $this->mainMenuItem->attributes([ + 'class' => 'test-class', + 'href' => '/path/to/partial', + ]); + + $attributes = $this->mainMenuItem->getAttributes(false); + expect($attributes)->toBe([ + 'class' => 'test-class', + 'href' => admin_url('/path/to/partial'), + 'disabled' => 'disabled', + ]); }); it('gets id correctly', function() { - expect($this->mainMenuItem->getId())->toBe('menuitem-testItem') - ->and($this->mainMenuItem->getId('suffix'))->toBe('menuitem-testItem-suffix'); + $this->mainMenuItem->idPrefix = 'prefix'; + expect($this->mainMenuItem->getId())->toBe('prefix-menuitem-testItem') + ->and($this->mainMenuItem->getId('suffix'))->toBe('prefix-menuitem-testItem-suffix'); }); it('sets label correctly', function() { diff --git a/tests/src/Admin/Classes/NavigationTest.php b/tests/src/Admin/Classes/NavigationTest.php index ba7b3eeb..1292b612 100644 --- a/tests/src/Admin/Classes/NavigationTest.php +++ b/tests/src/Admin/Classes/NavigationTest.php @@ -12,7 +12,28 @@ $this->navigation = new Navigation; }); -it('registers a navigation item', function() { +it('constructs correctly', function() { + $navigation = new Navigation('test/path'); + + expect($navigation->viewPath)->toBe(['test/path']); +}); + +it('sets context with item code only', function() { + $navigation = new Navigation(); + $navigation->setContext('settings', 'system'); + + expect($navigation->isActiveNavItem('invalid'))->toBeFalse() + ->and($navigation->isActiveNavItem('settings'))->toBeTrue(); +}); + +it('sets context with item code and parent code', function() { + $navigation = new Navigation(); + $navigation->setContext('settings', 'system'); + + expect($navigation->isActiveNavItem('system'))->toBeTrue(); +}); + +it('registers a navigation items', function() { $this->navigation->registerNavItems([ 'test' => [ 'code' => 'test', @@ -37,6 +58,51 @@ ->and($items['test']['child'])->toBeNull(); }); +it('registers a navigation item', function() { + $this->navigation->registerNavItem('test', [ + 'code' => 'test', + 'title' => 'Test', + 'class' => 'test', + 'icon' => 'fa fa-angle-double-right', + 'href' => 'http://localhost/admin/test', + 'priority' => 90, + 'permission' => ['Admin.Test'], + ]); + + $items = $this->navigation->getNavItems(); + + expect($items['test']['code'])->toBe('test') + ->and($items['test']['class'])->toBe('test') + ->and($items['test']['href'])->toBe('http://localhost/admin/test') + ->and($items['test']['icon'])->toBe('fa fa-angle-double-right') + ->and($items['test']['title'])->toBe('Test') + ->and($items['test']['priority'])->toBe(90) + ->and($items['test']['permission'])->toBe(['Admin.Test']); +}); + +it('registers a navigation child item', function() { + $this->navigation->registerNavItem('test', [ + 'code' => 'test', + 'title' => 'Test', + 'class' => 'test', + 'icon' => 'fa fa-angle-double-right', + 'href' => 'http://localhost/admin/test', + 'priority' => 90, + 'permission' => ['Admin.Test'], + ], 'parentItem'); + + $items = $this->navigation->getNavItems(); + $navItem = $items['parentItem']['child']['test']; + + expect($navItem['code'])->toBe('test') + ->and($navItem['class'])->toBe('test') + ->and($navItem['href'])->toBe('http://localhost/admin/test') + ->and($navItem['icon'])->toBe('fa fa-angle-double-right') + ->and($navItem['title'])->toBe('Test') + ->and($navItem['priority'])->toBe(90) + ->and($navItem['permission'])->toBe(['Admin.Test']); +}); + it('loads registered admin navigation items', function() { $items = AdminMenu::getNavItems(); @@ -49,6 +115,7 @@ 'tools.child.media_manager', 'system.child.settings', ]) + ->and(AdminMenu::loadItems())->toBeNull() ->and($items['dashboard']['code'])->toBe('dashboard') ->and($items['dashboard']['class'])->toBe('dashboard admin') ->and($items['dashboard']['href'])->toBe('http://localhost/admin/dashboard') @@ -91,7 +158,50 @@ ->and($items['testItem']['permission'])->toBe(['Admin.TestItem']); }); -it('removes navigation items correctly', function() { +it('adds and gets visible navigation items correctly', function() { + $this->actingAs(User::factory()->superUser()->create(), 'igniter-admin'); + $this->navigation->addNavItem('testItem', [ + 'code' => 'testItem', + 'class' => 'testClass', + 'href' => 'http://localhost/admin/testItem', + 'icon' => 'fa fa-angle-double-right', + 'title' => 'Test Item', + 'priority' => 500, + 'permission' => ['Admin.TestItem'], + 'child' => [ + 'testChildItem' => [ + 'code' => 'testChildItem', + 'class' => 'testChildClass', + 'href' => 'http://localhost/admin/testChildItem', + 'icon' => 'fa fa-angle-double-left', + 'title' => 'Test Child Item', + 'priority' => 2000, + 'permission' => ['Admin.TestChildItem'], + ], + 'testChildItem2' => [ + 'code' => 'testChildItem2', + 'class' => 'testChildClass2', + 'href' => 'http://localhost/admin/testChildItem2', + 'icon' => 'fa fa-angle-double-right', + 'title' => 'Test Child Item 2', + 'priority' => 1000, + 'permission' => ['Admin.TestChildItem2'], + ], + ], + ]); + + $items = $this->navigation->getVisibleNavItems(); + + expect($items['testItem']['code'])->toBe('testItem') + ->and($items['testItem']['class'])->toBe('testClass') + ->and($items['testItem']['href'])->toBe('http://localhost/admin/testItem') + ->and($items['testItem']['icon'])->toBe('fa fa-angle-double-right') + ->and($items['testItem']['title'])->toBe('Test Item') + ->and($items['testItem']['priority'])->toBe(500) + ->and($items['testItem']['permission'])->toBe(['Admin.TestItem']); +}); + +it('removes navigation item correctly', function() { $this->navigation->addNavItem('testItem', [ 'code' => 'testItem', 'class' => 'testClass', @@ -109,6 +219,53 @@ expect($items)->not->toHaveKey('testItem'); }); +it('removes navigation child item correctly', function() { + $this->navigation->addNavItem('testItem', [ + 'code' => 'testItem', + 'class' => 'testClass', + 'href' => 'http://localhost/admin/testItem', + 'icon' => 'fa fa-angle-double-right', + 'title' => 'Test Item', + 'priority' => 500, + 'permission' => ['Admin.TestItem'], + ]); + $this->navigation->addNavItem('testChildItem', [ + 'code' => 'testChildItem', + 'class' => 'testChildClass', + 'href' => 'http://localhost/admin/testChildItem', + 'icon' => 'fa fa-angle-double-left', + 'title' => 'Test Child Item', + 'priority' => 2000, + 'permission' => ['Admin.TestChildItem'], + ], 'testItem'); + + $this->navigation->removeNavItem('testChildItem', 'testItem'); + + $items = $this->navigation->getNavItems(); + + expect($items['testItem']['child'])->not->toHaveKey('testItem'); +}); + +it('removes main menu item correctly', function() { + $this->navigation->registerMainItems(['testItem' => [ + [ + 'code' => 'testItem', + 'class' => 'testClass', + 'href' => 'http://localhost/admin/testItem', + 'icon' => 'fa fa-angle-double-right', + 'title' => 'Test Item', + 'priority' => 500, + 'permission' => ['Admin.TestItem'], + ], + ]]); + + $this->navigation->removeMainItem('testItem'); + + $items = $this->navigation->getMainItems(); + + expect($items)->not->toHaveKey('testItem'); +}); + it('merges navigation items correctly', function() { $this->navigation->addNavItem('testItem', [ 'code' => 'testItem', @@ -119,6 +276,15 @@ 'priority' => 500, 'permission' => ['Admin.TestItem'], ]); + $this->navigation->addNavItem('testChildItem', [ + 'code' => 'testChildItem', + 'class' => 'testChildClass', + 'href' => 'http://localhost/admin/testChildItem', + 'icon' => 'fa fa-angle-double-left', + 'title' => 'Test Child Item', + 'priority' => 2000, + 'permission' => ['Admin.TestChildItem'], + ], 'testItem'); $this->navigation->mergeNavItem('testItem', [ 'class' => 'newTestClass', @@ -129,6 +295,11 @@ 'permission' => ['Admin.NewTestItem'], ]); + $this->navigation->mergeNavItem('testChildItem', [ + 'class' => 'newTestChildClass', + 'title' => 'New Test Child Item', + ], 'testItem'); + $items = $this->navigation->getNavItems(); expect($items['testItem']['class'])->toBe('newTestClass') @@ -136,7 +307,9 @@ ->and($items['testItem']['icon'])->toBe('fa fa-angle-double-left') ->and($items['testItem']['title'])->toBe('New Test Item') ->and($items['testItem']['priority'])->toBe(1000) - ->and($items['testItem']['permission'])->toBe(['Admin.NewTestItem']); + ->and($items['testItem']['permission'])->toBe(['Admin.NewTestItem']) + ->and($items['testItem']['child']['testChildItem']['class'])->toBe('newTestChildClass') + ->and($items['testItem']['child']['testChildItem']['title'])->toBe('New Test Child Item'); }); it('filters permitted navigation items correctly', function() { @@ -173,3 +346,21 @@ expect($items)->toHaveKey('testItem') ->and($items)->not->toHaveKey('testItem2'); }); + +it('sets previous URL with full URL', function() { + $navigation = new Navigation(); + $url = 'https://example.com/page'; + + $navigation->setPreviousUrl($url); + + expect($navigation->getPreviousUrl())->toBe($url); +}); + +it('sets previous URL with query parameters', function() { + request()->headers->set('referer', 'https://example.com/page?query=1'); + $navigation = new Navigation(); + + $navigation->setPreviousUrl('https://example.com/page'); + + expect($navigation->getPreviousUrl())->toBe('https://example.com/page?query=1'); +}); diff --git a/tests/src/Admin/Classes/OnboardingStepsTest.php b/tests/src/Admin/Classes/OnboardingStepsTest.php index 368e9c29..d8e5c11e 100644 --- a/tests/src/Admin/Classes/OnboardingStepsTest.php +++ b/tests/src/Admin/Classes/OnboardingStepsTest.php @@ -3,6 +3,7 @@ namespace Igniter\Tests\Admin\Classes; use Igniter\Admin\Classes\OnboardingSteps; +use Igniter\System\Classes\ExtensionManager; dataset('onboardingSteps', [ fn() => [ @@ -69,10 +70,9 @@ ->and($onboardingSteps->getStep('admin::extensions'))->toBeObject() ->and($onboardingSteps->getStep('admin::mail'))->toBeObject() ->and($onboardingSteps->getStep('admin::settings'))->toHaveProperties([ - 'code', 'label', 'description', 'icon', 'url', 'priority', 'complete', 'completed', + 'code', 'label', 'description', 'icon', 'url', 'priority', 'complete', ]) - ->and($onboardingSteps->getStep('admin::settings')->complete)->toBeCallable() - ->and($onboardingSteps->getStep('admin::settings')->completed)->toBeCallable(); + ->and($onboardingSteps->getStep('admin::settings')->complete)->toBeCallable(); }); it('lists onboarding steps correctly', function($steps) { @@ -84,13 +84,40 @@ ->and($steps)->toHaveKey('testStep2'); })->with('onboardingSteps'); +it('lists empty onboarding steps when nothing is registered', function() { + OnboardingSteps::clearCallbacks(); + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('getExtensions')->andReturn([ + 'testExtension' => new class + { + public function registerOnboardingSteps() + { + return 'not-an-array'; + } + }, + ]); + + $steps = $this->onboardingSteps->listSteps(); + + expect($steps)->toBeEmpty(); +}); + it('checks if onboarding is completed correctly', function($steps) { $this->onboardingSteps->registerSteps($steps); expect($this->onboardingSteps->completed())->toBeFalse(); + $class = new class + { + public function method() + { + return true; + } + }; + $steps['testStep1']['complete'] = fn() => true; - $steps['testStep2']['complete'] = fn() => true; + $steps['testStep2']['complete'] = [$class, 'method']; $this->onboardingSteps->registerSteps($steps); expect($this->onboardingSteps->completed())->toBeTrue(); diff --git a/tests/src/Admin/Classes/TemplateTest.php b/tests/src/Admin/Classes/TemplateTest.php index 13ebb4c0..8e015c08 100644 --- a/tests/src/Admin/Classes/TemplateTest.php +++ b/tests/src/Admin/Classes/TemplateTest.php @@ -20,11 +20,16 @@ it('tests appendBlock', function() { $this->template->setBlock('test', 'content'); + $this->template->appendBlock('test', '-append-content'); - $block = $this->template->getBlock('test'); + expect($this->template->getBlock('test')->toHtml())->toBe('content-append-content'); +}); + +it('tests appendBlock on unset block', function() { + $this->template->appendBlock('test', 'append-content'); - expect((string)$block)->toBe('content-append-content'); + expect($this->template->getBlock('test')->toHtml())->toBe('append-content'); }); it('tests setBlock', function() { @@ -65,6 +70,12 @@ expect($this->template->getHeading())->toBe('Test Heading'); }); +it('tests setHeading with subheading', function() { + $this->template->setHeading('Test Heading:Subheading'); + + expect($this->template->getHeading())->toBe('Test Heading Subheading'); +}); + it('tests setButton', function() { $this->template->setButton('Test Button', ['href' => '#']); diff --git a/tests/src/Admin/Classes/ToolbarButtonTest.php b/tests/src/Admin/Classes/ToolbarButtonTest.php index 6548a057..cd0a1444 100644 --- a/tests/src/Admin/Classes/ToolbarButtonTest.php +++ b/tests/src/Admin/Classes/ToolbarButtonTest.php @@ -19,7 +19,15 @@ }); it('tests getAttributes', function() { - $this->toolbarButton->displayAs('text', ['context' => 'test', 'permission' => 'test', 'label' => 'Test Label', 'class' => 'test-class', 'href' => 'test']); + $this->toolbarButton->displayAs('text', [ + 'context' => 'test', + 'permission' => 'test', + 'label' => 'Test Label', + 'class' => 'test-class', + 'href' => 'test', + 'arrayAttribute' => ['test'], + 'disabled' => true, + ]); $attributes = $this->toolbarButton->getAttributes(); diff --git a/tests/src/Admin/Classes/WidgetsTest.php b/tests/src/Admin/Classes/WidgetsTest.php index 9b3df82d..96a1ae34 100644 --- a/tests/src/Admin/Classes/WidgetsTest.php +++ b/tests/src/Admin/Classes/WidgetsTest.php @@ -5,6 +5,7 @@ use Igniter\Admin\Classes\Widgets; use Igniter\System\Classes\BaseExtension; use Igniter\System\Classes\ExtensionManager; +use Igniter\Tests\Fixtures\Widgets\TestWidget; beforeEach(function() { $this->testExtension = $this->createMock(BaseExtension::class); @@ -33,12 +34,25 @@ expect($widgets)->toBeArray()->toHaveKey('TestWidget'); }); -it('tests resolveBulkActionWidget', function() { - $this->widgets->registerBulkActionWidget('TestWidget', ['code' => 'testwidget']); +it('tests resolveBulkActionWidget with code', function() { + $this->widgets->registerBulkActionWidgets(function($manager) { + $manager->registerBulkActionWidget(TestWidget::class, ['code' => 'testwidget']); + }); $widget = $this->widgets->resolveBulkActionWidget('testwidget'); - expect($widget)->toBe('TestWidget'); + expect($widget)->toBe(TestWidget::class) + ->and($this->widgets->resolveBulkActionWidget('invalid-widget'))->toBe('invalid-widget'); +}); + +it('tests resolveBulkActionWidget with class name', function() { + $this->widgets->registerBulkActionWidgets(function($manager) { + $manager->registerBulkActionWidget(TestWidget::class, []); + }); + + $widget = $this->widgets->resolveBulkActionWidget(TestWidget::class); + + expect($widget)->toBe(TestWidget::class); }); it('tests listFormWidgets', function() { @@ -65,12 +79,25 @@ expect($widgets)->toBeArray()->toHaveKey('TestWidget'); }); -it('tests resolveFormWidget', function() { - $this->widgets->registerFormWidget('TestWidget', ['code' => 'testwidget']); +it('tests resolveFormWidget with code', function() { + $this->widgets->registerFormWidgets(function($manager) { + $manager->registerFormWidget(TestWidget::class, ['code' => 'testwidget']); + }); $widget = $this->widgets->resolveFormWidget('testwidget'); - expect($widget)->toBe('TestWidget'); + expect($widget)->toBe(TestWidget::class) + ->and($this->widgets->resolveFormWidget('invalid-widget'))->toBe('invalid-widget'); +}); + +it('tests resolveFormWidget with class name', function() { + $this->widgets->registerFormWidgets(function($manager) { + $manager->registerFormWidget(TestWidget::class, []); + }); + + $widget = $this->widgets->resolveFormWidget(TestWidget::class); + + expect($widget)->toBe(TestWidget::class); }); it('tests listDashboardWidgets', function() { @@ -97,10 +124,23 @@ expect($widgets)->toBeArray()->toHaveKey('TestWidget'); }); -it('tests resolveDashboardWidget', function() { - $this->widgets->registerDashboardWidget('TestWidget', ['code' => 'testwidget']); +it('tests resolveDashboardWidget with code', function() { + $this->widgets->registerDashboardWidgets(function($manager) { + $manager->registerDashboardWidget(TestWidget::class, ['code' => 'testwidget']); + }); $widget = $this->widgets->resolveDashboardWidget('testwidget'); - expect($widget)->toBe('TestWidget'); + expect($widget)->toBe(TestWidget::class) + ->and($this->widgets->resolveDashboardWidget('invalid-widget'))->toBe('invalid-widget'); +}); + +it('tests resolveDashboardWidget with class name', function() { + $this->widgets->registerDashboardWidgets(function($manager) { + $manager->registerDashboardWidget(TestWidget::class, []); + }); + + $widget = $this->widgets->resolveDashboardWidget(TestWidget::class); + + expect($widget)->toBe(TestWidget::class); }); diff --git a/tests/src/Admin/DashboardWidgets/ChartsTest.php b/tests/src/Admin/DashboardWidgets/ChartsTest.php index 132648a1..e90db126 100644 --- a/tests/src/Admin/DashboardWidgets/ChartsTest.php +++ b/tests/src/Admin/DashboardWidgets/ChartsTest.php @@ -2,40 +2,78 @@ namespace Igniter\Tests\Admin\DashboardWidgets; -use Igniter\Admin\Classes\AdminController; use Igniter\Admin\DashboardWidgets\Charts; +use Igniter\Admin\Http\Controllers\Dashboard; use Igniter\System\Facades\Assets; +use Igniter\System\Models\MailTemplate; use Illuminate\Support\Facades\Event; beforeEach(function() { - $this->controller = $this->createMock(AdminController::class); + $this->controller = resolve(Dashboard::class); $this->charts = new Charts($this->controller, [ 'startDate' => now()->subDays(30), 'endDate' => now(), ]); }); -it('tests initialize', function() { - expect($this->charts->property('rangeFormat'))->toBe('MMMM D, YYYY'); +it('initializes with default properties', function() { + expect($this->charts->contextDefinitions)->toBe([]) + ->and($this->charts->property('rangeFormat'))->toBe('MMMM D, YYYY') + ->and($this->charts->property('dataset'))->toBe('reports'); }); -it('tests defineProperties', function() { +it('defines properties correctly', function() { $properties = $this->charts->defineProperties(); - expect($properties)->toBeArray()->toHaveKey('dataset'); + expect($properties)->toHaveKey('dataset') + ->and($properties['dataset'])->toBe([ + 'label' => 'admin::lang.dashboard.text_charts_dataset', + 'default' => 'reports', + 'type' => 'select', + 'placeholder' => 'lang:admin::lang.text_please_select', + 'options' => [$this->charts, 'getDatasetOptions'], + 'validationRule' => 'required|alpha_dash', + ]); }); -it('tests loadAssets', function() { - Assets::shouldReceive('addCss')->once()->with('dashboardwidgets/charts.css', 'charts-css'); +it('loads assets correctly', function() { Assets::shouldReceive('addJs')->once()->with('js/vendor.datetime.js', 'vendor-datetime-js'); Assets::shouldReceive('addJs')->once()->with('js/vendor.chart.js', 'vendor-chart-js'); - Assets::shouldReceive('addJs')->once()->with('dashboardwidgets/charts.js', 'charts-js'); + Assets::shouldReceive('addCss')->once()->withArgs(function($css, $alias) { + return ends_with($css, 'dashboardwidgets/charts.css') && $alias === 'charts-css'; + }); + Assets::shouldReceive('addJs')->once()->withArgs(function($js, $alias) { + return ends_with($js, 'dashboardwidgets/charts.js') && $alias === 'charts-js'; + }); - // Call the loadAssets method $this->charts->loadAssets(); }); +it('renders widget correctly', function() { + $result = $this->charts->render(); + + expect($result)->toBeString(); +}); + it('tests prepareVars', function() { + Charts::registerDatasets(function() { + return [ + 'newDataset' => [ + 'label' => 'igniter::admin.dashboard.text_reports_chart', + 'sets' => [ + 'orders' => [ + 'model' => MailTemplate::class, + 'column' => 'created_at', + 'priority' => 1, + 'datasetFrom' => function() { + return []; + }, + ], + ], + ], + ]; + }); + $this->charts->render(); expect($this->charts->vars['chartContext'])->toBe('reports') @@ -45,16 +83,88 @@ ->and($this->charts->vars['chartData'])->toBeArray()->toHaveKey('datasets'); }); -it('tests getActiveDataset', function() { +it('returns active dataset', function() { $dataset = $this->charts->getActiveDataset(); expect($dataset)->toBe('reports'); }); -it('tests getData', function() { +it('loads dataset from', function() { + $this->travelTo(now()->setMonth(1)); + + Charts::registerDatasets(function() { + return [ + 'newDataset' => [ + 'label' => 'igniter::admin.dashboard.text_reports_chart', + 'datasetFrom' => function() { + return [ + 'datasets' => [ + [ + 'data' => [ + ['x' => '2021-01-01', 'y' => 10], + ['x' => '2021-01-02', 'y' => 20], + ], + ], + ], + ]; + }, + ], + ]; + }); + + $this->charts->setProperty('dataset', 'newDataset'); + $data = $this->charts->getData(); + + expect($data)->toHaveKey('datasets') + ->and($data['datasets'][0]['data'])->toHaveCount(2); +}); + +it('adds dataset correctly', function() { + $this->travelTo(now()->setMonth(1)); + + $this->charts->addDataset('reports', [ + 'label' => 'New Dataset', + 'sets' => [ + 'newDataset' => [ + 'model' => MailTemplate::class, + 'column' => 'created_at', + 'priority' => 1, + 'datasetFrom' => function() { + return []; + }, + ], + ], + ]); + $data = $this->charts->getData(); + expect($data)->toHaveKey('datasets') + ->and($data['datasets'][0]['data'])->toHaveCount(31); +}); + +it('merges dataset correctly', function() { + $this->charts->addDataset('reports', [ + 'label' => 'New Dataset', + 'sets' => [ + 'newDataset' => [ + 'model' => MailTemplate::class, + 'column' => 'created_at', + 'priority' => 1, + 'datasetFrom' => function() { + return []; + }, + ], + ], + ]); + $this->charts->mergeDataset('reports', 'sets', [ + 'newDataset' => [ + 'model' => MailTemplate::class, + 'column' => 'updated_at', + 'priority' => 99, + 'extraConfig' => 'extra', + ], + ]); - expect($data)->toBeArray()->toHaveKey('datasets'); + expect($this->charts->getData()['datasets'][0]['extraConfig'])->toBe('extra'); }); it('tests getDatasetOptions', function() { diff --git a/tests/src/Admin/DashboardWidgets/StatisticsTest.php b/tests/src/Admin/DashboardWidgets/StatisticsTest.php index 5c673486..f4b6b949 100644 --- a/tests/src/Admin/DashboardWidgets/StatisticsTest.php +++ b/tests/src/Admin/DashboardWidgets/StatisticsTest.php @@ -5,6 +5,7 @@ use Igniter\Admin\Classes\AdminController; use Igniter\Admin\DashboardWidgets\Statistics; use Igniter\System\Facades\Assets; +use Igniter\System\Models\MailTemplate; beforeEach(function() { Statistics::registerCards(function() { @@ -53,11 +54,23 @@ }); it('tests getValue', function() { + Statistics::registerCards(function() { + return [ + 'test-sale' => [ + 'label' => 'lang:igniter::admin.dashboard.text_total_sale', + 'icon' => ' text-success fa fa-4x fa-line-chart', + 'valueFrom' => function($cardCode, $start, $end, $callback) { + $callback(MailTemplate::query()); + return '£100.00'; + }, + ], + ]; + }); + + $this->statistics->setProperty('card', 'test-sale'); $this->statistics->render(); - // The exact value will depend on the data in your database - // Here we're just checking that we get a string - expect($this->statistics->vars['statsCount'])->toBe('£0.00'); + expect($this->statistics->vars['statsCount'])->toBe('£100.00'); }); it('renders widget with no errors', function() { diff --git a/tests/src/Admin/FormWidgets/CodeEditorTest.php b/tests/src/Admin/FormWidgets/CodeEditorTest.php index ecf46f07..b7bac7e2 100644 --- a/tests/src/Admin/FormWidgets/CodeEditorTest.php +++ b/tests/src/Admin/FormWidgets/CodeEditorTest.php @@ -7,15 +7,6 @@ use Igniter\System\Facades\Assets; use Igniter\Tests\Fixtures\Controllers\TestController; use Igniter\Tests\Fixtures\Models\TestModel; -use Illuminate\View\Factory; - -dataset('initialization', [ - ['fullPage', false], - ['lineSeparator', null], - ['mode', 'css'], - ['theme', 'material'], - ['readOnly', false], -]); beforeEach(function() { $this->controller = resolve(TestController::class); @@ -25,9 +16,13 @@ ]); }); -it('initializes correctly', function($property, $expected) { - expect($this->codeEditorWidget->$property)->toBe($expected); -})->with('initialization'); +it('initializes correctly', function() { + expect($this->codeEditorWidget->fullPage)->toBeFalse() + ->and($this->codeEditorWidget->lineSeparator)->toBeNull() + ->and($this->codeEditorWidget->mode)->toBe('css') + ->and($this->codeEditorWidget->theme)->toBe('material') + ->and($this->codeEditorWidget->readOnly)->toBeFalse(); +}); it('loads assets correctly', function() { Assets::shouldReceive('addCss')->once()->with('codeeditor.css', 'codeeditor-css'); @@ -56,11 +51,5 @@ }); it('renders correctly', function() { - app()->instance('view', $viewMock = $this->createMock(Factory::class)); - - $viewMock->expects($this->atLeastOnce()) - ->method('exists') - ->with($this->stringContains('codeeditor/codeeditor')); - - $this->codeEditorWidget->render(); -})->throws(\Exception::class); + expect($this->codeEditorWidget->render())->toBeString(); +}); diff --git a/tests/src/Admin/FormWidgets/ColorPickerTest.php b/tests/src/Admin/FormWidgets/ColorPickerTest.php index 08faf44f..9c96cc92 100644 --- a/tests/src/Admin/FormWidgets/ColorPickerTest.php +++ b/tests/src/Admin/FormWidgets/ColorPickerTest.php @@ -7,13 +7,6 @@ use Igniter\Admin\FormWidgets\ColorPicker; use Igniter\System\Facades\Assets; use Igniter\Tests\Fixtures\Models\TestModel; -use Illuminate\View\Factory; - -dataset('initialization', [ - ['showAlpha', false], - ['readOnly', false], - ['disabled', false], -]); beforeEach(function() { $this->defaultValue = '#1abc9c'; @@ -24,9 +17,11 @@ ]); }); -it('initializes correctly', function($property, $expected) { - expect($this->colorPickerWidget->$property)->toBe($expected); -})->with('initialization'); +it('initializes correctly', function() { + expect($this->colorPickerWidget->showAlpha)->toBeFalse(); + expect($this->colorPickerWidget->readOnly)->toBeFalse(); + expect($this->colorPickerWidget->disabled)->toBeFalse(); +}); it('loads assets correctly', function() { Assets::shouldReceive('addJs')->once()->with('colorpicker.js', 'colorpicker-js'); @@ -51,14 +46,8 @@ }); it('renders correctly', function() { - app()->instance('view', $viewMock = $this->createMock(Factory::class)); - - $viewMock->expects($this->atLeastOnce()) - ->method('exists') - ->with($this->stringContains('colorpicker/colorpicker')); - - $this->colorPickerWidget->render(); -})->throws(\Exception::class); + expect($this->colorPickerWidget->render())->toBeString(); +}); it('gets save value correctly', function() { $value = $this->colorPickerWidget->getSaveValue($this->defaultValue); diff --git a/tests/src/Admin/FormWidgets/ConnectorTest.php b/tests/src/Admin/FormWidgets/ConnectorTest.php index 1a5b109a..f4b29106 100644 --- a/tests/src/Admin/FormWidgets/ConnectorTest.php +++ b/tests/src/Admin/FormWidgets/ConnectorTest.php @@ -6,26 +6,9 @@ use Igniter\Admin\FormWidgets\Connector; use Igniter\Admin\Models\Status; use Igniter\Admin\Models\StatusHistory; +use Igniter\Flame\Exception\FlashException; use Igniter\System\Facades\Assets; use Igniter\Tests\Fixtures\Controllers\TestController; -use Illuminate\Http\Request; -use Illuminate\View\Factory; - -dataset('initialization', [ - ['editable', true], - ['sortable', false], -]); - -dataset('connectorData', [ - fn() => [ - 'object_id' => 1, - 'object_type' => 'order', - 'user_id' => 1, - 'status_id' => 1, - 'notify' => 1, - 'comment' => 'Test commment', - ], -]); beforeEach(function() { $this->controller = resolve(TestController::class); @@ -47,9 +30,15 @@ ]); }); -it('initializes correctly', function($property, $expected) { - expect($this->connectorWidget->$property)->toBe($expected); -})->with('initialization'); +it('initializes correctly', function() { + $this->formField->disabled = true; + + $this->connectorWidget->initialize(); + + expect($this->connectorWidget->editable)->toBeTrue() + ->and($this->connectorWidget->sortable)->toBeFalse() + ->and($this->connectorWidget->previewMode)->toBeTrue(); +}); it('loads assets correctly', function() { Assets::shouldReceive('addJs')->once()->with('formwidgets/repeater.js', 'repeater-js'); @@ -62,12 +51,8 @@ }); it('renders correctly', function() { - app()->instance('view', $viewMock = $this->createMock(Factory::class)); - - $viewMock->method('exists')->with($this->stringContains('connector/connector')); - - $this->connectorWidget->render(); -})->throws(\Exception::class); + expect($this->connectorWidget->render())->toBeString(); +}); it('prepares vars correctly', function() { $this->connectorWidget->prepareVars(); @@ -86,57 +71,171 @@ ->toHaveKey('confirmMessage'); }); -it('loads a record correctly', function() { +it('processes existing collection records on render', function() { + $this->connectorWidget->sortable = true; + $statuses = Status::factory()->count(2)->create(); + $this->formField->value = $statuses; + + $this->connectorWidget->prepareVars(); + + expect($this->connectorWidget->vars['fieldItems']->count())->toBe(2); +}); + +it('processes existing array records on render', function() { + $this->connectorWidget->sortable = true; + $statuses = [ + ['status_id' => 1, 'priority' => 1], + ['status_id' => 2, 'priority' => 0], + ]; + $this->formField->value = $statuses; + + $this->connectorWidget->prepareVars(); + + expect($this->connectorWidget->vars['fieldItems'])->toHaveCount(2); +}); + +it('returns no save data when not sortable', function() { + $this->connectorWidget->sortable = false; + + $result = $this->connectorWidget->getSaveValue([]); + + expect($result)->toBe(FormField::NO_SAVE_DATA); +}); + +it('returns processed save value when sortable', function() { + $statuses = Status::factory()->count(2)->create(); + request()->request->add(['___dragged_status_history' => $statuses->pluck('status_id')->all()]); + $this->formField->value = $statuses; + $this->connectorWidget->sortable = true; + + $result = $this->connectorWidget->getSaveValue([]); + + expect($result)->toBe($statuses->map(function($status, $index) use ($result) { + return [ + 'status_id' => $status->getKey(), + 'priority' => $index, + ]; + })->all()); +}); + +it('returns empty results when no sortable field', function() { + $this->connectorWidget->sortable = true; + $result = $this->connectorWidget->getSaveValue([]); + + expect($result)->toBeArray()->toBeEmpty(); +}); + +it('does not sort records when value is not a collection', function() { + $this->connectorWidget->sortable = true; + request()->request->add(['___dragged_status_history' => [1, 2]]); + $this->formField->value = [ + ['status_id' => 1, 'priority' => 1], + ['status_id' => 2, 'priority' => 0], + ]; + + $result = $this->connectorWidget->getSaveValue([]); + + expect($result)->toBeArray()->not->toBeEmpty(); +}); + +it('refreshes widget with existing record', function() { + $statusHistory = StatusHistory::factory()->create(); + request()->request->add(['recordId' => $statusHistory->getKey()]); + + $result = $this->connectorWidget->onRefresh(); + + expect($result)->toBeArray(); +}); + +it('loads new record correctly', function() { expect($this->connectorWidget->onLoadRecord())->toBeString(); }); -it('creates a record correctly', function($connectorData) { - $mockRequest = $this->mock(Request::class); - $mockRequest->shouldReceive('post')->andReturn([ - 'status' => ['connectorData' => $connectorData], - ]); - $mockRequest->shouldReceive('path')->andReturn('admin/dashboard'); - $mockRequest->shouldReceive('setUserResolver')->andReturnNull(); - app()->instance('request', $mockRequest); +it('loads existing record correctly', function() { + $statusHistory = StatusHistory::factory()->create(); + request()->request->add(['recordId' => $statusHistory->getKey()]); + + expect($this->connectorWidget->onLoadRecord())->toBeString(); +}); + +it('creates a record correctly', function() { + request()->request->add(['status' => ['connectorData' => [ + 'object_id' => 1, + 'object_type' => 'order', + 'user_id' => 1, + 'status_id' => 1, + 'notify' => 1, + 'comment' => 'Test commment', + ]]]); expect($this->connectorWidget->onSaveRecord())->toBeArray(); - $connectorData['status_id'] = $this->connectorWidget->model->getKey(); - $this->assertDatabaseHas('status_history', $connectorData); -})->with('connectorData'); + $this->assertDatabaseHas('status_history', [ + 'status_id' => $this->connectorWidget->model->getKey(), + 'object_type' => 'order', + 'notify' => 1, + 'comment' => 'Test commment', + ]); +}); -it('updates a record correctly', function($connectorData) { - $connectorData['status_id'] = $this->connectorWidget->model->getKey(); +it('updates a record correctly', function() { $statusHistory = StatusHistory::factory()->create(); - $mockRequest = $this->mock(Request::class); - $mockRequest->shouldReceive('post')->andReturn([ + request()->request->add([ 'recordId' => $statusHistory->getKey(), - 'status' => ['connectorData' => $connectorData], + 'status' => [ + 'connectorData' => [ + 'object_id' => 1, + 'object_type' => 'order', + 'user_id' => 1, + 'status_id' => $statusHistory->status_id, + 'notify' => 1, + 'comment' => 'Test commment', + ], + ], ]); - $mockRequest->shouldReceive('path')->andReturn('admin/dashboard'); - $mockRequest->shouldReceive('setUserResolver')->andReturnNull(); - app()->instance('request', $mockRequest); expect($this->connectorWidget->onSaveRecord())->toBeArray(); - $connectorData['status_history_id'] = $statusHistory->getKey(); - $this->assertDatabaseHas('status_history', $connectorData); -})->with('connectorData'); + $this->assertDatabaseHas('status_history', [ + 'status_id' => $statusHistory->status_id, + 'object_type' => 'order', + 'notify' => 1, + 'comment' => 'Test commment', + ]); +}); -it('deletes a record correctly', function($connectorData) { +it('returns false when record ID is missing', function() { + expect($this->connectorWidget->onDeleteRecord())->toBeFalse(); +}); + +it('throws exception when record is not found', function() { + request()->request->add(['recordId' => 123]); + + $this->expectException(FlashException::class); + $this->expectExceptionMessage(sprintf(lang('igniter::admin.form.not_found'), 123)); + + $this->connectorWidget->onDeleteRecord(); +}); + +it('deletes a record correctly', function() { $statusHistory = StatusHistory::factory()->create(); - $mockRequest = $this->mock(Request::class); - $mockRequest->shouldReceive('post')->andReturn([ + request()->request->add([ 'recordId' => $statusHistory->getKey(), - 'status' => ['connectorData' => $connectorData], + 'status' => [ + 'connectorData' => [ + 'object_id' => 1, + 'object_type' => 'order', + 'user_id' => 1, + 'status_id' => 1, + 'notify' => 1, + 'comment' => 'Test commment', + ], + ], ]); - $mockRequest->shouldReceive('path')->andReturn('admin/dashboard'); - $mockRequest->shouldReceive('setUserResolver')->andReturnNull(); - app()->instance('request', $mockRequest); $this->connectorWidget->onDeleteRecord(); $this->assertDatabaseMissing('status_history', [ 'status_history_id' => $statusHistory->getKey(), ]); -})->with('connectorData'); +}); diff --git a/tests/src/Admin/FormWidgets/DataTableTest.php b/tests/src/Admin/FormWidgets/DataTableTest.php index d8a19c04..62ce81df 100644 --- a/tests/src/Admin/FormWidgets/DataTableTest.php +++ b/tests/src/Admin/FormWidgets/DataTableTest.php @@ -5,7 +5,10 @@ use Igniter\Admin\Classes\FormField; use Igniter\Admin\FormWidgets\DataTable; use Igniter\Admin\Models\Status; +use Igniter\Admin\Models\StatusHistory; use Igniter\Admin\Widgets\Table; +use Igniter\Flame\Database\Model; +use Igniter\Flame\Exception\SystemException; use Igniter\Tests\Fixtures\Controllers\TestController; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\View\Factory; @@ -13,23 +16,32 @@ beforeEach(function() { $this->controller = resolve(TestController::class); $this->formField = new FormField('status_history', 'Connector'); - $this->colorPickerWidget = new DataTable($this->controller, $this->formField, [ + $this->dataTableWidget = new DataTable($this->controller, $this->formField, [ 'model' => Status::factory()->create(), + 'searchableFields' => ['comment'], + 'showRefreshButton' => true, ]); }); it('initializes correctly', function() { - expect($this->colorPickerWidget->size)->toBe('large') - ->and($this->colorPickerWidget->defaultSort)->toBeNull() - ->and($this->colorPickerWidget->searchableFields)->toBeArray() - ->and($this->colorPickerWidget->showRefreshButton)->toBeFalse() - ->and($this->colorPickerWidget->useAjax)->toBeFalse(); + $this->dataTableWidget->useAjax = true; + + $this->dataTableWidget->initialize(); + + expect($this->dataTableWidget->size)->toBe('large') + ->and($this->dataTableWidget->defaultSort)->toBeNull() + ->and($this->dataTableWidget->searchableFields)->toBeArray() + ->and($this->dataTableWidget->showRefreshButton)->toBeTrue() + ->and($this->dataTableWidget->useAjax)->toBeTrue() + ->and($this->dataTableWidget->config['attributes'])->toHaveKeys([ + 'data-search', 'data-show-refresh', 'data-side-pagination', 'data-silent-sort', + ]); }); it('prepares vars correctly', function() { - $this->colorPickerWidget->prepareVars(); + $this->dataTableWidget->prepareVars(); - expect($this->colorPickerWidget->vars) + expect($this->dataTableWidget->vars) ->toHaveKey('table') ->toHaveKey('dataTableId') ->toHaveKey('size'); @@ -40,18 +52,82 @@ $viewMock->method('exists')->with($this->stringContains('datatable/datatable')); - $this->colorPickerWidget->render(); + $this->dataTableWidget->render(); })->throws(\Exception::class); -it('gets load value correctly', function() { - expect($this->colorPickerWidget->getLoadValue())->toBeArray(); +it('returns load value correctly when value is a collection', function() { + expect($this->dataTableWidget->getLoadValue())->toBeArray(); +}); + +it('returns load value correctly when value is an array', function() { + $this->formField->value = [ + ['name' => 'Test'], + ]; + + $value = $this->dataTableWidget->getLoadValue(); + + expect($value[0])->toHaveKey('id'); +}); + +it('returns save value', function() { + StatusHistory::factory()->for($this->dataTableWidget->model, 'status')->count(2)->create(); + + $this->dataTableWidget->prepareVars(); + $result = $this->dataTableWidget->getSaveValue([]); + + expect($result)->toBeArray(); }); it('gets table correctly', function() { - expect($this->colorPickerWidget->getTable())->toBeInstanceOf(Table::class); + expect($this->dataTableWidget->getTable())->toBeInstanceOf(Table::class); }); it('gets data table records correctly', function() { - expect($this->colorPickerWidget->getDataTableRecords(0, 10, '')) + $this->dataTableWidget->defaultSort = ['status_id', 'desc']; + + expect($this->dataTableWidget->getDataTableRecords(0, 10, 'comment')) ->toBeInstanceOf(LengthAwarePaginator::class); }); + +it('returns options from specific method if exists', function() { + $model = mock(Model::class); + $model->shouldReceive('methodExists')->with('getStatusHistoryDataTableOptions')->andReturn(true); + $model->shouldReceive('getStatusHistoryDataTableOptions')->with('field', [])->andReturn(['option1', 'option2']); + $this->dataTableWidget->model = $model; + + $result = $this->dataTableWidget->getDataTableOptions('field', []); + + expect($result)->toBe(['option1', 'option2']); +}); + +it('returns options from generic method if specific method does not exist', function() { + $model = mock(Model::class); + $model->shouldReceive('methodExists')->with('getStatusHistoryDataTableOptions')->andReturn(false); + $model->shouldReceive('methodExists')->with('getDataTableOptions')->andReturn(true); + $model->shouldReceive('getDataTableOptions')->with('status_history', 'field', [])->andReturn(['option1', 'option2']); + $this->dataTableWidget->model = $model; + + $result = $this->dataTableWidget->getDataTableOptions('field', []); + + expect($result)->toBe(['option1', 'option2']); +}); + +it('throws exception if neither specific nor generic method exists', function() { + $model = mock(Model::class); + $model->shouldReceive('methodExists')->with('getStatusHistoryDataTableOptions')->andReturn(false); + $model->shouldReceive('methodExists')->with('getDataTableOptions')->andReturn(false); + $this->dataTableWidget->model = $model; + + expect(fn() => $this->dataTableWidget->getDataTableOptions('field', []))->toThrow(SystemException::class); +}); + +it('returns empty array if method returns non-array value', function() { + $model = mock(Model::class); + $model->shouldReceive('methodExists')->with('getStatusHistoryDataTableOptions')->andReturn(true); + $model->shouldReceive('getStatusHistoryDataTableOptions')->with('field', [])->andReturn('non-array value'); + $this->dataTableWidget->model = $model; + + $result = $this->dataTableWidget->getDataTableOptions('field', []); + + expect($result)->toBe([]); +}); diff --git a/tests/src/Admin/FormWidgets/DatePickerTest.php b/tests/src/Admin/FormWidgets/DatePickerTest.php index 1188cdf5..d4c7ed29 100644 --- a/tests/src/Admin/FormWidgets/DatePickerTest.php +++ b/tests/src/Admin/FormWidgets/DatePickerTest.php @@ -7,7 +7,6 @@ use Igniter\System\Facades\Assets; use Igniter\Tests\Fixtures\Controllers\TestController; use Igniter\Tests\Fixtures\Models\TestModel; -use Illuminate\View\Factory; beforeEach(function() { $this->controller = resolve(TestController::class); @@ -18,6 +17,8 @@ }); it('initializes correctly', function() { + $this->datePickerWidget->initialize(); + expect($this->datePickerWidget->mode)->toBe('date') ->and($this->datePickerWidget->startDate)->toBeNull() ->and($this->datePickerWidget->endDate)->toBeNull() @@ -26,6 +27,28 @@ ->and($this->datePickerWidget->datesDisabled)->toBeArray(); }); +it('initializes with startDate and endDate string correctly', function() { + $this->datePickerWidget->config['startDate'] = '2022-01-01'; + $this->datePickerWidget->config['endDate'] = '2022-12-31'; + + $this->datePickerWidget->initialize(); + + expect($this->datePickerWidget->mode)->toBe('date') + ->and($this->datePickerWidget->startDate->toDateString())->toBe('2022-01-01') + ->and($this->datePickerWidget->endDate->toDateString())->toBe('2022-12-31'); +}); + +it('initializes with startDate and endDate timestamp correctly', function() { + $this->datePickerWidget->config['startDate'] = strtotime('2022-01-01'); + $this->datePickerWidget->config['endDate'] = strtotime('2022-12-31'); + + $this->datePickerWidget->initialize(); + + expect($this->datePickerWidget->mode)->toBe('date') + ->and($this->datePickerWidget->startDate->toDateString())->toBe('2022-01-01') + ->and($this->datePickerWidget->endDate->toDateString())->toBe('2022-12-31'); +}); + it('loads assets correctly', function() { Assets::shouldReceive('addCss')->once()->with('datepicker.css', 'datepicker-css'); @@ -35,22 +58,43 @@ }); it('renders correctly', function() { - app()->instance('view', $viewMock = $this->createMock(Factory::class)); - - $viewMock->method('exists')->with($this->stringContains('datepicker/datepicker')); - expect($this->datePickerWidget->render())->toBeString(); -})->throws(\Exception::class); +}); + +it('prepares vars in datetime mode correctly', function() { + $this->formField->value = '2022-12-31 23:59:59'; + $this->datePickerWidget->mode = 'datetime'; -it('prepares vars correctly', function() { $this->datePickerWidget->prepareVars(); expect($this->datePickerWidget->vars) - ->toHaveKey('name') + ->toHaveKey('name', 'test_field') ->toHaveKey('timeFormat') ->toHaveKey('dateFormat') + ->toHaveKey('dateTimeFormat', 'Y-m-d H:i') + ->toHaveKey('datePickerFormat', 'DD MMM YYYY HH:mm') + ->toHaveKey('formatAlias', lang('igniter::system.php.date_time_format')) + ->toHaveKey('value') + ->toHaveKey('field') + ->toHaveKey('mode') + ->toHaveKey('startDate') + ->toHaveKey('endDate') + ->toHaveKey('datesDisabled'); +}); + +it('prepares vars in date mode correctly', function() { + $this->formField->value = '2022-12-31'; + $this->datePickerWidget->mode = 'date'; + + $this->datePickerWidget->prepareVars(); + + expect($this->datePickerWidget->vars) + ->toHaveKey('name', 'test_field') + ->toHaveKey('timeFormat') + ->toHaveKey('dateFormat', 'Y-m-d') ->toHaveKey('dateTimeFormat') - ->toHaveKey('formatAlias') + ->toHaveKey('datePickerFormat', 'yyyy-mm-dd') + ->toHaveKey('formatAlias', lang('igniter::system.php.date_format')) ->toHaveKey('value') ->toHaveKey('field') ->toHaveKey('mode') @@ -59,9 +103,44 @@ ->toHaveKey('datesDisabled'); }); -it('gets save value correctly', function() { +it('prepares vars in time mode correctly', function() { + $this->formField->value = '12:00:00'; + $this->datePickerWidget->mode = 'time'; + + $this->datePickerWidget->prepareVars(); + + expect($this->datePickerWidget->vars) + ->toHaveKey('name', 'test_field') + ->toHaveKey('timeFormat', 'H:i') + ->toHaveKey('dateFormat') + ->toHaveKey('dateTimeFormat', 'Y-m-d H:i') + ->toHaveKey('formatAlias', lang('igniter::system.php.time_format')) + ->toHaveKey('value') + ->toHaveKey('field') + ->toHaveKey('mode') + ->toHaveKey('startDate') + ->toHaveKey('endDate') + ->toHaveKey('datesDisabled'); +}); + +it('returns save value correctly', function() { $value = '2022-12-31'; $result = $this->datePickerWidget->getSaveValue($value); expect($result)->toBe($value); }); + +it('returns save value correctly when value is empty', function() { + $value = ''; + $result = $this->datePickerWidget->getSaveValue($value); + + expect($result)->toBeNull(); +}); + +it('returns save value correctly when field is disabled', function() { + $this->formField->disabled = true; + $value = '2022-12-31'; + $result = $this->datePickerWidget->getSaveValue($value); + + expect($result)->toBe(-1); +}); diff --git a/tests/src/Admin/FormWidgets/MarkdownEditorTest.php b/tests/src/Admin/FormWidgets/MarkdownEditorTest.php index e40d0ec4..25396739 100644 --- a/tests/src/Admin/FormWidgets/MarkdownEditorTest.php +++ b/tests/src/Admin/FormWidgets/MarkdownEditorTest.php @@ -8,7 +8,6 @@ use Igniter\Tests\Fixtures\Controllers\TestController; use Igniter\Tests\Fixtures\Models\TestModel; use Illuminate\Http\Request; -use Illuminate\View\Factory; beforeEach(function() { $this->controller = resolve(TestController::class); @@ -22,13 +21,18 @@ expect($this->markdownEditorWidget->mode)->toBe('tab'); }); -it('renders correctly', function() { - app()->instance('view', $viewMock = $this->createMock(Factory::class)); +it('initializes correctly when field is disabled', function() { + $this->formField->disabled = true; + + $this->markdownEditorWidget->initialize(); - $viewMock->method('exists')->with($this->stringContains('markdowneditor/markdowneditor')); + expect($this->markdownEditorWidget->mode)->toBe('tab') + ->and($this->markdownEditorWidget->previewMode)->toBeTrue(); +}); +it('renders correctly', function() { expect($this->markdownEditorWidget->render())->toBeString(); -})->throws(\Exception::class); +}); it('prepares vars correctly', function() { $this->markdownEditorWidget->prepareVars(); diff --git a/tests/src/Admin/FormWidgets/RecordEditorTest.php b/tests/src/Admin/FormWidgets/RecordEditorTest.php index 3e60f4b9..647ef2ed 100644 --- a/tests/src/Admin/FormWidgets/RecordEditorTest.php +++ b/tests/src/Admin/FormWidgets/RecordEditorTest.php @@ -3,45 +3,49 @@ namespace Igniter\Tests\Admin\FormWidgets; use Igniter\Admin\Classes\FormField; +use Igniter\Admin\FormWidgets\Connector; use Igniter\Admin\FormWidgets\RecordEditor; -use Igniter\Admin\Models\Status; -use Igniter\Admin\Models\StatusHistory; +use Igniter\Flame\Exception\FlashException; use Igniter\System\Facades\Assets; +use Igniter\System\Models\Language; +use Igniter\System\Models\Page; use Igniter\Tests\Fixtures\Controllers\TestController; -use Illuminate\Http\Request; -use Illuminate\View\Factory; - -dataset('recordData', [ - fn() => [ - 'object_id' => 1, - 'object_type' => 'order', - 'user_id' => 1, - 'status_id' => 1, - 'notify' => 1, - 'comment' => 'Test commment', - ], -]); +use Illuminate\Database\Eloquent\Model; beforeEach(function() { $this->controller = resolve(TestController::class); $this->formField = new FormField('test_field', 'Record editor'); - $this->formField->arrayName = 'status'; + $this->formField->arrayName = 'language'; $this->recordEditorWidget = new RecordEditor($this->controller, $this->formField, [ - 'model' => Status::factory()->create(), - 'modelClass' => StatusHistory::class, + 'model' => Page::factory()->create(), + 'modelClass' => RecordEditorLanguage::class, 'form' => [ 'fields' => [ - 'object_id' => [], - 'object_type' => [], - 'user_id' => [], - 'status_id' => [], - 'notify' => [], - 'comment' => [], + 'name' => [], + 'code' => [], + 'idiom' => [], + 'status' => [], ], ], ]); }); +it('initializes correctly when request data exists', function() { + $language = Language::factory()->create(); + $recordData = [ + 'recordId' => $language->getKey(), + 'name' => 'Test Language', + 'code' => 'test-language', + 'idiom' => 'te', + 'status' => true, + ]; + request()->headers->add([ + 'X-IGNITER-RECORD-EDITOR-REQUEST-DATA' => json_encode(['recordeditor' => $recordData]), + ]); + + expect($this->recordEditorWidget->initialize())->toBeNull(); +}); + it('prepares vars correctly', function() { $this->recordEditorWidget->prepareVars(); @@ -70,35 +74,126 @@ }); it('renders correctly', function() { - app()->instance('view', $viewMock = $this->createMock(Factory::class)); + request()->headers->add([ + 'X-IGNITER-RECORD-EDITOR-REQUEST-DATA' => json_encode(['recordData' => []]), + ]); + $this->recordEditorWidget->config['modelClass'] = RecordEditorLanguageCustomMethod::class; + $this->recordEditorWidget->addonLeft = 'icon-plus'; - $viewMock->method('exists')->with($this->stringContains('recordeditor/recordeditor')); + $this->recordEditorWidget->initialize(); expect($this->recordEditorWidget->render())->toBeString(); -})->throws(\Exception::class); +}); -it('loads record correctly', function() { +it('loads new record correctly', function() { expect($this->recordEditorWidget->onLoadRecord())->toBeString(); }); -it('creates record correctly', function($recordData) { - $mockRequest = $this->mock(Request::class); - $mockRequest->shouldReceive('post')->andReturn([ - 'status' => ['recordData' => $recordData], - ]); - $mockRequest->shouldReceive('path')->andReturn('admin/dashboard'); - $mockRequest->shouldReceive('setUserResolver')->andReturnNull(); - app()->instance('request', $mockRequest); +it('loads existing record correctly', function() { + $language = Language::factory()->create(); + request()->request->add(['recordId' => $language->getKey()]); + + expect($this->recordEditorWidget->onLoadRecord())->toBeString(); +}); + +it('creates record correctly', function() { + $recordData = [ + 'name' => 'Test Language', + 'code' => 'test-language', + 'idiom' => 'te', + 'status' => true, + ]; + request()->request->add(['language' => ['recordData' => $recordData]]); expect($this->recordEditorWidget->onSaveRecord())->toBeArray(); - $recordData['status_id'] = $this->recordEditorWidget->model->getKey(); - $this->assertDatabaseHas('status_history', $recordData); -})->with('recordData')->skip('This test is failing with error: Missing method [getRecordEditorOptions] in Igniter\Admin\Models\StatusHistory.'); + $this->assertDatabaseHas('languages', $recordData); +}); it('updates record correctly', function() { + $language = Language::factory()->create(); + $recordData = [ + 'name' => 'Test Language', + 'code' => 'test-language', + 'idiom' => 'te', + 'status' => true, + ]; + request()->request->add(['recordId' => $language->getKey()]); + request()->request->add(['language' => ['recordData' => $recordData]]); + expect($this->recordEditorWidget->onSaveRecord())->toBeArray(); -})->skip('This test is failing with error: Missing method [getRecordEditorOptions] in Igniter\Admin\Models\StatusHistory.'); + $this->assertDatabaseHas('languages', $recordData); +}); + +it('onDeleteRecord throws exception when record ID is missing', function() { + expect(fn() => $this->recordEditorWidget->onDeleteRecord()) + ->toThrow(FlashException::class, lang('igniter::admin.form.missing_id')); +}); + +it('onDeleteRecord throws exception when record is not found', function() { + request()->request->add(['recordId' => 123]); + + $this->expectException(FlashException::class); + $this->expectExceptionMessage(sprintf(lang('igniter::admin.form.record_not_found_in_model'), 123, RecordEditorLanguage::class)); + + $this->recordEditorWidget->onDeleteRecord(); +}); it('deletes record correctly', function() { + $language = Language::factory()->create(); + request()->request->add(['recordId' => $language->getKey()]); + expect($this->recordEditorWidget->onDeleteRecord())->toBeArray(); -})->skip('This test is failing with error: Missing method [getRecordEditorOptions] in Igniter\Admin\Models\StatusHistory.'); + + $this->assertDatabaseMissing('languages', ['language_id' => $language->getKey()]); +}); + +it('onAttachRecord throws exception when record ID is missing', function() { + expect(fn() => $this->recordEditorWidget->onAttachRecord()) + ->toThrow(FlashException::class, 'Please select a record to attach.'); +}); + +it('onAttachRecord throws exception when record is not found', function() { + request()->request->add(['recordId' => 123]); + + $this->expectException(FlashException::class); + $this->expectExceptionMessage(sprintf(lang('igniter::admin.form.record_not_found_in_model'), 123, RecordEditorLanguage::class)); + + $this->recordEditorWidget->onAttachRecord(); +}); + +it('attaches record correctly', function() { + $this->recordEditorWidget->bindToController(); + $language = Language::factory()->create(); + request()->request->add(['recordId' => $language->getKey()]); + + $connectorWidget = new Connector($this->controller, $this->formField, [ + 'model' => Page::factory()->create(), + ]); + $connectorWidget->bindToController(); + + expect($this->recordEditorWidget->onAttachRecord())->toBeArray(); +}); + +class RecordEditorLanguage extends Language +{ + protected $table = 'languages'; + + public function getRecordEditorOptions() + { + return []; + } + + public function attachRecordTo($model) {} +} + +class RecordEditorLanguageCustomMethod extends Model +{ + protected $table = 'languages'; + + public function getTestFieldRecordEditorOptions() + { + return []; + } + + public function attachRecordTo($model) {} +} diff --git a/tests/src/Admin/FormWidgets/RelationTest.php b/tests/src/Admin/FormWidgets/RelationTest.php index 2b7dcffa..b9d330f9 100644 --- a/tests/src/Admin/FormWidgets/RelationTest.php +++ b/tests/src/Admin/FormWidgets/RelationTest.php @@ -5,17 +5,10 @@ use Igniter\Admin\Classes\FormField; use Igniter\Admin\FormWidgets\Relation; use Igniter\Admin\Models\Status; +use Igniter\Admin\Models\StatusHistory; +use Igniter\Flame\Exception\SystemException; +use Igniter\System\Models\Currency; use Igniter\Tests\Fixtures\Controllers\TestController; -use Illuminate\View\Factory; - -dataset('initialization', [ - ['relationFrom', null], - ['nameFrom', 'name'], - ['sqlSelect', null], - ['emptyOption', null], - ['scope', null], - ['order', null], -]); beforeEach(function() { $this->controller = resolve(TestController::class); @@ -23,27 +16,87 @@ $this->formField->valueFrom = 'status_history'; $this->relationWidget = new Relation($this->controller, $this->formField, [ 'model' => Status::factory()->create(), + 'select' => 'comment', + 'order' => 'status_id asc', ]); }); -it('initialize correctly', function($property, $expected) { - expect($this->relationWidget->$property)->toBe($expected); -})->with('initialization'); +it('initialize correctly', function() { + expect($this->relationWidget->relationFrom)->toBeNull() + ->and($this->relationWidget->nameFrom)->toBe('name') + ->and($this->relationWidget->sqlSelect)->toBe('comment') + ->and($this->relationWidget->emptyOption)->toBeNull() + ->and($this->relationWidget->scope)->toBeNull() + ->and($this->relationWidget->order)->toBe('status_id asc'); +}); it('renders correctly', function() { - app()->instance('view', $viewMock = $this->createMock(Factory::class)); + expect($this->relationWidget->render())->toBeString(); +}); - $viewMock->method('exists')->with($this->stringContains('relation/relation')); +it('prepares vars correctly', function() { + $this->relationWidget->prepareVars(); - $this->relationWidget->render(); -})->throws(\Exception::class); + expect($this->relationWidget->vars)->toHaveKey('field'); +}); + +it('prepares vars correctly when relation type is belongsTo', function() { + $this->formField = new FormField('status', 'Relation'); + $this->formField->valueFrom = 'status'; + $this->relationWidget = new Relation($this->controller, $this->formField, [ + 'model' => StatusHistory::factory()->create(), + 'scope' => 'isForOrder', + ]); + + $this->relationWidget->prepareVars(); + + expect($this->relationWidget->vars)->toHaveKey('field'); +}); + +it('applies custom sorted scope correctly', function() { + $this->formField = new FormField('country', 'Relation'); + $this->formField->valueFrom = 'country'; + $this->relationWidget = new Relation($this->controller, $this->formField, [ + 'model' => Currency::factory()->create(), + ]); + + $this->relationWidget->prepareVars(); + + expect($this->relationWidget->vars['field']->options)->not->toBeEmpty(1); +}); + +it('prepares vars correctly when model and related model are the same', function() { + $this->formField = new FormField('status', 'Relation'); + $this->formField->valueFrom = 'status'; + $this->relationWidget = new Relation($this->controller, $this->formField, [ + 'model' => (new TestStatusModel)->create([ + 'status_name' => 'Test Status', + 'status_for' => 'order', + 'status_color' => '#000000', + 'status_comment' => 'Test Status', + 'notify_customer' => true, + ]), + ]); -it('prepares vars correctly', function() { $this->relationWidget->prepareVars(); expect($this->relationWidget->vars)->toHaveKey('field'); }); +it('throws exception when relationship does not exists', function() { + $this->formField = new FormField('invalid_relation', 'Relation'); + $this->formField->valueFrom = 'invalid_relation'; + $this->relationWidget = new Relation($this->controller, $this->formField, [ + 'model' => StatusHistory::factory()->create(), + ]); + + expect(fn() => $this->relationWidget->prepareVars()) + ->toThrow(SystemException::class, sprintf( + lang('igniter::admin.alert_missing_model_definition'), + $this->relationWidget->model::class, 'invalid_relation', + )); +}); + it('getSaveValue method works correctly', function($value, $expected) { $result = $this->relationWidget->getSaveValue($value); @@ -71,3 +124,12 @@ ['attribute', 'attribute'], ['relation_field', 'relation_field'], ]); + +class TestStatusModel extends Status +{ + public $relation = [ + 'belongsTo' => [ + 'status' => [TestStatusModel::class, 'status_id'], + ], + ]; +} diff --git a/tests/src/Admin/FormWidgets/RepeaterTest.php b/tests/src/Admin/FormWidgets/RepeaterTest.php index 0563558f..c8a9d568 100644 --- a/tests/src/Admin/FormWidgets/RepeaterTest.php +++ b/tests/src/Admin/FormWidgets/RepeaterTest.php @@ -9,17 +9,6 @@ use Igniter\Admin\Widgets\Form; use Igniter\System\Facades\Assets; use Igniter\Tests\Fixtures\Controllers\TestController; -use Illuminate\Http\Request; -use Illuminate\View\Factory; - -dataset('initialization', [ - ['sortable', false], - ['prompt', null], - ['sortColumnName', 'priority'], - ['showAddButton', true], - ['showRemoveButton', true], - ['emptyMessage', 'lang:igniter::admin.text_empty'], -]); dataset('repeaterData', [ fn() => [ @@ -42,7 +31,9 @@ 'fields' => [ 'object_id' => [], 'object_type' => [], - 'user_id' => [], + 'user_id' => [ + 'type' => 'hidden', + ], 'status_id' => [], 'notify' => [], 'comment' => [], @@ -51,9 +42,28 @@ ]); }); -it('initializes correctly', function($property, $expected) { - expect($this->repeaterWidget->$property)->toBe($expected); -})->with('initialization'); +it('initializes correctly', function() { + expect($this->repeaterWidget->sortable)->toBeFalse() + ->and($this->repeaterWidget->prompt)->toBeNull() + ->and($this->repeaterWidget->sortColumnName)->toBe('priority') + ->and($this->repeaterWidget->showAddButton)->toBeTrue() + ->and($this->repeaterWidget->showRemoveButton)->toBeTrue() + ->and($this->repeaterWidget->emptyMessage)->toBe('lang:igniter::admin.text_empty'); +}); + +it('initializes correctly when related model does not exits', function() { + $this->formField = new FormField('invalid_relation', 'Repeater'); + $this->formField->arrayName = 'status'; + + $repeaterWidget = new Repeater($this->controller, $this->formField, [ + 'model' => Status::factory()->create(), + 'form' => [], + ]); + + $repeaterWidget->prepareVars(); + + expect($repeaterWidget->vars['widgetTemplate'])->not->toBeNull(); +}); it('loads assets correctly', function() { Assets::shouldReceive('addJs')->once()->with('repeater.js', 'repeater-js'); @@ -81,18 +91,38 @@ }); it('renders correctly', function() { - app()->instance('view', $viewMock = $this->createMock(Factory::class)); + StatusHistory::factory()->times(3)->create([ + 'status_id' => $this->repeaterWidget->model->getKey(), + ]); - $viewMock->method('exists')->with($this->stringContains('repeater/repeater')); + $this->repeaterWidget->initialize(); - $this->repeaterWidget->render(); -})->throws(\Exception::class); + expect($this->repeaterWidget->render())->toBeString(); +}); + +it('renders correctly when existing item is a collection', function() { + $statusHistory = StatusHistory::factory()->times(3)->create([ + 'status_id' => $this->repeaterWidget->model->getKey(), + ]); + $this->formField->value = $statusHistory; + request()->request->add([ + '___dragged_field-status-status-history' => range(0, 4), + ]); + + $repeaterWidget = new Repeater($this->controller, $this->formField, [ + 'model' => Status::factory()->create(), + 'form' => [], + ]); + + expect($repeaterWidget->render())->toBeString(); +}); it('gets value from model correctly', function() { StatusHistory::factory()->times(3)->create([ 'status_id' => $this->repeaterWidget->model->getKey(), ]); + $this->repeaterWidget->sortable = true; $this->repeaterWidget->model->reloadRelations(); $value = $this->repeaterWidget->getLoadValue(); @@ -101,13 +131,12 @@ }); it('gets value from request correctly', function($repeaterData) { - $mockRequest = $this->mock(Request::class); - $mockRequest->shouldReceive('post')->andReturn([ + request()->request->add([ 'status' => ['status_history' => [$repeaterData, $repeaterData, $repeaterData]], ]); - $mockRequest->shouldReceive('path')->andReturn('admin/dashboard'); - $mockRequest->shouldReceive('setUserResolver')->andReturnNull(); - app()->instance('request', $mockRequest); + + $this->repeaterWidget->sortable = true; + $this->repeaterWidget->initialize(); $value = $this->repeaterWidget->getLoadValue(); @@ -115,12 +144,9 @@ })->with('repeaterData'); it('gets save value correctly', function($repeaterData) { - $mockRequest = $this->mock(Request::class); - $mockRequest->shouldReceive('post')->andReturn([ + request()->request->add([ Repeater::SORT_PREFIX.$this->formField->getId() => array_flip(range(1, 3)), ]); - $mockRequest->shouldReceive('setUserResolver')->andReturnNull(); - app()->instance('request', $mockRequest); $this->repeaterWidget->sortable = true; @@ -131,19 +157,36 @@ expect($result) ->toBeArray() ->toHaveCount(3) - ->and($result[0])->toHaveKey($this->repeaterWidget->sortColumnName); + ->and($result[0])->toHaveKey($this->repeaterWidget->sortColumnName) + ->and($this->repeaterWidget->getSaveValue('not-an-array'))->toBe(['not-an-array']); })->with('repeaterData'); +it('returns empty visible column when field definition is missing', function() { + $repeaterWidget = new Repeater($this->controller, $this->formField, [ + 'model' => Status::factory()->create(), + 'form' => [], + ]); + + $columns = $repeaterWidget->getVisibleColumns(); + + expect($columns)->toBeArray()->toBeEmpty(); +}); + it('gets visible columns correctly', function($repeaterData) { $columns = $this->repeaterWidget->getVisibleColumns(); expect($columns) ->toBeArray() - ->toHaveKeys(array_keys($repeaterData)); + ->toHaveKeys(array_keys(array_except($repeaterData, ['user_id']))); })->with('repeaterData'); it('gets form widget template correctly', function() { - $template = $this->repeaterWidget->getFormWidgetTemplate(); + $repeaterWidget = new Repeater($this->controller, $this->formField, [ + 'model' => Status::factory()->create(), + 'form' => 'status', + ]); + + $template = $repeaterWidget->getFormWidgetTemplate(); expect($template)->toBeInstanceOf(Form::class); }); diff --git a/tests/src/Admin/FormWidgets/RichEditorTest.php b/tests/src/Admin/FormWidgets/RichEditorTest.php index 39049d62..be269d2a 100644 --- a/tests/src/Admin/FormWidgets/RichEditorTest.php +++ b/tests/src/Admin/FormWidgets/RichEditorTest.php @@ -9,24 +9,21 @@ use Igniter\Tests\Fixtures\Models\TestModel; use Illuminate\View\Factory; -dataset('initialization', [ - ['fullPage', false], - ['stretch', null], - ['size', null], - ['toolbarButtons', null], -]); - beforeEach(function() { $this->controller = resolve(TestController::class); $this->formField = new FormField('test_field', 'RichEditor'); $this->richEditorWidget = new RichEditor($this->controller, $this->formField, [ 'model' => new TestModel, + 'toolbarButtons' => 'save|delete', ]); }); -it('initializes correctly', function($property, $expected) { - expect($this->richEditorWidget->$property)->toBe($expected); -})->with('initialization'); +it('initializes correctly', function() { + expect($this->richEditorWidget->fullPage)->toBeFalse() + ->and($this->richEditorWidget->stretch)->toBeNull() + ->and($this->richEditorWidget->size)->toBeNull() + ->and($this->richEditorWidget->toolbarButtons)->toBe('save|delete'); +}); it('loads assets correctly', function() { Assets::shouldReceive('addJs')->once()->with('js/vendor.editor.js', 'vendor-editor-js'); diff --git a/tests/src/Admin/FormWidgets/StatusEditorTest.php b/tests/src/Admin/FormWidgets/StatusEditorTest.php index 03b7abad..2dbfa949 100644 --- a/tests/src/Admin/FormWidgets/StatusEditorTest.php +++ b/tests/src/Admin/FormWidgets/StatusEditorTest.php @@ -2,43 +2,25 @@ namespace Igniter\Tests\Admin\FormWidgets; -use Igniter\Admin\Classes\AdminController; use Igniter\Admin\Classes\FormField; use Igniter\Admin\FormWidgets\StatusEditor; use Igniter\Admin\Models\Status; use Igniter\Admin\Models\StatusHistory; +use Igniter\Admin\Widgets\Form; use Igniter\Cart\Models\Order; +use Igniter\Flame\Exception\FlashException; +use Igniter\Local\Facades\Location; use Igniter\System\Facades\Assets; +use Igniter\Tests\Fixtures\Controllers\TestController; +use Igniter\User\Facades\AdminAuth; use Igniter\User\Models\User; use Igniter\User\Models\UserGroup; -use Illuminate\Http\Request; use Illuminate\Support\Facades\Event; -use Illuminate\View\Factory; - -dataset('initialization', [ - ['formTitle', 'igniter::admin.statuses.text_editor_title'], - ['statusArrayName', 'statusData'], - ['statusFormName', 'Status'], - ['statusKeyFrom', 'status_id'], - ['statusNameFrom', 'status_name'], - ['statusModelClass', StatusHistory::class], - ['statusColorFrom', 'status_color'], - ['statusRelationFrom', 'status'], - ['assigneeFormName', 'Assignee'], - ['assigneeArrayName', 'assigneeData'], - ['assigneeKeyFrom', 'assignee_id'], - ['assigneeGroupKeyFrom', 'assignee_group_id'], - ['assigneeGroupNameFrom', 'user_group_name'], - ['assigneeRelationFrom', 'assignee'], - ['assigneeNameFrom', 'name'], - ['assigneeOrderPermission', 'Admin.AssignOrders'], - ['assigneeReservationPermission', 'Admin.AssignReservations'], -]); beforeEach(function() { - $this->controllerMock = $this->createMock(AdminController::class); + $this->controller = resolve(TestController::class); $this->formField = new FormField('test_field', 'RichEditor'); - $this->statusEditorWidget = new StatusEditor($this->controllerMock, $this->formField, [ + $this->statusEditorWidget = new StatusEditor($this->controller, $this->formField, [ 'model' => Order::factory()->create(), 'form' => [ 'fields' => [ @@ -50,9 +32,25 @@ ]); }); -it('initializes correctly', function($property, $expected) { - expect($this->statusEditorWidget->$property)->toBe($expected); -})->with('initialization'); +it('initializes correctly', function() { + expect($this->statusEditorWidget->formTitle)->toBe('igniter::admin.statuses.text_editor_title') + ->and($this->statusEditorWidget->statusArrayName)->toBe('statusData') + ->and($this->statusEditorWidget->statusFormName)->toBe('Status') + ->and($this->statusEditorWidget->statusKeyFrom)->toBe('status_id') + ->and($this->statusEditorWidget->statusNameFrom)->toBe('status_name') + ->and($this->statusEditorWidget->statusModelClass)->toBe(StatusHistory::class) + ->and($this->statusEditorWidget->statusColorFrom)->toBe('status_color') + ->and($this->statusEditorWidget->statusRelationFrom)->toBe('status') + ->and($this->statusEditorWidget->assigneeFormName)->toBe('Assignee') + ->and($this->statusEditorWidget->assigneeArrayName)->toBe('assigneeData') + ->and($this->statusEditorWidget->assigneeKeyFrom)->toBe('assignee_id') + ->and($this->statusEditorWidget->assigneeGroupKeyFrom)->toBe('assignee_group_id') + ->and($this->statusEditorWidget->assigneeGroupNameFrom)->toBe('user_group_name') + ->and($this->statusEditorWidget->assigneeRelationFrom)->toBe('assignee') + ->and($this->statusEditorWidget->assigneeNameFrom)->toBe('name') + ->and($this->statusEditorWidget->assigneeOrderPermission)->toBe('Admin.AssignOrders') + ->and($this->statusEditorWidget->assigneeReservationPermission)->toBe('Admin.AssignReservations'); +}); it('loads assets correctly', function() { Assets::shouldReceive('addJs')->once()->with('formwidgets/recordeditor.modal.js', 'recordeditor-modal-js'); @@ -74,28 +72,17 @@ }); it('renders correctly', function() { - app()->instance('view', $viewMock = $this->createMock(Factory::class)); - - $viewMock->method('exists')->with($this->stringContains('repeater/repeater')); - - $this->repeaterWidget->render(); -})->throws(\Exception::class); + expect($this->statusEditorWidget->render())->toBeString(); +}); it('gets save value correctly', function() { expect($this->statusEditorWidget->getSaveValue(null))->toBe(FormField::NO_SAVE_DATA); }); it('loads status without errors', function() { - $mockRequest = $this->mock(Request::class); - $mockRequest->shouldReceive('post')->andReturn([ + request()->request->add([ 'recordId' => 'load-status', ]); - $mockRequest->shouldReceive('setUserResolver')->andReturnNull(); - $mockRequest->shouldReceive('getPathInfo')->andReturn('admin/dashboard'); - $mockRequest->shouldReceive('root')->andReturn('localhost'); - $mockRequest->shouldReceive('getScheme')->andReturn('https'); - $mockRequest->shouldReceive('input')->andReturn(''); - app()->instance('request', $mockRequest); $result = $this->statusEditorWidget->onLoadRecord(); @@ -106,23 +93,17 @@ Event::fake(); $user = User::factory()->create(); + $this->controller->setUser($user); $status = Status::factory()->create(); $selectedStatus = Status::factory()->create(); $this->statusEditorWidget->model->status_id = $status->getKey(); - - $mockRequest = $this->mock(Request::class); - $mockRequest->shouldReceive('post')->andReturn([ + request()->request->add([ 'context' => 'status', 'statusData' => [ 'status_id' => $selectedStatus->getKey(), 'comment' => 'Test new comment', ], ]); - $mockRequest->shouldReceive('setUserResolver')->andReturnNull(); - $mockRequest->shouldReceive('path')->andReturn('admin/dashboard'); - app()->instance('request', $mockRequest); - - $this->controllerMock->method('getUser')->willReturn($user); expect($this->statusEditorWidget->onSaveRecord())->toBeArray(); @@ -135,17 +116,28 @@ ]); }); +it('updates status fails with errors', function() { + Event::fake(); + + $user = User::factory()->create(); + $this->controller->setUser($user); + $status = Status::factory()->create(); + $this->statusEditorWidget->model->status_id = $status->getKey(); + request()->request->add([ + 'context' => 'status', + 'statusData' => [ + 'status_id' => 123, + 'comment' => 'Test new comment', + ], + ]); + + expect($this->statusEditorWidget->onSaveRecord())->toBeArray(); +}); + it('loads assignee without errors', function() { - $mockRequest = $this->mock(Request::class); - $mockRequest->shouldReceive('post')->andReturn([ + request()->request->add([ 'recordId' => 'load-assignee', ]); - $mockRequest->shouldReceive('setUserResolver')->andReturnNull(); - $mockRequest->shouldReceive('getPathInfo')->andReturn('admin/dashboard'); - $mockRequest->shouldReceive('root')->andReturn('localhost'); - $mockRequest->shouldReceive('getScheme')->andReturn('https'); - $mockRequest->shouldReceive('input')->andReturn(''); - app()->instance('request', $mockRequest); $result = $this->statusEditorWidget->onLoadRecord(); @@ -156,19 +148,14 @@ Event::fake(); $user = User::factory()->create(['super_user' => 1]); + $this->controller->setUser($user); $assignee = User::factory()->create(); - $mockRequest = $this->mock(Request::class); - $mockRequest->shouldReceive('post')->andReturn([ + request()->request->add([ 'context' => 'assignee', 'assigneeData' => [ 'assignee_id' => $assignee->getKey(), ], ]); - $mockRequest->shouldReceive('setUserResolver')->andReturnNull(); - $mockRequest->shouldReceive('path')->andReturn('admin/dashboard'); - app()->instance('request', $mockRequest); - - $this->controllerMock->method('getUser')->willReturn($user); expect($this->statusEditorWidget->onSaveRecord())->toBeArray(); @@ -179,16 +166,24 @@ ]); }); +it('updates assignee fails when user is not permitted', function() { + Event::fake(); + + $user = User::factory()->create(); + $this->controller->setUser($user); + request()->request->add([ + 'context' => 'assignee', + ]); + + expect(fn() => $this->statusEditorWidget->onSaveRecord()) + ->toThrow(FlashException::class, lang('igniter::admin.alert_user_restricted')); +}); + it('loads selected status without errors', function() { $status = Status::factory()->create(); - $mockRequest = $this->mock(Request::class); - $mockRequest->shouldReceive('post')->andReturn([ + request()->request->add([ 'statusId' => $status->getKey(), ]); - $mockRequest->shouldReceive('setUserResolver')->andReturnNull(); - $mockRequest->shouldReceive('path')->andReturn('admin/dashboard'); - app()->instance('request', $mockRequest); - $result = $this->statusEditorWidget->onLoadStatus(); expect($result)->toBeArray() @@ -197,13 +192,82 @@ it('loads assignee list without errors', function() { $assigneeGroup = UserGroup::factory()->create(); - $mockRequest = $this->mock(Request::class); - $mockRequest->shouldReceive('post')->andReturn([ + request()->request->add([ 'groupId' => $assigneeGroup->getKey(), ]); - $mockRequest->shouldReceive('setUserResolver')->andReturnNull(); - $mockRequest->shouldReceive('path')->andReturn('admin/dashboard'); - app()->instance('request', $mockRequest); expect($this->statusEditorWidget->onLoadAssigneeList())->toBeArray(); }); + +it('returns empty array when groupId is not provided', function() { + $form = new class extends Form + { + public function __construct() {} + }; + + $result = StatusEditor::getAssigneeOptions($form, 'field'); + + expect($result)->toBe([]); +}); + +it('returns assignee options when groupId is provided', function() { + $form = new class extends Form + { + public function __construct() {} + + public function getField($field): ?FormField + { + $formField = new FormField('assignee_group_id', 'select'); + $formField->value = '1'; + + return $formField; + } + }; + request()->request->add(['groupId' => '1']); + + Location::shouldReceive('currentOrAssigned')->andReturn([1, 2]); + + $result = StatusEditor::getAssigneeOptions($form, 'field'); + + expect($result)->toBeCollection(); +}); + +it('returns empty array when no locations are assigned', function() { + $form = new class extends Form + { + public function __construct() {} + + public function getField($field): ?FormField + { + $formField = new FormField('assignee_group_id', 'select'); + $formField->value = '1'; + + return $formField; + } + }; + request()->request->add(['groupId' => '1']); + + Location::shouldReceive('currentOrAssigned')->andReturn([]); + + $result = StatusEditor::getAssigneeOptions($form, 'field'); + + expect($result)->toBeCollection(); +}); + +it('returns all user group options for super user', function() { + $user = User::factory()->superUser()->create(); + AdminAuth::setUser($user); + + $result = StatusEditor::getAssigneeGroupOptions(); + + expect($result)->toBeCollection(); +}); + +it('returns user group options for non-super user', function() { + $user = User::factory()->create(); + AdminAuth::setUser($user); + + $result = StatusEditor::getAssigneeGroupOptions(); + + expect($result)->toBeCollection(); +}); diff --git a/tests/src/Admin/Http/Actions/CalendarControllerTest.php b/tests/src/Admin/Http/Actions/CalendarControllerTest.php index 1da697e7..218c7cdb 100644 --- a/tests/src/Admin/Http/Actions/CalendarControllerTest.php +++ b/tests/src/Admin/Http/Actions/CalendarControllerTest.php @@ -9,6 +9,10 @@ beforeEach(function() { $this->controller = new class extends AdminController { + public array $implement = [ + \Igniter\Admin\Http\Actions\CalendarController::class, + ]; + public $calendarConfig = [ 'calendar' => [ 'title' => 'Calendar Title', @@ -22,6 +26,7 @@ ], ]; }; + $this->controller->widgets['toolbar'] = new \Igniter\Admin\Widgets\Toolbar($this->controller); $this->calendarController = new CalendarController($this->controller); }); @@ -42,5 +47,21 @@ it('renders calendar toolbar without errors', function() { $this->calendarController->calendar(); - expect($this->calendarController->renderCalendarToolbar())->toBeNull(); + expect($this->calendarController->renderCalendarToolbar())->toBeString(); +}); + +it('generates calender events without errors', function() { + $this->calendarController->calendar(); + + $calendarWidget = $this->calendarController->getCalendarWidget(); + + expect($calendarWidget->onGenerateEvents())->toBeArray(); +}); + +it('updates calender events without errors', function() { + $this->calendarController->calendar(); + + $calendarWidget = $this->calendarController->getCalendarWidget(); + + expect($calendarWidget->onUpdateEvent())->toBeNull(); }); diff --git a/tests/src/Admin/Http/Actions/FormControllerTest.php b/tests/src/Admin/Http/Actions/FormControllerTest.php index 42bdd275..164fbb4f 100644 --- a/tests/src/Admin/Http/Actions/FormControllerTest.php +++ b/tests/src/Admin/Http/Actions/FormControllerTest.php @@ -5,7 +5,8 @@ use Igniter\Admin\Classes\AdminController; use Igniter\Admin\Http\Actions\FormController; use Igniter\Admin\Models\Status; -use Igniter\System\Classes\FormRequest; +use Igniter\Admin\Widgets\Form; +use Igniter\System\Models\Currency; use Illuminate\Http\RedirectResponse; beforeEach(function() { @@ -52,11 +53,14 @@ ], ], ]; + + public function formValidate($model, $formWidget) + { + return $formWidget->getSaveData() ?: false; + } }; + $this->controller->widgets['toolbar'] = new \Igniter\Admin\Widgets\Toolbar($this->controller); $this->formController = new FormController($this->controller); - - $this->formRequestMock = $this->createMock(FormRequest::class); - app()->instance('TestRequest', $this->formRequestMock); }); it('initializes form with model and context', function() { @@ -87,6 +91,10 @@ ], ]); + $formConfig = $this->formController->getConfig(); + $formConfig['request'] = \Igniter\Admin\Http\Requests\StatusRequest::class; + $this->formController->setConfig($formConfig); + $response = $this->formController->create_onSave(); expect($response)->toBeInstanceOf(RedirectResponse::class) @@ -102,6 +110,12 @@ ]); }); +it('create_onSave returns null when controller level form validation fails', function() { + $response = $this->formController->create_onSave(); + + expect($response)->toBeNull(); +}); + it('runs the edit action', function() { $record = Status::factory()->create(); $recordId = $record->getKey(); @@ -117,7 +131,6 @@ $record = Status::factory()->create(); $recordId = $record->getKey(); $context = 'edit'; - request()->request->add([ 'Status' => [ 'status_name' => 'New status - '.$recordId, @@ -143,6 +156,80 @@ ]); }); +it('edits an existing record with relationship', function() { + $currency = Currency::factory()->create(); + $recordId = $currency->getKey(); + $context = 'edit'; + request()->request->add([ + 'Currency' => [ + 'currency_name' => 'New currency - '.$recordId, + 'currency_code' => 'GBP', + 'currency_rate' => 1.0, + 'country_id' => 1, + 'symbol_position' => '1', + 'currency_symbol' => '£', + 'thousand_sign' => ',', + 'decimal_sign' => '.', + 'decimal_position' => '2', + 'currency_status' => 1, + 'country' => [ + 'country_id' => 1, + ], + ], + ]); + + $controller = new class extends AdminController + { + public array $implement = [ + \Igniter\Admin\Http\Actions\FormController::class, + ]; + + public $formConfig = [ + 'name' => 'Controller', + 'model' => Currency::class, + 'edit' => [ + 'title' => 'Edit record', + 'redirect' => 'path/edit/{id}', + 'redirectClose' => 'path', + 'redirectNew' => 'path/create', + ], + 'configFile' => [ + 'form' => [ + 'toolbar' => [ + 'buttons' => [], + ], + 'fields' => [ + 'currency_name' => [], + 'currency_code' => [], + 'country_id' => [], + 'currency_rate' => [], + 'symbol_position' => [], + 'currency_symbol' => [], + 'thousand_sign' => [], + 'decimal_sign' => [], + 'decimal_position' => [], + 'currency_status' => [], + 'country' => [], + ], + ], + ], + ]; + }; + $formController = new FormController($controller); + $response = $formController->edit_onSave($context, $recordId); + + expect($response)->toBeInstanceOf(RedirectResponse::class); +}); + +it('edit_onSave returns null when controller level form validation fails', function() { + $record = Status::factory()->create(); + $recordId = $record->getKey(); + $context = 'edit'; + $response = $this->formController->edit_onSave($context, $recordId); + + expect($response)->toBeNull(); +}); + it('deletes an existing record', function() { $record = Status::factory()->create(); $recordId = $record->getKey(); @@ -155,6 +242,22 @@ $this->assertDatabaseMissing('statuses', ['status_id' => $recordId]); }); +it('returns error when deleting fails', function() { + $record = Status::factory()->create(); + $recordId = $record->getKey(); + $context = 'edit'; + + Status::deleting(function() { + return false; + }); + + $response = $this->formController->edit_onDelete($context, $recordId); + + expect($response)->toBeInstanceOf(RedirectResponse::class); + + $this->assertDatabaseHas('statuses', ['status_id' => $recordId]); +}); + it('previews an existing record', function() { $record = Status::factory()->create(); $recordId = $record->getKey(); @@ -166,3 +269,91 @@ ->and($this->formController->getFormModel()->getKey())->toBe($recordId) ->and($this->formController->getFormContext())->toBe($context); }); + +it('binds events correctly', function() { + $recordId = Status::factory()->create()->getKey(); + $context = 'edit'; + $controller = new class extends AdminController + { + public array $implement = [ + \Igniter\Admin\Http\Actions\FormController::class, + ]; + + public $formConfig = [ + 'name' => 'Controller', + 'model' => Status::class, + 'edit' => [ + 'title' => 'Edit record', + 'redirect' => 'path/edit/{id}', + 'redirectClose' => 'path', + 'redirectNew' => 'path/create', + ], + 'configFile' => [ + 'form' => [ + 'toolbar' => [ + 'buttons' => [], + ], + 'fields' => [ + 'status_name' => [], + 'status_for' => [], + 'status_color' => [], + 'status_comment' => [], + 'notify_customer' => [], + ], + ], + ], + ]; + + public function formExtendRefreshData(Form $host, array $saveData) + { + return []; + } + }; + $formController = new FormController($controller); + $formController->edit($context, $recordId); + + expect($controller->widgets['form']->onRefresh())->toBeArray(); +}); + +it('renders form correctly', function() { + $recordId = Status::factory()->create()->getKey(); + $context = 'edit'; + $this->formController->edit($context, $recordId); + + expect($this->formController->renderForm())->toBeString() + ->and($this->controller->widgets['form']->onRefresh())->toBeArray(); +}); + +it('renders form toolbar correctly', function() { + $recordId = Status::factory()->create()->getKey(); + $context = 'edit'; + $this->formController->edit($context, $recordId); + + expect($this->formController->renderFormToolbar())->toBeString(); +}); + +it('appends new to redirect context when post new is true', function() { + request()->request->add(['new' => 1]); + $this->formController->setConfig(['context' => ['redirectNew' => 'redirect-url']]); + + $result = $this->formController->makeRedirect('context'); + + expect($result->getTargetUrl())->toBe(admin_url('redirect-url')); +}); + +it('appends close to redirect context when post close is true', function() { + request()->request->add(['close' => 1]); + $this->formController->setConfig(['context' => ['redirectClose' => 'redirect-url']]); + + $result = $this->formController->makeRedirect('context'); + + expect($result->getTargetUrl())->toBe(admin_url('redirect-url')); +}); + +it('refreshes controller when post refresh is true', function() { + request()->request->add(['refresh' => 1]); + + $result = $this->formController->makeRedirect('context'); + + expect($result)->toBeInstanceOf(RedirectResponse::class); +}); diff --git a/tests/src/Admin/Http/Actions/ListControllerTest.php b/tests/src/Admin/Http/Actions/ListControllerTest.php index 13a15469..d15fecac 100644 --- a/tests/src/Admin/Http/Actions/ListControllerTest.php +++ b/tests/src/Admin/Http/Actions/ListControllerTest.php @@ -3,10 +3,10 @@ namespace Igniter\Tests\Admin\Http\Actions; use Igniter\Admin\Classes\AdminController; +use Igniter\Admin\Facades\AdminMenu; use Igniter\Admin\Http\Actions\ListController; use Igniter\Admin\Models\Status; use Igniter\Admin\Widgets\Lists; -use Illuminate\Pagination\LengthAwarePaginator; beforeEach(function() { $this->controller = new class extends AdminController @@ -21,13 +21,24 @@ 'title' => 'List records', 'emptyMessage' => 'No records found', 'defaultSort' => ['status_id', 'DESC'], + 'back' => 'admin/statuses', 'configFile' => [ 'list' => [ 'toolbar' => [ 'buttons' => [], ], 'filter' => [ - 'scopes' => [], + 'scopes' => [ + 'status_for' => [ + 'label' => 'Status for', + 'type' => 'select', + 'conditions' => 'status_for = :filtered', + 'options' => [ + 'order' => 'Order', + 'reservation' => 'Reservation', + ], + ], + ], 'search' => [ 'prompt' => 'Search records', ], @@ -47,17 +58,31 @@ $this->listController = new ListController($this->controller); }); -it('runs index action', function() { +it('runs index action correctly', function() { + AdminMenu::shouldReceive('setPreviousUrl')->once(); + $this->listController->index(); - $this->listController->renderList(); - $listWidget = $this->listController->getListWidget(); + expect($this->listController->getListWidget())->toBeInstanceOf(Lists::class); +}); - expect($listWidget)->toBeInstanceOf(Lists::class) - ->and($listWidget->vars['records'])->toBeInstanceOf(LengthAwarePaginator::class); +it('flashes warning message when no checked ids are provided', function() { + $this->listController->index_onDelete(); + + expect(flash()->messages()->first())->level->toBe('success'); }); -it('can delete', function() { +it('flashes warning message when no records to delete', function() { + request()->request->add([ + 'checked' => [999], + ]); + + $this->listController->index_onDelete(); + + expect(flash()->messages()->first())->level->toBe('warning'); +}); + +it('deletes records and flashes success message', function() { $checkedIds = [ Status::factory()->create()->getKey(), Status::factory()->create()->getKey(), @@ -70,7 +95,47 @@ $this->listController->index_onDelete(); - expect(Status::whereIn('status_id', $checkedIds)->count())->toBe(0); + expect(Status::whereIn('status_id', $checkedIds)->count())->toBe(0) + ->and(flash()->messages()->first())->level->toBe('success'); +}); + +it('binds events correctly', function() { + request()->request->add(['list_filter' => [ + 'status_for' => 'order', + ]]); + + $this->listController->index(); + $this->listController->renderList(); + + $listFilterWidget = $this->controller->widgets['list_filter']; + $query = Status::query(); + $scope = $listFilterWidget->getScope('status_for'); + $listFilterWidget->fireSystemEvent('admin.filter.extendQuery', [$query, $scope]); + + expect($this->controller->widgets['list_filterSearch']->onSubmit())->toBeArray() + ->and($listFilterWidget->onSubmit())->toBeArray(); +}); + +it('renders list widget correctly', function() { + $this->controller->widgets['toolbar'] = new \Igniter\Admin\Widgets\Toolbar($this->controller); + + $this->listController->index(); + + expect($this->listController->renderList())->toBeString(); +}); + +it('renders list toolbar correctly', function() { + $this->controller->widgets['toolbar'] = new \Igniter\Admin\Widgets\Toolbar($this->controller); + $this->listController->index(); + + expect($this->listController->renderListToolbar())->toBeString(); +}); + +it('renders list filter correctly', function() { + $this->listController->index(); + + expect($this->listController->renderListFilter())->toBeString() + ->and($this->listController->renderListFilter('invalid-alias'))->toBeNull(); }); it('can refresh list', function() { @@ -82,3 +147,9 @@ ->toBeArray() ->toHaveKey('~#'.$listWidget->getId('list')); }); + +it('returns list config correctly', function() { + $this->listController->index(); + + expect($this->listController->getListConfig())->toBeArray(); +}); diff --git a/tests/src/Admin/Http/Controllers/DashboardTest.php b/tests/src/Admin/Http/Controllers/DashboardTest.php new file mode 100644 index 00000000..5995cacb --- /dev/null +++ b/tests/src/Admin/Http/Controllers/DashboardTest.php @@ -0,0 +1,17 @@ +extendDashboardContainer(function($widget) { + return true; + }); + }); + + actingAsSuperUser() + ->get(route('igniter.admin.dashboard')) + ->assertOk(); +}); diff --git a/tests/src/Admin/Http/Controllers/StatusesTest.php b/tests/src/Admin/Http/Controllers/StatusesTest.php new file mode 100644 index 00000000..a73b109f --- /dev/null +++ b/tests/src/Admin/Http/Controllers/StatusesTest.php @@ -0,0 +1,83 @@ +get(route('igniter.admin.statuses')) + ->assertOk(); +}); + +it('loads create status page', function() { + actingAsSuperUser() + ->get(route('igniter.admin.statuses', ['slug' => 'create'])) + ->assertOk(); +}); + +it('loads edit status page', function() { + $status = Status::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.admin.statuses', ['slug' => 'edit/'.$status->getKey()])) + ->assertOk(); +}); + +it('loads status preview page', function() { + $status = Status::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.admin.statuses', ['slug' => 'preview/'.$status->getKey()])) + ->assertOk(); +}); + +it('creates status', function() { + actingAsSuperUser() + ->post(route('igniter.admin.statuses', ['slug' => 'create']), [ + 'Status' => [ + 'status_name' => 'New Status', + 'status_for' => 'order', + 'status_color' => '#000000', + 'status_comment' => 'This is a new status', + 'notify_customer' => 1, + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(Status::where('status_name', 'New Status')->exists())->toBeTrue(); +}); + +it('updates status', function() { + $status = Status::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.admin.statuses', ['slug' => 'edit/'.$status->getKey()]), [ + 'Status' => [ + 'status_name' => 'Updated Status', + 'status_for' => 'order', + 'status_color' => '#000000', + 'status_comment' => 'This is an updated status', + 'notify_customer' => 1, + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(Status::where('status_name', 'Updated Status')->exists())->toBeTrue(); +}); + +it('deletes status', function() { + $status = Status::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.admin.statuses', ['slug' => 'edit/'.$status->getKey()]), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]); + + expect(Status::where('status_id', $status->getKey())->exists())->toBeFalse(); +}); diff --git a/tests/src/Admin/Http/Requests/StatusRequestTest.php b/tests/src/Admin/Http/Requests/StatusRequestTest.php new file mode 100644 index 00000000..9c89a408 --- /dev/null +++ b/tests/src/Admin/Http/Requests/StatusRequestTest.php @@ -0,0 +1,33 @@ +attributes(); + + expect($attributes)->toBe([ + 'status_name' => lang('igniter::admin.label_name'), + 'status_for' => lang('igniter::admin.statuses.label_for'), + 'status_color' => lang('igniter::admin.statuses.label_color'), + 'status_comment' => lang('igniter::admin.statuses.label_comment'), + 'notify_customer' => lang('igniter::admin.statuses.label_notify'), + ]); +}); + +it('returns correct validation rules', function() { + $request = new StatusRequest(); + + $rules = $request->rules(); + + expect($rules)->toBe([ + 'status_name' => ['required', 'string', 'between:2,32'], + 'status_for' => ['required', 'in:order,reservation'], + 'status_color' => ['string', 'max:7'], + 'status_comment' => ['string', 'max:1028'], + 'notify_customer' => ['required', 'boolean'], + ]); +}); diff --git a/tests/src/Admin/Http/Requests/StatusTest.php b/tests/src/Admin/Http/Requests/StatusTest.php deleted file mode 100644 index 521b7982..00000000 --- a/tests/src/Admin/Http/Requests/StatusTest.php +++ /dev/null @@ -1,21 +0,0 @@ -toBeIn(array_get((new StatusRequest)->rules(), 'status_name')) - ->and('required')->toBeIn(array_get((new StatusRequest)->rules(), 'status_for')) - ->and('required')->toBeIn(array_get((new StatusRequest)->rules(), 'notify_customer')); -}); - -it('has max characters rule for inputs', function() { - expect('between:2,32')->toBeIn(array_get((new StatusRequest)->rules(), 'status_name')) - ->and('max:7')->toBeIn(array_get((new StatusRequest)->rules(), 'status_color')) - ->and('max:1028')->toBeIn(array_get((new StatusRequest)->rules(), 'status_comment')); -}); - -it('has in:order,reservation rule for inputs', function() { - expect('in:order,reservation')->toBeIn(array_get((new StatusRequest)->rules(), 'status_for')); -}); diff --git a/tests/src/Admin/Models/Concerns/GeneratesHashTest.php b/tests/src/Admin/Models/Concerns/GeneratesHashTest.php new file mode 100644 index 00000000..1cfb2fa6 --- /dev/null +++ b/tests/src/Admin/Models/Concerns/GeneratesHashTest.php @@ -0,0 +1,50 @@ +query = mock(Builder::class); + $this->traitModel = new class($this->query) + { + use GeneratesHash; + + public function __construct(protected $query) {} + + protected function newQuery() + { + return $this->query; + } + }; +}); + +it('generates a unique hash', function() { + $this->query->shouldReceive('where->count')->andReturn(0); + $hash = $this->traitModel->generateHash(); + + expect($hash)->toBeString() + ->and(strlen($hash))->toBe(32); +}); + +it('regenerates hash if collision occurs', function() { + $this->query->shouldReceive('where->count')->andReturn(1, 0); + + $hash = $this->traitModel->generateHash(); + + expect($hash)->toBeString() + ->and(strlen($hash))->toBe(32); +}); + +it('uses specified column to check for hash uniqueness', function() { + $this->query->shouldReceive('where') + ->withArgs(fn($column, $hash) => $column === 'custom_column') + ->andReturnSelf(); + $this->query->shouldReceive('count')->andReturn(0); + + $hash = $this->traitModel->generateHash('custom_column'); + + expect($hash)->toBeString() + ->and(strlen($hash))->toBe(32); +}); diff --git a/tests/src/Admin/Models/Concerns/LogsStatusHistoryTest.php b/tests/src/Admin/Models/Concerns/LogsStatusHistoryTest.php new file mode 100644 index 00000000..b1615caf --- /dev/null +++ b/tests/src/Admin/Models/Concerns/LogsStatusHistoryTest.php @@ -0,0 +1,162 @@ +create(); + $model = Order::factory()->for($status, 'status')->create(); + + expect($model->status_name)->toBe($status->status_name); +}); + +it('returns status color attribute correctly', function() { + $status = Status::factory()->create(); + $model = Order::factory()->for($status, 'status')->create(); + + expect($model->status_color)->toBe($status->status_color); +}); + +it('returns null for status name attribute if status is null', function() { + $model = Order::factory()->create(); + + expect($model->status_name)->toBeNull(); +}); + +it('returns latest status history correctly', function() { + $model = Order::factory()->create(); + $model->status_history()->create(['status_id' => 1]); + + expect($model->getLatestStatusHistory())->not->toBeNull(); +}); + +it('returns false when adding status history if model does not exist', function() { + $status = Status::factory()->create(); + $model = Order::factory()->make(); + + expect($model->addStatusHistory($status->getKey()))->toBeFalse(); +}); + +it('returns false when adding status history if status is null', function() { + $model = Order::factory()->create(); + + expect($model->addStatusHistory(null))->toBeFalse(); +}); + +it('adds status history successfully', function() { + Event::fake(['admin.statusHistory.added']); + $status = Status::factory()->create(); + $model = Order::factory()->create(); + + $history = $model->addStatusHistory($status); + + expect($history)->toBeInstanceOf(StatusHistory::class) + ->status_id->toBe($status->getKey()) + ->object_id->toBe($model->getKey()); + + Event::assertDispatched('admin.statusHistory.added'); +}); + +it('adds status history with additional data', function() { + $status = Status::factory()->create(); + $model = Order::factory()->create(); + + $history = $model->addStatusHistory($status, ['comment' => 'Test comment']); + + expect($history->comment)->toBe('Test comment'); +}); + +it('returns false when event admin.statusHistory.beforeAddStatus returns false', function() { + Event::listen('admin.statusHistory.beforeAddStatus', function() { + return false; + }); + + $status = Status::factory()->create(); + $model = Order::factory()->create(); + + expect($model->addStatusHistory($status))->toBeFalse(); +}); + +it('returns true if model has status history', function() { + $model = Order::factory()->create(); + $model->status_history()->create(['status_id' => 1]); + + expect($model->hasStatus())->toBeTrue(); +}); + +it('returns false if model does not have status history', function() { + $model = Order::factory()->create(); + + expect($model->hasStatus())->toBeFalse(); +}); + +it('returns true if model has specific status in history', function() { + $status = Status::factory()->create(); + $model = Order::factory()->create(); + $model->status_history()->create(['status_id' => $status->getKey()]); + + expect($model->hasStatus($status->getKey()))->toBeTrue(); +}); + +it('returns false if model does not have specific status in history', function() { + $status = Status::factory()->create(); + $model = Order::factory()->create(); + + expect($model->hasStatus($status->getKey()))->toBeFalse(); +}); + +it('filters query by status id', function() { + $status = Status::factory()->create(); + $model = Order::factory()->create(); + $query = $model->newQuery(); + + $result = $model->scopeWhereStatus($query, $status->getKey()); + + expect($result->toSql())->toContain('where `status_id` in (?)'); +}); + +it('filters query by multiple status ids', function() { + $statuses = Status::factory()->count(2)->create(); + $model = Order::factory()->create(); + $query = $model->newQuery(); + + $result = $model->scopeWhereStatus($query, $statuses->pluck('id')->toArray()); + + expect($result->toSql())->toContain('where `status_id` in (?, ?)'); +}); + +it('filters query by status id greater than or equal to 1 when status id is null', function() { + $model = Order::factory()->create(); + $query = $model->newQuery(); + + $result = $model->scopeWhereStatus($query, null); + + expect($result->toSql())->toContain('where `status_id` >= ?'); +}); + +it('filters query by status id in history', function() { + $status = Status::factory()->create(); + $model = Order::factory()->create(); + $model->status_history()->create(['status_id' => $status->getKey()]); + $query = $model->newQuery(); + + $sql = $model->scopeWhereHasStatusInHistory($query, $status->getKey())->toSql(); + + expect($sql)->toContain('where exists (select * from `status_history` where `orders`.`order_id`') + ->and($sql)->toContain('and `status_id` = ?'); +}); + +it('filters query by not having status id in history', function() { + $status = Status::factory()->create(); + $model = Order::factory()->create(); + $query = $model->newQuery(); + + $sql = $model->scopeDoesntHaveStatusInHistory($query, $status->getKey())->toSql(); + + expect($sql)->toContain('where not exists (select * from `status_history`') + ->and($sql)->toContain('and `status_id` = ?'); +}); diff --git a/tests/src/Admin/Models/StatusHistoryTest.php b/tests/src/Admin/Models/StatusHistoryTest.php new file mode 100644 index 00000000..d7ab0f8b --- /dev/null +++ b/tests/src/Admin/Models/StatusHistoryTest.php @@ -0,0 +1,111 @@ +create(); + $status = Status::factory()->create(); + StatusHistory::factory()->create([ + 'object_id' => $order->getKey(), + 'object_type' => $order->getMorphClass(), + 'status_id' => $status->getKey(), + ]); + + $exists = StatusHistory::alreadyExists($order, $status->getKey()); + + expect($exists)->toBeTrue(); +}); + +it('returns false if object type is not order', function() { + $statusHistory = new StatusHistory([ + 'object_type' => 'some_other_type', + ]); + + expect($statusHistory->isForOrder())->toBeFalse(); +}); + +it('applies related scope correctly', function() { + $model = Order::factory()->create(); + StatusHistory::factory()->create([ + 'object_id' => $model->getKey(), + 'object_type' => $model->getMorphClass(), + ]); + + $query = StatusHistory::applyRelated($model); + + expect($query->count())->toBe(1); +}); + +it('filters by latest status correctly', function() { + $statusId = 1; + StatusHistory::factory()->create([ + 'status_id' => $statusId, + 'created_at' => now()->subDay(), + ]); + StatusHistory::factory()->create([ + 'status_id' => $statusId, + 'created_at' => now(), + ]); + + $query = StatusHistory::whereStatusIsLatest($statusId); + + expect($query->first()->created_at->isToday())->toBeTrue(); +}); + +it('creates a new status history record', function() { + $status = Status::factory()->create(); + $order = Order::factory()->create(); + + $history = StatusHistory::createHistory($status, $order); + + expect($history)->toBeInstanceOf(StatusHistory::class) + ->and($history->status_id)->toBe($status->getKey()) + ->and($history->object_id)->toBe($order->getKey()) + ->and($history->object_type)->toBe($order->getMorphClass()); +}); + +it('returns false if beforeAddStatus event returns false', function() { + Event::listen('admin.statusHistory.beforeAddStatus', function() { + return false; + }); + + $status = Status::factory()->create(); + $order = Order::factory()->create(); + + $history = StatusHistory::createHistory($status, $order); + + expect($history)->toBeFalse(); +}); + +it('updates the object status and status_updated_at', function() { + $status = Status::factory()->create(); + $order = Order::factory()->create(); + + StatusHistory::createHistory($status, $order); + + $order->refresh(); + + expect($order->status_id)->toBe($status->getKey()) + ->and($order->status_updated_at)->not->toBeNull(); +}); + +it('creates history with options', function() { + $status = Status::factory()->create(); + $order = Order::factory()->create(); + $options = [ + 'staff_id' => 1, + 'comment' => 'Test comment', + 'notify' => true, + ]; + + $history = StatusHistory::createHistory($status->getKey(), $order, $options); + + expect($history->user_id)->toBe(1) + ->and($history->comment)->toBe('Test comment') + ->and($history->notify)->toBeTrue(); +}); diff --git a/tests/src/Admin/Models/StatusTest.php b/tests/src/Admin/Models/StatusTest.php new file mode 100644 index 00000000..06633414 --- /dev/null +++ b/tests/src/Admin/Models/StatusTest.php @@ -0,0 +1,63 @@ +create(['status_for' => 'order']); + $status2 = Status::factory()->create(['status_for' => 'order']); + $status3 = Status::factory()->create(['status_for' => 'reservation']); + + $options = Status::getDropdownOptionsForOrder(); + + expect($options)->toHaveKey($status1->status_id) + ->and($options)->toHaveKey($status2->status_id) + ->and($options)->not->toHaveKey($status3->status_id); +}); + +it('returns dropdown options for reservation statuses', function() { + $status1 = Status::factory()->create(['status_for' => 'reservation']); + $status2 = Status::factory()->create(['status_for' => 'reservation']); + $status3 = Status::factory()->create(['status_for' => 'order']); + + $options = Status::getDropdownOptionsForReservation(); + + expect($options)->toHaveKey($status1->status_id) + ->and($options)->toHaveKey($status2->status_id) + ->and($options)->not->toHaveKey($status3->status_id); +}); + +it('returns only order statuses', function() { + Status::where('status_for', 'order')->update(['status_for' => 'reservation']); + Status::factory()->create(['status_for' => 'order']); + Status::factory()->create(['status_for' => 'order']); + Status::factory()->create(['status_for' => 'reservation']); + + $statuses = Status::isForOrder()->get(); + + expect($statuses)->toHaveCount(2) + ->and($statuses->pluck('status_for')->unique())->toContain('order'); +}); + +it('returns only reservation statuses', function() { + Status::where('status_for', 'reservation')->update(['status_for' => 'order']); + Status::factory()->create(['status_for' => 'reservation']); + Status::factory()->create(['status_for' => 'reservation']); + Status::factory()->create(['status_for' => 'order']); + + $statuses = Status::isForReservation()->get(); + + expect($statuses)->toHaveCount(2) + ->and($statuses->pluck('status_for')->unique())->toContain('reservation'); +}); + +it('lists all statuses keyed by status_id', function() { + $status1 = Status::factory()->create(['status_for' => 'order']); + $status2 = Status::factory()->create(['status_for' => 'reservation']); + + $statuses = Status::listStatuses(); + + expect($statuses->keys())->toContain($status1->status_id) + ->and($statuses->keys())->toContain($status2->status_id); +}); diff --git a/tests/src/Admin/Notifications/StatusUpdatedNotificationTest.php b/tests/src/Admin/Notifications/StatusUpdatedNotificationTest.php new file mode 100644 index 00000000..90c8f5ab --- /dev/null +++ b/tests/src/Admin/Notifications/StatusUpdatedNotificationTest.php @@ -0,0 +1,167 @@ +create(); + $assignee = User::factory()->create(['status' => true]); + $assignee2 = User::factory()->create(['status' => true]); + $order = Order::factory()->for($customer, 'customer')->create(); + $status = Status::factory()->create(); + $assigneeGroup = UserGroup::factory()->create(); + $assigneeGroup->users()->attach([$assignee, $assignee2]); + $order->assignee_group()->associate($assigneeGroup)->save(); + $history = StatusHistory::factory()->create([ + 'object_id' => $order->getKey(), + 'object_type' => $order->getMorphClass(), + 'status_id' => $status->getKey(), + 'notify' => true, + ]); + $this->actingAs($assignee, 'igniter-admin'); + + $notification = StatusUpdatedNotification::make()->subject($history); + + $recipients = $notification->getRecipients(); + + expect($recipients)->toHaveCount(2); +}); + +it('returns correct title for order', function() { + $order = Order::factory()->create(); + $status = Status::factory()->create(); + $history = StatusHistory::factory()->create([ + 'object_id' => $order->getKey(), + 'object_type' => $order->getMorphClass(), + 'status_id' => $status->getKey(), + 'notify' => true, + ]); + $notification = StatusUpdatedNotification::make()->subject($history); + + $title = $notification->getTitle(); + + expect($title)->toBe(lang('igniter.cart::default.orders.notify_status_updated_title')); +}); + +it('returns correct title for reservation', function() { + $reservation = Reservation::factory()->create(); + $status = Status::factory()->create(); + $history = StatusHistory::factory()->create([ + 'object_id' => $reservation->getKey(), + 'object_type' => $reservation->getMorphClass(), + 'status_id' => $status->getKey(), + 'notify' => true, + ]); + $notification = StatusUpdatedNotification::make()->subject($history); + + $title = $notification->getTitle(); + + expect($title)->toBe(lang('igniter.reservation::default.notify_status_updated_title')); +}); + +it('returns correct URL for order', function() { + $order = Order::factory()->create(); + $status = Status::factory()->create(); + $history = StatusHistory::factory()->create([ + 'object_id' => $order->getKey(), + 'object_type' => $order->getMorphClass(), + 'status_id' => $status->getKey(), + 'notify' => true, + ]); + $notification = StatusUpdatedNotification::make()->subject($history); + + $url = $notification->getUrl(); + + expect($url)->toBe(admin_url('orders/edit/'.$order->getKey())); +}); + +it('returns correct message for order', function() { + $user = User::factory()->create(['status' => true]); + $order = Order::factory()->create(); + $status = Status::factory()->create(); + $history = StatusHistory::factory()->create([ + 'user_id' => $user->getKey(), + 'object_id' => $order->getKey(), + 'object_type' => $order->getMorphClass(), + 'status_id' => $status->getKey(), + 'notify' => true, + ]); + $notification = StatusUpdatedNotification::make()->subject($history); + + $message = $notification->getMessage(); + + expect($message)->toBe(sprintf( + lang('igniter.cart::default.orders.notify_status_updated'), + $user->full_name, + $order->getKey(), + $status->status_name, + )); +}); + +it('returns correct message for reservation when no user', function() { + $reservation = Reservation::factory()->create(); + $status = Status::factory()->create(); + $history = StatusHistory::factory()->create([ + 'object_id' => $reservation->getKey(), + 'object_type' => $reservation->getMorphClass(), + 'status_id' => $status->getKey(), + 'notify' => true, + ]); + $notification = StatusUpdatedNotification::make()->subject($history); + + $message = $notification->getMessage(); + + expect($message)->toBe(sprintf( + lang('igniter.reservation::default.notify_status_updated'), + lang('igniter::admin.text_system'), + $reservation->getKey(), + $status->status_name, + )); +}); + +it('returns correct message for reservation', function() { + $user = User::factory()->create(['status' => true]); + $reservation = Reservation::factory()->create(); + $status = Status::factory()->create(); + $history = StatusHistory::factory()->create([ + 'user_id' => $user->getKey(), + 'object_id' => $reservation->getKey(), + 'object_type' => $reservation->getMorphClass(), + 'status_id' => $status->getKey(), + 'notify' => true, + ]); + $notification = StatusUpdatedNotification::make()->subject($history); + + $message = $notification->getMessage(); + + expect($message)->toBe(sprintf( + lang('igniter.reservation::default.notify_status_updated'), + $user->full_name, + $reservation->getKey(), + $status->status_name, + )); +}); + +it('returns correct icon', function() { + $notification = StatusUpdatedNotification::make(); + + $icon = $notification->getIcon(); + + expect($icon)->toBe('fa-clipboard-check'); +}); + +it('returns correct alias', function() { + $notification = StatusUpdatedNotification::make(); + + $alias = $notification->getAlias(); + + expect($alias)->toBe('status-updated'); +}); diff --git a/tests/src/Admin/Providers/PermissionServiceProviderTest.php b/tests/src/Admin/Providers/PermissionServiceProviderTest.php new file mode 100644 index 00000000..5e08285c --- /dev/null +++ b/tests/src/Admin/Providers/PermissionServiceProviderTest.php @@ -0,0 +1,16 @@ +listPermissions(); + + expect(array_first($permissions, fn($permission) => $permission->code === 'Admin.Dashboard')->group) + ->toBe('igniter::admin.permissions.name') + ->and(array_first($permissions, fn($permission) => $permission->code === 'Admin.Statuses')->group) + ->toBe('igniter::admin.permissions.name'); +}); diff --git a/tests/src/Admin/Traits/ControllerHelpersTest.php b/tests/src/Admin/Traits/ControllerHelpersTest.php new file mode 100644 index 00000000..68ff2e75 --- /dev/null +++ b/tests/src/Admin/Traits/ControllerHelpersTest.php @@ -0,0 +1,53 @@ +controller = new class extends AdminController + { + }; +}); + +it('returns correct URL with parameters', function() { + $url = $this->controller->pageUrl('dashboard', ['param' => 'value']); + + expect($url)->toBe('http://localhost/admin/dashboard/value'); +}); + +it('returns correct secure URL', function() { + $url = $this->controller->pageUrl('dashboard', [], true); + + expect($url)->toStartWith('https://'); +}); + +it('redirects to correct URL', function() { + $response = $this->controller->redirect('dashboard'); + + expect($response->getTargetUrl())->toBe('http://localhost/admin/dashboard'); +}); + +it('redirects guest to correct URL', function() { + $response = $this->controller->redirectGuest('login'); + + expect($response->getTargetUrl())->toBe('http://localhost/admin/login'); +}); + +it('redirects to intended URL', function() { + $response = $this->controller->redirectIntended('dashboard'); + + expect($response->getTargetUrl())->toBe('http://localhost/admin/dashboard'); +}); + +it('redirects back to fallback URL', function() { + $response = $this->controller->redirectBack(302, [], 'fallback'); + + expect($response->getTargetUrl())->toBe('http://localhost/admin/fallback'); +}); + +it('refreshes the current page', function() { + $response = $this->controller->refresh(); + + expect($response->getTargetUrl())->toBe('http://localhost'); +}); diff --git a/tests/src/Admin/Traits/ControllerUtilsTest.php b/tests/src/Admin/Traits/ControllerUtilsTest.php new file mode 100644 index 00000000..6e675d6d --- /dev/null +++ b/tests/src/Admin/Traits/ControllerUtilsTest.php @@ -0,0 +1,107 @@ +controller = new class extends AdminController + { + }; +}); + +it('returns false if action does not exist', function() { + expect($this->controller->getClass())->toBe($this->controller::class) + ->and($this->controller->checkAction('nonExistentAction'))->toBeFalse(); +}); + +it('throws exception if action is hidden', function() { + $controller = new class extends AdminController + { + public array $hiddenActions = ['hiddenAction']; + + public function hiddenAction() + { + return 'hidden action called'; + } + }; + + expect(fn() => $controller->checkAction('hiddenAction'))->toThrow(FlashException::class); +}); + +it('calls action method via remap correctly', function() { + $controller = new class extends AdminController + { + public string $action = 'actionMethod'; + + public function actionMethod() + { + return 'action called'; + } + }; + + $result = $controller->callAction('actionMethod', []); + + expect($result->getContent())->toBe('action called') + ->and($controller->getAction())->toBe('actionMethod'); +}); + +it('calls action method correctly', function() { + $controller = new class extends Controller + { + use ControllerUtils; + + public function actionMethod() + { + return 'action called'; + } + }; + + $result = $controller->callAction('actionMethod', []); + + expect($result)->toBe('action called'); +}); + +it('throws exception if action method is not found', function() { + expect(fn() => $this->controller->callAction('nonExistentMethod', []))->toThrow(FlashException::class); +}); + +it('sets and gets property value using magic get', function() { + $controller = new class extends AdminController + { + protected $properties = []; + + public function extendableSet(string $name, mixed $value): void + { + $this->properties[$name] = $value; + } + + public function extendableGet(string $name): mixed + { + return $this->properties[$name] ?? null; + } + }; + + $controller->testProperty = 'testValue'; + + expect($controller->testProperty)->toBe('testValue'); +}); + +it('calls method using magic call', function() { + $controller = new class extends AdminController + { + public function extendableCall(string $name, ?array $params = null): string + { + if ($name === 'testMethod') { + return 'method called'; + } + } + }; + + $result = $controller->testMethod(); + + expect($result)->toBe('method called'); +}); diff --git a/tests/src/Admin/Traits/FormExtendableTest.php b/tests/src/Admin/Traits/FormExtendableTest.php new file mode 100644 index 00000000..f56151d9 --- /dev/null +++ b/tests/src/Admin/Traits/FormExtendableTest.php @@ -0,0 +1,38 @@ +controller = resolve(FormExtendableTestController::class); + $this->form = new class($this->controller) extends Form + { + public function __construct(protected \Igniter\Admin\Classes\AdminController $controller) {} + }; +}); + +it('calls callback when extending form fields', function() { + $called = false; + FormExtendableTestController::extendFormFields(function() use (&$called) { + $called = true; + }); + + Event::dispatch('admin.form.extendFields', [$this->form]); + + expect($called)->toBeTrue(); +}); + +it('calls callback when extending form fields before', function() { + $called = false; + $callback = function() use (&$called) { + $called = true; + }; + + FormExtendableTestController::extendFormFieldsBefore($callback); + Event::dispatch('admin.form.extendFieldsBefore', [$this->form]); + + expect($called)->toBeTrue(); +}); diff --git a/tests/src/Admin/Traits/FormModelWidgetTest.php b/tests/src/Admin/Traits/FormModelWidgetTest.php new file mode 100644 index 00000000..1f9807c6 --- /dev/null +++ b/tests/src/Admin/Traits/FormModelWidgetTest.php @@ -0,0 +1,290 @@ +createFormModel(); + + expect($model)->toBeInstanceOf(Status::class); +}); + +it('throws exception when model class is missing', function() { + $widget = new class + { + public $modelClass; + + use FormModelWidget; + }; + + expect(fn() => $widget->createFormModel())->toThrow(FlashException::class); +}); + +it('finds form model by record ID', function() { + $widget = new class + { + use FormModelWidget; + + public $modelClass = Status::class; + }; + + $model = Status::factory()->create(); + + $result = $widget->findFormModel($model->getKey()); + + expect($result->getKey())->toBe($model->getKey()); +}); + +it('throws exception when record ID is missing', function() { + $widget = new class + { + use FormModelWidget; + + public $modelClass = Status::class; + }; + + expect(fn() => $widget->findFormModel(''))->toThrow(FlashException::class); +}); + +it('throws exception when record not found', function() { + $widget = new class + { + use FormModelWidget; + + public $modelClass = Status::class; + }; + + expect(fn() => $widget->findFormModel('123'))->toThrow(FlashException::class); +}); + +it('resolves model attribute correctly', function() { + $widget = new class + { + use FormModelWidget; + + public $formField; + public $model; + }; + $widget->formField = new FormField('text', 'Text field'); + $widget->model = Status::factory()->create(); + + [$model, $attribute] = $widget->resolveModelAttribute('status_history'); + + expect($model)->toBe($widget->model) + ->and($attribute)->toBe('status_history'); +}); + +it('returns null when resolving model attribute fails', function() { + $widget = new class + { + use FormModelWidget; + + public $formField; + public $model; + }; + $widget->formField = new FormField('text', 'Text field'); + $widget->model = Status::factory()->create(); + + $result = $widget->resolveModelAttribute('status[invalid_attribute]'); + + expect($result)->toBe([null, 'invalid_attribute']); +}); + +it('returns relation model correctly', function() { + $widget = new class + { + use FormModelWidget; + + public $model; + public $formField; + public $valueFrom = 'status_history'; + + public function testGetRelationModel() + { + return $this->getRelationModel(); + } + }; + + $widget->formField = new FormField('text', 'Text field'); + $widget->model = Status::factory()->create(); + + expect($widget->testGetRelationModel())->toBeInstanceOf(StatusHistory::class); +}); + +it('throws exception when getting relation model fails', function() { + $widget = new class + { + use FormModelWidget; + + public $model; + public $formField; + public $valueFrom = 'invalid_relation'; + + public function testGetRelationModel() + { + $this->formField = new FormField('text', 'Text field'); + $this->model = Status::factory()->create(); + return $this->getRelationModel(); + } + }; + + expect(fn() => $widget->testGetRelationModel())->toThrow(FlashException::class); +}); + +it('returns relation model instance correctly', function() { + $widget = new class + { + use FormModelWidget; + + public $formField; + public $model; + public $valueFrom = 'status_history'; + + public function testGetRelationObject() + { + $this->formField = new FormField('text', 'Text field'); + $this->model = Status::factory()->create(); + return $this->getRelationObject(); + } + }; + + expect($widget->testGetRelationObject())->toBeInstanceOf(HasMany::class); +}); + +it('throws exception when getting relation model instance fails', function() { + $widget = new class + { + use FormModelWidget; + + public $formField; + public $model; + public $valueFrom = 'invalid_relation'; + + public function testGetRelationObject() + { + $this->formField = new FormField('text', 'Text field'); + $this->model = Status::factory()->create(); + return $this->getRelationObject(); + } + }; + + expect(fn() => $widget->testGetRelationObject())->toThrow(FlashException::class); +}); + +it('returns relation type correctly', function() { + $widget = new class + { + use FormModelWidget; + + public $model; + public $formField; + public $valueFrom = 'status_history'; + + public function testGetRelationType() + { + return $this->getRelationType(); + } + }; + + $widget->formField = new FormField('text', 'Text field'); + $widget->model = Status::factory()->create(); + + expect($widget->testGetRelationType())->toBe('hasMany'); +}); + +it('makes model relation correctly', function() { + $widget = new class + { + use FormModelWidget; + + public $model; + public $valueFrom = 'status_history'; + + public function testMakeModelRelation() + { + return $this->makeModelRelation($this->model, $this->valueFrom); + } + }; + $widget->model = new IlluminateModel(); + + expect($widget->testMakeModelRelation())->toBeInstanceOf(StatusHistory::class); +}); + +it('sets model attributes with nested data', function() { + $model = new IlluminateModel(); + $widget = new class + { + use FormModelWidget; + + public function testSetModelAttributes($model, $saveData) + { + return $this->setModelAttributes($model, $saveData); + } + }; + + $saveData = ['status_history' => [['comment' => 'Some comment']]]; + + expect(fn() => $widget->testSetModelAttributes($model, $saveData))->toThrow(\LogicException::class); +}); + +it('does not set attributes starting with underscore', function() { + $model = new Status(); + $widget = new class + { + use FormModelWidget; + + public function testSetModelAttributes($model, $saveData) + { + return $this->setModelAttributes($model, $saveData); + } + }; + + $saveData = ['_token' => 'some_token', 'status_name' => 'Pending']; + $widget->testSetModelAttributes($model, $saveData); + + expect($model->status_name)->toBe('Pending') + ->and(isset($model->_token))->toBeFalse(); +}); + +it('skips setting attributes with NO_SAVE_DATA', function() { + $model = new Status(); + $widget = new class + { + use FormModelWidget; + + public function testSetModelAttributes($model, $saveData) + { + return $this->setModelAttributes($model, $saveData); + } + }; + + $saveData = ['status_name' => FormField::NO_SAVE_DATA, 'status_for' => 'example']; + $widget->testSetModelAttributes($model, $saveData); + + expect($model->status_for)->toBe('example') + ->and($model->status_name)->toBeNull(); +}); + +class IlluminateModel extends Model +{ + protected $table = 'statuses'; + + public function status_history() + { + return $this->hasMany(StatusHistory::class); + } +} diff --git a/tests/src/Admin/Traits/ListExtendableTest.php b/tests/src/Admin/Traits/ListExtendableTest.php new file mode 100644 index 00000000..0efb6776 --- /dev/null +++ b/tests/src/Admin/Traits/ListExtendableTest.php @@ -0,0 +1,38 @@ +controller = resolve(ListExtendableTestController::class); + $this->listsWidget = new class($this->controller) extends Lists + { + public function __construct(protected \Igniter\Admin\Classes\AdminController $controller) {} + }; +}); + +it('extends list columns successfully', function() { + $called = false; + ListExtendableTestController::extendListColumns(function() use (&$called) { + $called = true; + }); + + Event::dispatch('admin.list.extendColumns', [$this->listsWidget]); + + expect($called)->toBeTrue(); +}); + +it('extends list query successfully', function() { + $called = false; + ListExtendableTestController::extendListQuery(function() use (&$called) { + $called = true; + }); + + Event::dispatch('admin.list.extendQuery', [$this->listsWidget, Status::query()]); + + expect($called)->toBeTrue(); +}); diff --git a/tests/src/Admin/Traits/ValidatesFormTest.php b/tests/src/Admin/Traits/ValidatesFormTest.php new file mode 100644 index 00000000..0cae3c90 --- /dev/null +++ b/tests/src/Admin/Traits/ValidatesFormTest.php @@ -0,0 +1,184 @@ +traitObject = new class + { + use ValidatesForm; + }; +}); + +it('validates request successfully with valid data', function() { + $request = ['name' => 'John Doe']; + $rules = ['name' => 'required|string']; + + $this->traitObject->validateAfter(function($request) { + return $request; + }); + $result = $this->traitObject->validate($request, $rules); + + expect($result)->toBe($request); +}); + +it('throws validation exception with invalid data', function() { + $request = ['name' => '']; + $rules = ['name' => 'required|string']; + + expect(fn() => $this->traitObject->validate($request, $rules))->toThrow(ValidationException::class); +}); + +it('returns false when validation fails', function() { + $request = ['name' => '']; + $rules = ['name' => 'required|string']; + + $result = $this->traitObject->validatePasses($request, $rules); + + expect($result)->toBeFalse(); +}); + +it('returns validated data when validation passes', function() { + $request = ['name' => 'John Doe']; + $rules = ['name' => 'required|string']; + + $result = $this->traitObject->validatePasses($request, $rules); + + expect($result)->toBe($request); +}); + +it('parses rules correctly', function() { + $rules = [ + ['name', 'Name', 'required|string'], + ]; + $expected = ['name' => 'required|string']; + + $result = $this->traitObject->parseRules($rules); + + expect($result)->toBe($expected); +}); + +it('parses rules returns empty array when no rules provided', function() { + $rules = []; + $expected = []; + + $result = $this->traitObject->parseRules($rules); + + expect($result)->toBe($expected); +}); + +it('parses attributes correctly', function() { + $rules = [ + ['name', 'Name', 'required|string'], + ]; + $expected = ['name' => 'Name']; + + $result = $this->traitObject->parseAttributes($rules); + + expect($result)->toBe($expected); +}); + +it('parses attributes returns empty array when no attributes provided', function() { + $rules = []; + $expected = []; + + $result = $this->traitObject->parseAttributes($rules); + + expect($result)->toBe($expected); +}); + +it('flashes validation errors to session', function() { + $trait = new class + { + use ValidatesForm; + + public function testFlashValidationErrors(array $errors) + { + $this->flashValidationErrors($errors); + } + }; + + $errors = ['name' => ['The name field is required.']]; + +// session()->flash('errors', $errors); + + $trait->testFlashValidationErrors($errors); + + expect(session('errors'))->toBe($errors); +}); + +it('validates form widget with rules in config', function() { + $trait = new class + { + use ValidatesForm; + + public $config = []; + + public function testValidateFormWidget(Form $form, mixed $saveData): mixed + { + return $this->validateFormWidget($form, $saveData); + } + }; + + $form = new class extends Form + { + public ?array $config = ['rules' => ['name' => 'required|string']]; + + public function __construct() {} + }; + + $saveData = ['name' => 'John Doe']; + + $result = $trait->testValidateFormWidget($form, $saveData); + + expect($result)->toBe($saveData); +}); + +it('validates form widget with form request class', function() { + $trait = new class + { + use ValidatesForm; + + public $config = ['request' => TestRequest::class]; + + public function testValidateFormWidget(Form $form, mixed $saveData): mixed + { + return $this->validateFormWidget($form, $saveData); + } + }; + + $form = new class extends Form + { + public ?array $config = []; + + public function __construct() {} + }; + + $saveData = ['name' => 'John Doe']; + + $result = $trait->testValidateFormWidget($form, $saveData); + + expect($result)->toBe($saveData); +}); + +it('validates form request class', function() { + $trait = new class + { + use ValidatesForm; + + public function testValidateFormRequest(?string $requestClass, callable $callback): array + { + return $this->validateFormRequest($requestClass, $callback); + } + }; + $saveData = ['name' => 'John Doe']; + request()->merge($saveData); + + $result = $trait->testValidateFormRequest(TestRequest::class, fn($request) => true); + + expect($result)->toBe($saveData); +}); diff --git a/tests/src/Admin/Traits/WidgetMakerTest.php b/tests/src/Admin/Traits/WidgetMakerTest.php new file mode 100644 index 00000000..c641a64a --- /dev/null +++ b/tests/src/Admin/Traits/WidgetMakerTest.php @@ -0,0 +1,65 @@ +traitObject = new class + { + use WidgetMaker; + + public $controller; + public $configKey; + public $vars = []; + }; + $this->traitObject->controller = resolve(TestController::class); +}); + +it('creates a widget instance with valid class and config', function() { + $widget = $this->traitObject->makeWidget(TestWidget::class, ['property' => 'configValue']); + + expect($widget)->toBeInstanceOf(TestWidget::class) + ->and($widget->property)->toBe('configValue'); +}); + +it('throws exception when widget class does not exist', function() { + expect(fn() => $this->traitObject->makeWidget('NonExistentClass'))->toThrow(SystemException::class); +}); + +it('creates a form widget instance with valid class and string field config', function() { + $widget = $this->traitObject->makeFormWidget(TestFormWidget::class, 'fieldName', ['property' => 'configValue']); + + expect($widget)->toBeInstanceOf(TestFormWidget::class) + ->and($widget->getFormField()->fieldName)->toBe('fieldName') + ->and($widget->property)->toBe('configValue'); +}); + +it('creates a form widget instance with valid class and array field config', function() { + $fieldConfig = ['name' => 'fieldName', 'label' => 'Field Label']; + $widget = $this->traitObject->makeFormWidget(TestFormWidget::class, $fieldConfig, ['property' => 'configValue']); + + $formField = $widget->getFormField(); + expect($widget)->toBeInstanceOf(TestFormWidget::class) + ->and($formField->fieldName)->toBe('fieldName') + ->and($formField->label)->toBe('Field Label') + ->and($widget->property)->toBe('configValue'); +}); + +it('creates a form widget instance with valid class and FormField object', function() { + $formField = new FormField('fieldName', 'Field Label'); + $widget = $this->traitObject->makeFormWidget(TestFormWidget::class, $formField, ['property' => 'configValue']); + + expect($widget)->toBeInstanceOf(TestFormWidget::class) + ->and($widget->getFormField())->toBe($formField) + ->and($widget->property)->toBe('configValue'); +}); + +it('throws exception when form widget class does not exist', function() { + expect(fn() => $this->traitObject->makeFormWidget('NonExistentClass', 'fieldName'))->toThrow(SystemException::class); +}); diff --git a/tests/src/Admin/Widgets/CalendarTest.php b/tests/src/Admin/Widgets/CalendarTest.php index b9da7df7..0efeff04 100644 --- a/tests/src/Admin/Widgets/CalendarTest.php +++ b/tests/src/Admin/Widgets/CalendarTest.php @@ -6,24 +6,19 @@ use Igniter\Flame\Exception\SystemException; use Igniter\System\Facades\Assets; use Igniter\Tests\Fixtures\Controllers\TestController; -use Illuminate\View\Factory; - -dataset('initialization', [ - ['aspectRatio', 2], - ['editable', true], - ['eventLimit', 5], - ['defaultDate', null], - ['popoverPartial', null], -]); beforeEach(function() { $this->controller = resolve(TestController::class); $this->calendarWidget = new Calendar($this->controller); }); -it('initializes correctly', function($property, $expected) { - expect($this->calendarWidget->{$property})->toEqual($expected); -})->with('initialization'); +it('initializes correctly', function() { + expect($this->calendarWidget->aspectRatio)->toBe(2) + ->and($this->calendarWidget->editable)->toBeTrue() + ->and($this->calendarWidget->eventLimit)->toBe(5) + ->and($this->calendarWidget->defaultDate)->toBeNull() + ->and($this->calendarWidget->popoverPartial)->toBeNull(); +}); it('loads assets correctly', function() { Assets::shouldReceive('addJs')->once()->with('js/vendor.datetime.js', 'vendor-datetime-js'); @@ -38,12 +33,9 @@ }); it('renders correctly', function() { - app()->instance('view', $viewMock = $this->createMock(Factory::class)); - - $viewMock->method('exists')->with($this->stringContains('calendar/calendar')); - + $this->calendarWidget->popoverPartial = 'test-partial'; expect($this->calendarWidget->render())->toBeString(); -})->throws(\Exception::class); +}); it('prepares variables correctly', function() { $this->calendarWidget->prepareVars(); @@ -75,5 +67,12 @@ }); it('renders popover partial correctly', function() { + $this->calendarWidget->popoverPartial = 'test-partial'; expect($this->calendarWidget->renderPopoverPartial())->toBeString(); -})->throws(SystemException::class); +}); + +it('throws exception when missing popover partial', function() { + $this->calendarWidget->popoverPartial = null; + expect(fn() => $this->calendarWidget->renderPopoverPartial()) + ->toThrow(SystemException::class, sprintf(lang('igniter::admin.calendar.missing_partial'), TestController::class)); +}); diff --git a/tests/src/Admin/Widgets/DashboardContainerTest.php b/tests/src/Admin/Widgets/DashboardContainerTest.php index f3fbb94f..1d661737 100644 --- a/tests/src/Admin/Widgets/DashboardContainerTest.php +++ b/tests/src/Admin/Widgets/DashboardContainerTest.php @@ -2,38 +2,45 @@ namespace Igniter\Tests\Admin\Widgets; +use Igniter\Admin\Classes\Widgets; use Igniter\Admin\Widgets\DashboardContainer; +use Igniter\Flame\Exception\FlashException; use Igniter\System\Facades\Assets; use Igniter\Tests\Fixtures\Controllers\TestController; +use Igniter\Tests\Fixtures\Widgets\TestDashboardWidget; use Igniter\User\Facades\AdminAuth; use Igniter\User\Models\User; -use Illuminate\View\Factory; - -dataset('initialization', [ - ['canManage', true], - ['canSetDefault', false], - ['dateRangeFormat', 'MMMM D, YYYY hh:mm A'], - ['startDate', null], - ['endDate', null], -]); beforeEach(function() { AdminAuth::shouldReceive('getUser')->andReturn(User::factory()->create()); $this->controller = resolve(TestController::class); - $this->dashboardContainerWidget = new DashboardContainer($this->controller, [ + $this->widgetConfig = [ 'defaultWidgets' => [ + 'test-dashboard-widget' => [ + 'priority' => 20, + 'width' => '6', + ], 'onboarding' => [ 'priority' => 10, 'width' => '6', ], + 'invalid' => [ + 'priority' => 20, + 'width' => '6', + ], ], - ]); + ]; + $this->dashboardContainerWidget = new DashboardContainer($this->controller, $this->widgetConfig); }); -it('initializes correctly', function($property, $expected) { - expect($this->dashboardContainerWidget->{$property})->toEqual($expected); -})->with('initialization'); +it('initializes correctly', function() { + expect($this->dashboardContainerWidget->canManage)->toEqual(true) + ->and($this->dashboardContainerWidget->canSetDefault)->toEqual(false) + ->and($this->dashboardContainerWidget->dateRangeFormat)->toEqual('MMMM D, YYYY hh:mm A') + ->and($this->dashboardContainerWidget->startDate)->toEqual(null) + ->and($this->dashboardContainerWidget->endDate)->toEqual(null); +}); it('loads assets correctly', function() { Assets::shouldReceive('addJs')->once()->with('js/vendor.datetime.js', 'vendor-datetime-js'); @@ -47,16 +54,12 @@ }); it('renders correctly', function() { - app()->instance('view', $viewMock = $this->createMock(Factory::class)); - - $viewMock->method('exists')->with($this->stringContains('dashboardcontainer/dashboardcontainer')); - expect($this->dashboardContainerWidget->render())->toBeString() ->and($this->dashboardContainerWidget->vars) ->toHaveKey('startDate') ->toHaveKey('endDate') ->toHaveKey('dateRangeFormat'); -})->throws(\Exception::class); +}); it('renders widgets without errors', function() { expect($this->dashboardContainerWidget->onRenderWidgets()) @@ -74,16 +77,37 @@ }); it('loads update popup', function() { - $widgetAlias = 'onboarding'; + $widgetAlias = 'test-dashboard-widget'; + resolve(Widgets::class)->registerDashboardWidget(TestDashboardWidget::class, [ + 'code' => $widgetAlias, + 'label' => 'Test Dashboard widget', + ]); request()->request->add(['widgetAlias' => $widgetAlias]); - expect($this->dashboardContainerWidget->onLoadUpdatePopup()) + $dashboardContainerWidget = new DashboardContainer($this->controller, $this->widgetConfig); + + expect($dashboardContainerWidget->onLoadUpdatePopup()) ->toHaveKey('#'.$widgetAlias.'-modal-content') - ->and($this->dashboardContainerWidget->vars) + ->and($dashboardContainerWidget->vars) ->toHaveKey('widget') ->toHaveKey('widgetAlias'); }); +it('loads update popup throws exception when missing widget alias', function() { + request()->request->add([]); + + expect(fn() => $this->dashboardContainerWidget->onLoadUpdatePopup()) + ->toThrow(FlashException::class, lang('igniter::admin.dashboard.alert_select_widget_to_update')); +}); + +it('loads update popup throws exception when missing widget', function() { + $widgetAlias = 'invalid'; + request()->request->add(['widgetAlias' => $widgetAlias]); + + expect(fn() => $this->dashboardContainerWidget->onLoadUpdatePopup()) + ->toThrow(FlashException::class, lang('igniter::admin.dashboard.alert_widget_not_found')); +}); + it('resets widgets', function() { expect($this->dashboardContainerWidget->onResetWidgets()) ->toBeArray() @@ -96,6 +120,13 @@ expect($this->dashboardContainerWidget->onSetAsDefault())->toBeNull(); }); +it('sets as default throws exception when canSetDefault is disabled', function() { + $this->dashboardContainerWidget->canSetDefault = false; + + expect(fn() => $this->dashboardContainerWidget->onSetAsDefault()) + ->toThrow(FlashException::class, lang('igniter::admin.alert_access_denied')); +}); + it('adds widget', function() { request()->request->add([ 'widget' => 'onboarding', diff --git a/tests/src/Admin/Widgets/FilterTest.php b/tests/src/Admin/Widgets/FilterTest.php index 24c5182e..036b6ca0 100644 --- a/tests/src/Admin/Widgets/FilterTest.php +++ b/tests/src/Admin/Widgets/FilterTest.php @@ -3,15 +3,18 @@ namespace Igniter\Tests\Admin\Widgets; use Igniter\Admin\Classes\FilterScope; +use Igniter\Admin\Models\Status; use Igniter\Admin\Widgets\Filter; use Igniter\Admin\Widgets\SearchBox; +use Igniter\Flame\Exception\SystemException; +use Igniter\Local\Facades\Location as LocationFacade; +use Igniter\Local\Models\Location; use Igniter\System\Facades\Assets; use Igniter\Tests\Fixtures\Controllers\TestController; -use Illuminate\View\Factory; beforeEach(function() { $this->controller = resolve(TestController::class); - $this->filterWidget = new Filter($this->controller, [ + $this->widgetConfig = [ 'context' => 'test-context', 'search' => [ 'prompt' => 'Search text', @@ -19,7 +22,7 @@ ], 'scopes' => [ 'status' => [ - 'label' => 'Status', + 'label' => 'Status array option', 'type' => 'select', 'mode' => 'radio', 'options' => [ @@ -27,8 +30,25 @@ 2 => 'Inactive', ], ], + 'status-model' => [ + 'label' => 'Status model option', + 'type' => 'select', + 'modelClass' => Status::class, + ], + 'status-dropdown' => [ + 'label' => 'Status callback option', + 'type' => 'select', + 'options' => [Status::class, 'getDropdownOptionsForOrder'], + ], + 'status-string-option' => [ + 'label' => 'Status string option', + 'type' => 'select', + 'modelClass' => Status::class, + 'options' => 'getDropdownOptionsForOrder', + ], ], - ]); + ]; + $this->filterWidget = new Filter($this->controller, $this->widgetConfig); }); it('loads assets correctly', function() { @@ -42,15 +62,8 @@ }); it('renders correctly', function() { - app()->instance('view', $viewMock = $this->createMock(Factory::class)); - - $viewMock->method('exists')->willReturnMap([ - [$this->stringContains('filter/filter'), true], - [$this->stringContains('searchbox/searchbox'), true], - ]); - expect($this->filterWidget->render())->toBeString(); -})->throws(\Exception::class); +}); it('prepares variables correctly', function() { $this->filterWidget->prepareVars(); @@ -66,7 +79,7 @@ ->toHaveKey('scopes'); }); -it('gets search widget', function() { +it('returns search widget', function() { $this->filterWidget->prepareVars(); $result = $this->filterWidget->getSearchWidget(); @@ -81,7 +94,7 @@ expect($this->filterWidget->renderScopeElement($scope))->toBeString(); }); -it('submits correctly', function() { +it('submits correctly and dispatches filter.submit event', function() { request()->request->add(['filter' => [ 'status' => 'value', ]]); @@ -96,6 +109,27 @@ ->and($this->filterWidget->getScopeValue('status'))->toEqual('value'); }); +it('submits correctly when scope type is selectlist', function($scopeType, $scopeValue) { + request()->request->add(['filter' => [ + 'status' => $scopeValue, + ]]); + + $this->widgetConfig['scopes']['status']['type'] = $scopeType; + $filterWidget = new Filter($this->controller, $this->widgetConfig); + + $filterWidget->onSubmit(); + + expect($filterWidget->getScopeValue('status'))->toEqual($scopeValue); +})->with([ + ['select', 'value'], + ['selectlist', ['value']], + ['checkbox', '1'], + ['switch', '1'], + ['date', '2021-01-01'], + ['daterange', ['2021-01-01', '2021-01-31']], + ['daterange', null], +]); + it('clears correctly', function() { request()->request->add(['filter' => [ 'status' => 'value', @@ -105,12 +139,17 @@ expect($this->filterWidget->getScopeValue('status'))->toEqual('value'); - $this->filterWidget->onClear(); + $this->filterWidget->bindEvent('filter.submit', function($params) { + return ['triggered']; + }); - expect($this->filterWidget->getScopeValue('status'))->toBeNull(); + $response = $this->filterWidget->onClear(); + + expect($this->filterWidget->getScopeValue('status'))->toBeNull() + ->and($response)->toEqual(['triggered']); }); -it('gets select options', function() { +it('returns select options', function() { $result = $this->filterWidget->getSelectOptions('status'); expect($result)->toBeArray() @@ -118,11 +157,112 @@ ->toHaveKey('active'); }); -it('gets scope name', function() { +it('throws exception when model is missing', function() { + $this->widgetConfig['scopes']['status-missing-model'] = [ + 'label' => 'Status missing model', + 'type' => 'select', + 'options' => 'getDropdownOptionsForOrder', + ]; + + $filterWidget = new Filter($this->controller, $this->widgetConfig); + + expect(fn() => $filterWidget->getSelectOptions('status-missing-model')) + ->toThrow(SystemException::class, sprintf(lang('igniter::admin.list.filter_missing_scope_model'), 'status-missing-model')); +}); + +it('throws exception when model method is missing', function() { + $this->widgetConfig['scopes']['status-missing-model-method'] = [ + 'label' => 'Status missing model method', + 'type' => 'select', + 'modelClass' => Status::class, + 'options' => 'getDropdownOptionsForInvalid', + ]; + + $filterWidget = new Filter($this->controller, $this->widgetConfig); + + expect(fn() => $filterWidget->getSelectOptions('status-missing-model-method')) + ->toThrow(SystemException::class, sprintf(lang('igniter::admin.list.filter_missing_definitions'), + Status::class, 'getDropdownOptionsForInvalid', 'status-missing-model-method', + )); +}); + +it('returns empty select options when option is invalid', function() { + $this->widgetConfig['scopes']['status-invalid-option'] = [ + 'label' => 'Status invalid option', + 'type' => 'select', + 'modelClass' => Status::class, + 'options' => true, + ]; + $filterWidget = new Filter($this->controller, $this->widgetConfig); + + $result = $filterWidget->getSelectOptions('status-invalid-option'); + + expect($result)->toBeArray() + ->toHaveKey('available') + ->toHaveKey('active') + ->and($result['available'])->toBeEmpty(); +}); + +it('returns select options from model method', function() { + $result = $this->filterWidget->getSelectOptions('status-string-option'); + + expect($result)->toBeArray() + ->toHaveKey('available') + ->toHaveKey('active') + ->and($result['available'])->not->toBeEmpty(); +}); + +it('returns select options from model', function() { + Status::factory()->count(2)->create(); + + $result = $this->filterWidget->getSelectOptions('status-model'); + + expect($result)->toBeArray() + ->toHaveKey('available') + ->toHaveKey('active') + ->and(count($result['available']))->toBeGreaterThanOrEqual(2); +}); + +it('returns select options from callback', function() { + $result = $this->filterWidget->getSelectOptions('status-dropdown'); + + expect($result)->toBeArray() + ->toHaveKey('available') + ->toHaveKey('active'); +}); + +it('filters scope definition based on permission, context and location aware', function() { + LocationFacade::setModel(Location::factory()->create()); + $this->widgetConfig['scopes']['restricted'] = [ + 'label' => 'Restricted scope', + 'type' => 'select', + 'permissions' => 'Admin.ManageOrders', + ]; + $this->widgetConfig['scopes']['context'] = [ + 'label' => 'Context scope', + 'type' => 'select', + 'context' => ['context'], + ]; + $this->widgetConfig['scopes']['location'] = [ + 'label' => 'Location aware scope', + 'type' => 'select', + 'locationAware' => true, + ]; + $this->widgetConfig['context'] = 'test-context'; + $filterWidget = new Filter($this->controller, $this->widgetConfig); + + $filterWidget->prepareVars(); + + expect($filterWidget->vars['scopes'])->not->toHaveKey('restricted'); +}); + +it('returns scope name', function() { $scope = new FilterScope('test', 'Test'); - $result = $this->filterWidget->getScopeName($scope); + $this->filterWidget->prepareVars(); - expect($result)->toEqual('filter[test]'); + expect($this->filterWidget->getScopeName('status'))->toEqual('filter[status]') + ->and($this->filterWidget->getScopeNameFrom('status'))->toEqual('name') + ->and($this->filterWidget->getScopeName($scope))->toEqual('filter[test]'); }); it('sets scope value', function() { @@ -132,7 +272,7 @@ expect($this->filterWidget->getSession('scope-test'))->toEqual('value'); }); -it('gets scope', function() { +it('returns scope', function() { $this->filterWidget->prepareVars(); $result = $this->filterWidget->getScope('status'); @@ -141,6 +281,38 @@ ->and($result->scopeName)->toEqual('status'); }); -it('gets context', function() { +it('throws exception when scope is missing', function() { + expect(fn() => $this->filterWidget->getScope('invalid-scope')) + ->toThrow(SystemException::class, sprintf(lang('igniter::admin.list.filter_missing_scope_definitions'), 'invalid-scope')); +}); + +it('returns context', function() { expect($this->filterWidget->getContext())->toEqual('test-context'); }); + +it('applies date scope to query', function($type, $value, $config, $expected) { + $this->widgetConfig['scopes']['status-'.$type] = array_merge([ + 'label' => 'Status', + 'type' => $type, + ], $config); + $filterWidget = new Filter($this->controller, $this->widgetConfig); + $filterWidget->prepareVars(); + $filterWidget->setScopeValue('status-'.$type, $value); + $query = Status::query(); + + $filterWidget->applyScopeToQuery('status-'.$type, $query); + + expect($query->toSql())->toContain($expected); +})->with([ + 'date type with conditions' => ['date', '2023-01-01', ['conditions' => 'status_for = :filtered'], 'where status_for = 2023-01-01'], + 'date type with model scope' => ['date', '2023-01-01', ['scope' => 'isForOrder'], 'where `status_for` = ?'], + 'daterange type with conditions' => ['daterange', ['2023-01-01', '2023-01-31'], [ + 'conditions' => 'created_at >= CAST(:filtered_start AS DATE) AND created_at <= CAST(:filtered_end AS DATE)', + ], 'where created_at >= CAST("2023-01-01" AS DATE) AND created_at <= CAST("2023-01-31" AS DATE)'], + 'daterange type with scope' => ['daterange', ['2023-01-01', '2023-01-31'], ['scope' => 'isForOrder'], 'where `status_for` = ?'], + 'select type with conditions' => ['select', 'value', ['conditions' => 'status_for = :filtered'], "where status_for = 'value'"], + 'select type with multiple conditions' => ['select', ['value'], ['conditions' => ['status_for = :filtered']], "where status_for = 'value'"], + 'select type with multiple values' => ['select', ['2023-01-01', '2023-01-31'], ['conditions' => 'status_for = :filtered'], "where status_for = '2023-01-01','2023-01-31'"], + 'select type with model scope' => ['select', 'value', ['scope' => 'isForOrder'], 'where `status_for` = ?'], + 'disabled select type' => ['select', 'value', ['disabled' => true], 'from `statuses`'], +]); diff --git a/tests/src/Admin/Widgets/FormTest.php b/tests/src/Admin/Widgets/FormTest.php index 91f31217..37ffa624 100644 --- a/tests/src/Admin/Widgets/FormTest.php +++ b/tests/src/Admin/Widgets/FormTest.php @@ -5,30 +5,69 @@ use Igniter\Admin\Classes\AdminController; use Igniter\Admin\Classes\BaseFormWidget; use Igniter\Admin\Classes\FormField; +use Igniter\Admin\FormWidgets\ColorPicker; use Igniter\Admin\Models\Status; use Igniter\Admin\Widgets\Form; +use Igniter\Flame\Exception\SystemException; +use Igniter\Local\Facades\Location as LocationFacade; +use Igniter\Local\Models\Location; use Igniter\System\Facades\Assets; -use Illuminate\View\Factory; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Event; beforeEach(function() { - $this->controller = new class extends AdminController {}; - $this->formWidget = new Form($this->controller, [ + $this->controller = new class extends AdminController + { + }; + $this->widgetConfig = [ 'toolbar' => [ 'prompt' => 'Search text', 'mode' => 'all', ], 'fields' => [ 'status_name' => [], - 'notify_customer' => [], - 'status_for' => [], - 'status_comment' => [], + 'test-context@status_name' => 'Status Name', + 'test-context@status_color' => 'colorpicker', + 'notify_customer' => ['span' => 'left'], + 'status_comment' => [ + 'span' => 'auto', + ], + 'status_for' => [ + 'permissions' => ['Admin.Statuses'], + ], + ], + 'tabs' => [ + 'fields' => [ + 'status_color' => [ + 'tab' => 'tab1', + 'span' => 'auto', + ], + 'status_history' => [ + 'tab' => 'tab2', + 'span' => 'auto', + ], + 'out-of-context' => [ + 'context' => ['another-context'], + ], + ], ], 'model' => Status::factory()->create(), - ]); - + 'context' => 'test-context', + ]; + $this->formWidget = new Form($this->controller, $this->widgetConfig); $this->formWidget->bindToController(); }); +it('throws exception when initializing with invalid model', function() { + $this->widgetConfig['model'] = ''; + + $this->expectException(SystemException::class); + $this->expectExceptionMessage(sprintf(lang('igniter::admin.form.missing_model'), $this->controller::class)); + + $formWidget = new Form($this->controller, $this->widgetConfig); + $formWidget->bindToController(); +}); + it('loads assets correctly', function() { Assets::shouldReceive('addJs')->once()->with('form.js', 'form-js'); Assets::shouldReceive('addJs')->once()->with('formwidget.js', 'formwidget-js'); @@ -38,26 +77,16 @@ $this->formWidget->loadAssets(); }); -it('renders correctly', function() { - app()->instance('view', $viewMock = $this->createMock(Factory::class)); - - $viewMock->method('exists')->with($this->stringContains('form/form')); +it('renders using section correctly', function() { + expect($this->formWidget->render(['section' => 'primary']))->toBeString(); +}); - expect($this->formWidget->render())->toBeString() - ->and($this->formWidget->vars)->toBeArray() - ->toHaveKey('filterAlias') - ->toHaveKey('filterId') - ->toHaveKey('onSubmitHandler') - ->toHaveKey('onClearHandler') - ->toHaveKey('cssClasses') - ->toHaveKey('search') - ->toHaveKey('scopes'); -})->throws(\Exception::class); +it('renders correctly', function() { + expect($this->formWidget->render())->toBeString(); +}); it('renders field correctly', function() { - $renderedField = $this->formWidget->renderField('status_name'); - - expect($renderedField)->toBeString(); + expect($this->formWidget->renderField('status_name'))->toBeString(); }); it('renders field element correctly', function() { @@ -66,12 +95,87 @@ expect($this->formWidget->renderFieldElement($field))->toBeString(); }); -it('sets active tab', function() { - request()->request->add(['tab' => 'testTab']); +it('throws exception when rendering missing field', function() { + expect(fn() => $this->formWidget->renderField('missingField')) + ->toThrow(new SystemException(sprintf(lang('igniter::admin.form.missing_definition'), 'missingField'))); +}); + +it('reloads form correctly', function() { + Event::fake([ + 'admin.form.beforeRefresh', + 'admin.form.refreshFields', + 'admin.form.refresh', + ]); + + request()->request->add([ + 'status_name' => 'Test Value', + ]); + + expect($this->formWidget->onRefresh())->toBeArray(); + + Event::assertDispatched('admin.form.beforeRefresh'); + Event::assertDispatched('admin.form.refreshFields'); + Event::assertDispatched('admin.form.refresh'); +}); + +it('reloads form with filtered fields correctly', function() { + request()->request->add([ + 'status_name' => 'Test Value', + ]); + + $this->widgetConfig['tabs'] = []; + $this->widgetConfig['fields'] = [ + 'status_name' => [], + 'status_color' => [], + ]; + $this->widgetConfig['model'] = new class extends Status + { + public function filterFields($form, &$allFields, $context) + { + unset($allFields['status_color']); + } + }; + $formWidget = new Form($this->controller, $this->widgetConfig); + $formWidget->bindToController(); + + expect($formWidget->render())->toBeString() + ->and($formWidget->getField('status_color'))->toBeNull(); +}); + +it('reloads form with additional fields correctly', function() { + Event::fake([ + 'admin.form.beforeRefresh', + 'admin.form.refreshFields', + ]); + + request()->request->add([ + 'status_name' => 'Test Value', + 'fields' => ['status_color', 'invalid_field'], + ]); + $this->formWidget->data = ['status_color' => 'Test Color']; + Event::listen('admin.form.refresh', function($formWidget) { + return ['#id-element' => 'Test content']; + }); + + expect($this->formWidget->onRefresh())->toBeArray() + ->toHaveKey('#id-element') + ->toHaveKey('#form-field-status-color-group'); + + Event::assertDispatched('admin.form.beforeRefresh'); + Event::assertDispatched('admin.form.refreshFields'); +}); + +it('sets and returns active tab', function() { + request()->request->add(['tab' => 'tab1']); $this->formWidget->onActiveTab(); - expect($this->formWidget->getSession('activeTab'))->toBe('testTab'); + expect($this->formWidget->getSession('activeTab'))->toBe('tab1'); + + $this->formWidget->setActiveTab('tab2'); + + expect($this->formWidget->activeTab)->toBe('tab2') + ->and($this->formWidget->getCookieKey())->toEndWith('test-context'); }); it('adds field correctly', function() { @@ -91,21 +195,23 @@ }); it('removes field', function() { - expect($this->formWidget->getField('status_name'))->toBeInstanceOf(FormField::class); - $this->formWidget->removeField('status_name'); - expect($this->formWidget->getField('status_name'))->toBeNull(); + expect($this->formWidget->getField('status_name'))->toBeNull() + ->and($this->formWidget->removeField('invalid-field'))->toBeFalse(); }); it('removes tab', function() { $fields = ['testField' => ['label' => 'Test Field', 'tab' => 'Test Tab']]; $this->formWidget->addTabFields($fields); - expect($this->formWidget->getField('testField'))->toBeInstanceOf(FormField::class); + $this->formWidget->removeTab('tab1'); $this->formWidget->removeTab('Test Tab'); - expect($this->formWidget->getField('testField'))->toBeNull(); + expect($this->formWidget->getField('testField'))->toBeNull() + ->and($this->formWidget->getField('status_color'))->toBeNull() + ->and($this->formWidget->getTab('invalid-tab'))->toBeNull() + ->and($this->formWidget->getTabs())->toBeArray(); }); it('makes form field', function() { @@ -118,16 +224,148 @@ ->and($field->idPrefix)->toBe($this->formWidget->getId()); }); +it('make form field with callable options', function() { + $this->widgetConfig['model'] = new class extends Model + { + public function getTestFieldOptions() + { + return ['option1' => 'Option 1', 'option2' => 'Option 2']; + } + }; + $formWidget = new Form($this->controller, $this->widgetConfig); + $formWidget->bindToController(); + + $field = $formWidget->makeFormField('testField', [ + 'label' => 'Test Field', + 'type' => 'select', + ]); + + expect($field)->toBeInstanceOf(FormField::class) + ->and($field->options())->not()->toBeEmpty(); +}); + +it('make form field with model getDropdownOptions method', function() { + $this->widgetConfig['model'] = new class extends Model + { + public function getDropdownOptions() + { + return ['option1' => 'Option 1', 'option2' => 'Option 2']; + } + }; + $formWidget = new Form($this->controller, $this->widgetConfig); + $formWidget->bindToController(); + + $field = $formWidget->makeFormField('testField', [ + 'label' => 'Test Field', + 'type' => 'select', + ]); + + expect($field)->toBeInstanceOf(FormField::class) + ->and($field->options())->not()->toBeEmpty(); +}); + +it('throws exception when options model method does not exists', function() { + $field = $this->formWidget->makeFormField('testField', [ + 'label' => 'Test Field', + 'type' => 'select', + ]); + + expect(fn() => $field->options())->toThrow(SystemException::class, sprintf(lang('igniter::admin.form.options_method_not_exists'), + Status::class, 'getTestFieldOptions', 'testField', + )); +}); + +it('throws exception when defined options model method does not exists', function() { + $field = $this->formWidget->makeFormField('testField', [ + 'label' => 'Test Field', + 'type' => 'select', + 'options' => 'invalidOptionsMethod', + ]); + + expect(fn() => $field->options())->toThrow(SystemException::class, sprintf(lang('igniter::admin.form.options_method_not_exists'), + Status::class, 'invalidOptionsMethod', 'testField', + )); +}); + +it('throws exception when making field with invalid type', function() { + $this->widgetConfig['fields']['invalid-type'] = ['type' => ['invalid']]; + + $this->expectException(SystemException::class); + $this->expectExceptionMessage(sprintf(lang('igniter::admin.form.field_invalid_type'), 'array')); + + $formWidget = new Form($this->controller, $this->widgetConfig); + $formWidget->bindToController(); + $formWidget->makeFormField('invalid', ['type' => 'invalid']); +}); + +it('makes form field with class name type', function() { + expect($this->formWidget->makeFormField('testField', ['type' => ColorPicker::class])) + ->toBeInstanceOf(FormField::class); +}); + +it('makes form field without form widget class as parent', function() { + expect($this->formWidget->makeFormField('testField', ['type' => 'Exception'])) + ->toBeInstanceOf(FormField::class); +}); + it('make form field widget', function() { $field = new FormField('testField', 'Test Field'); $field->displayAs('widget', ['widget' => 'colorpicker']); + $this->formWidget->makeFormFieldWidget($field); + + expect($this->formWidget->getFormWidget('testField'))->toBeInstanceOf(BaseFormWidget::class) + ->and($this->formWidget->getFormWidgets())->toBeArray(); +}); + +it('make form field widget with callable options', function() { + $field = new FormField('testField', 'Test Field'); + $field->displayAs('widget', [ + 'widget' => 'colorpicker', + 'options' => [Status::class, 'getDropdownOptionsForOrder'], + ]); + $widget = $this->formWidget->makeFormFieldWidget($field); - expect($widget)->toBeInstanceOf(BaseFormWidget::class); + expect($widget)->toBeInstanceOf(BaseFormWidget::class) + ->and($field->options()->isNotEmpty())->toBeTrue(); }); -it('gets field name', function($name, $context) { +it('throws exception when field widget options model method does not exists', function() { + $field = new FormField('testField', 'Test Field'); + $field->displayAs('widget', [ + 'widget' => 'colorpicker', + 'options' => 'invalidMethod', + ]); + + $this->formWidget->makeFormFieldWidget($field); + + expect(fn() => $field->options()) + ->toThrow(SystemException::class, sprintf(lang('igniter::admin.form.options_method_not_exists'), + Status::class, 'invalidMethod', 'testField', + )); +}); + +it('returns null when making field widget with invalid type', function() { + $field = new FormField('testField', 'Test Field'); + $field->displayAs('invalid', ['widget' => 'colorpicker']); + + $widget = $this->formWidget->makeFormFieldWidget($field); + + expect($widget)->toBeNull(); +}); + +it('throws exception when field widget class does not exists', function() { + $field = new FormField('testField', 'Test Field'); + $field->displayAs('widget', ['widget' => 'invalidClass']); + + $this->expectException(SystemException::class); + $this->expectExceptionMessage(sprintf(lang('igniter::admin.alert_widget_class_name'), 'invalidClass')); + + $this->formWidget->makeFormFieldWidget($field); +}); + +it('returns field name', function($name, $context) { [$fieldName, $fieldContext] = $this->formWidget->getFieldName($context ? $name.'@'.$context : $name); expect($fieldName)->toBe($name) @@ -137,15 +375,23 @@ ['testField', 'context'], ]); -it('gets field value', function() { +it('returns field value', function() { request()->request->add(['status_name' => 'Test Value']); - $value = $this->formWidget->getFieldValue('status_name'); + expect($this->formWidget->getFieldValue('status_name'))->toBe('Test Value'); +}); - expect($value)->toBe('Test Value'); +it('getFieldValue throws exception when field does not exist', function() { + request()->request->add(['status_name' => 'Test Value']); + + expect(fn() => $this->formWidget->getFieldValue('invalid-field')) + ->toThrow(SystemException::class, lang( + 'igniter::admin.form.missing_definition', + ['field' => 'invalid-field'], + )); }); -it('gets field depends', function() { +it('returns field depends', function() { $field = new FormField('testField', 'Test Field'); $field->dependsOn = ['otherField']; @@ -156,17 +402,78 @@ it('shows field labels', function() { $showLabels = $this->formWidget->showFieldLabels( - $this->formWidget->getField('status_name') + $this->formWidget->getField('status_name'), ); expect($showLabels)->toBeTrue(); }); -it('gets save data', function() { - request()->request->add(['status_name' => 'Test Value']); +it('does not show field labels for section type', function() { + $field = new FormField('testField', 'Test Field'); + $field->displayAs('section', ['widget' => 'colorpicker']); - $saveData = $this->formWidget->getSaveData(); + expect($this->formWidget->showFieldLabels($field))->toBeFalse(); +}); + +it('shows field labels for widget type', function() { + $field = new FormField('testField', 'Test Field'); + $field->displayAs('widget', ['widget' => 'colorpicker']); + + $showLabels = $this->formWidget->showFieldLabels($field); + + expect($showLabels)->toBeTrue(); +}); + +it('returns save data', function() { + request()->request->add([ + 'status_name' => 'Test Value', + 'colors' => ['status_color' => '#000000'], + ]); + $this->widgetConfig['fields']['nested[field]'] = []; + $this->widgetConfig['fields']['colors[status_color]'] = []; + $this->widgetConfig['fields']['widget'] = ['type' => 'colorpicker']; + + $formWidget = new Form($this->controller, $this->widgetConfig); + $formWidget->bindToController(); + $saveData = $formWidget->getSaveData(); expect($saveData)->toBeArray()->toHaveKey('status_name') ->and($saveData['status_name'])->toBe('Test Value'); }); + +it('skips disabled, hidden and private field from save data', function() { + $this->widgetConfig['fields'] = [ + 'status_name' => [], + 'status_for' => ['type' => 'checkboxtoggle'], + 'disabledTestField' => ['disabled' => true], + 'hiddenTestField' => ['hidden' => true], + 'disabledTestFieldWidget' => ['disabled' => true, 'type' => 'colorpicker'], + '_privateTestField' => [], + ]; + $formWidget = new Form($this->controller, $this->widgetConfig); + $formWidget->bindToController(); + request()->request->add([ + 'status_name' => 'Test Value', + 'status_for' => null, + ]); + + $saveData = $formWidget->getSaveData(); + + expect($saveData)->toBeArray()->toHaveKey('status_name', 'Test Value') + ->and($saveData)->toBeArray()->toHaveKey('status_for', null) + ->and($saveData)->toBeArray()->not()->toHaveKey('disabledTestField') + ->and($saveData)->toBeArray()->not()->toHaveKey('hiddenTestField') + ->and($saveData)->toBeArray()->not()->toHaveKey('_privateTestField'); +}); + +it('disables location aware form field', function() { + LocationFacade::setCurrent(Location::factory()->create()); + $this->widgetConfig['fields'] = [ + 'status_name' => ['locationAware' => true], + ]; + $this->widgetConfig['tabs'] = []; + $formWidget = new Form($this->controller, $this->widgetConfig); + $formWidget->bindToController(); + + expect($formWidget->getField('status_name')->disabled)->toBeTrue(); +}); diff --git a/tests/src/Admin/Widgets/ListsTest.php b/tests/src/Admin/Widgets/ListsTest.php index c55b8adf..4cfb209c 100644 --- a/tests/src/Admin/Widgets/ListsTest.php +++ b/tests/src/Admin/Widgets/ListsTest.php @@ -5,19 +5,26 @@ use Igniter\Admin\Classes\AdminController; use Igniter\Admin\Classes\ListColumn; use Igniter\Admin\Models\Status; +use Igniter\Admin\Models\StatusHistory; use Igniter\Admin\Widgets\Lists; +use Igniter\Flame\Exception\FlashException; +use Igniter\Flame\Exception\SystemException; +use Igniter\Local\Facades\Location as LocationFacade; +use Igniter\Local\Models\Location; use Igniter\System\Facades\Assets; -use Illuminate\View\Factory; +use Igniter\System\Models\Currency; +use Igniter\User\Models\User; +use InvalidArgumentException; beforeEach(function() { $this->controller = new class extends AdminController { - public function refreshList($alias) + public function refreshList($alias = null) { return [$alias => 'refreshed']; } }; - $this->listsWidget = new Lists($this->controller, [ + $this->widgetConfig = [ 'title' => 'Statuses', 'model' => new Status, 'toolbar' => [ @@ -33,6 +40,20 @@ public function refreshList($alias) 'delete' => [ 'label' => 'Delete', ], + 'status' => [ + 'label' => 'Status', + 'type' => 'dropdown', + 'menuItems' => [ + 'active' => [ + 'label' => 'Active', + 'icon' => 'fa fa-check', + ], + 'inactive' => [ + 'label' => 'Inactive', + 'icon' => 'fa fa-times', + ], + ], + ], ], 'columns' => [ 'status_id' => [ @@ -40,9 +61,10 @@ public function refreshList($alias) 'type' => 'number', 'searchable' => true, ], - 'status_name' => [ + 'name' => [ 'label' => 'Name', 'type' => 'text', + 'select' => 'status_name', 'searchable' => true, ], 'status_for' => [ @@ -51,12 +73,14 @@ public function refreshList($alias) 'searchable' => true, ], 'created_at' => [ - 'label' => 'Comment', - 'type' => 'textarea', + 'label' => 'Created At', + 'type' => 'datetime', 'invisible' => true, ], ], - ]); + ]; + $this->listsWidget = new Lists($this->controller, $this->widgetConfig); + $this->listsWidget->bindToController(); }); it('loads assets correctly', function() { @@ -68,12 +92,13 @@ public function refreshList($alias) }); it('renders correctly', function() { - app()->instance('view', $viewMock = $this->createMock(Factory::class)); - - $viewMock->method('exists')->with($this->stringContains('lists/list')); + $this->listsWidget->columns = array_map(function($column) { + $column['sortable'] = false; + return $column; + }, $this->listsWidget->columns); expect($this->listsWidget->render())->toBeString(); -})->throws(\Exception::class); +}); it('prepares var correctly', function() { $this->listsWidget->prepareVars(); @@ -95,22 +120,164 @@ public function refreshList($alias) ->toHaveKey('sortDirection'); }); -it('gets columns', function() { +it('refreshes list widget on paginate', function() { + $this->listsWidget->putSession('sort', ['status_id', 'desc']); + expect($this->listsWidget->onPaginate())->toBeArray(); +}); + +it('throws exception when initializing with missing model', function() { + $this->widgetConfig['model'] = null; + + expect(fn() => new Lists($this->controller, $this->widgetConfig)) + ->toThrow(SystemException::class, sprintf(lang('igniter::admin.list.missing_model'), $this->controller::class)); +}); + +it('applies search term to list results', function() { + $listsWidget = new Lists($this->controller, $this->widgetConfig); + $listsWidget->bindToController(); + $listsWidget->setSearchTerm('Test'); + + $listsWidget->prepareVars(); + + expect($listsWidget->vars['records'])->toHaveCount(0); +}); + +it('applies search term on related column', function() { + $this->widgetConfig['showDragHandle'] = true; + $this->widgetConfig['model'] = new Currency; + $this->widgetConfig['columns'] = [ + 'related' => [ + 'label' => 'Related', + 'type' => 'text', + 'relation' => 'country', + 'select' => 'country_name', + 'searchable' => true, + ], + 'country_id' => [ + 'label' => 'Related', + 'type' => 'text', + 'relation' => 'country', + 'searchable' => true, + ], + ]; + $listsWidget = new Lists($this->controller, $this->widgetConfig); + $listsWidget->setSearchOptions(['scope' => 'applySwitchable']); + $listsWidget->setSearchTerm('Test'); + + $listsWidget->prepareVars(); + + expect($listsWidget->vars['records']->isNotEmpty())->toBeTrue(); +}); + +it('throws an exception when applying custom select query on morphTo relation', function() { + $this->widgetConfig['model'] = new StatusHistory; + $this->widgetConfig['columns'] = [ + 'order_id' => [ + 'label' => 'Related', + 'type' => 'text', + 'relation' => 'object', + 'select' => 'name', + 'searchable' => true, + ], + ]; + $listsWidget = new Lists($this->controller, $this->widgetConfig); + $listsWidget->setSearchTerm('Test'); + + $this->expectException(SystemException::class); + $this->expectExceptionMessage(sprintf(lang('igniter::admin.list.alert_relationship_not_supported'), 'morphTo')); + + $listsWidget->prepareVars(); +}); + +it('extends list query using events', function() { + $this->widgetConfig['model'] = new Status; + $this->widgetConfig['columns'] = [ + 'status_name' => [ + 'label' => 'Related', + 'type' => 'text', + 'searchable' => true, + ], + ]; + $listsWidget = new Lists($this->controller, $this->widgetConfig); + $listsWidget->bindEvent('list.extendQuery', function($query) { + return $query->where('status_id', 1); + }); + + $listsWidget->prepareVars(); + + expect($listsWidget->vars['records'])->toHaveCount(1); +}); + +it('extends list records using events', function() { + $this->listsWidget->showPagination = false; + $this->listsWidget->bindEvent('list.extendRecords', function($records) { + return $records; + }); + + expect($this->listsWidget->render())->toBeString(); +}); + +it('returns all defined columns', function() { expect($this->listsWidget->getColumns())->toBeArray(); }); -it('gets column', function() { +it('throws exception when missing defined columns', function() { + $this->widgetConfig['columns'] = []; + + $this->expectException(SystemException::class); + $this->expectExceptionMessage(sprintf(lang('igniter::admin.list.missing_column'), $this->controller::class)); + + $listsWidget = new Lists($this->controller, $this->widgetConfig); + $listsWidget->prepareVars(); +}); + +it('filter list columns using model filterColumns method', function() { + $this->widgetConfig['model'] = new class extends Status + { + public function filterColumns(&$listColumn) + { + unset($listColumn['status_id']); + } + }; + $listsWidget = new Lists($this->controller, $this->widgetConfig); + $listsWidget->prepareVars(); + + expect($listsWidget->vars['columns'])->toHaveCount(2); +}); + +it('makes list column with custom name', function() { + $listColumn = $this->listsWidget->makeListColumn('pivot[testColumn]', 'Test Column'); + + expect($listColumn)->toBeInstanceOf(ListColumn::class) + ->and($listColumn->valueFrom)->toBe('testColumn') + ->and($listColumn->relation)->toBe('pivot'); +}); + +it('returns column', function() { $this->listsWidget->prepareVars(); - $column = $this->listsWidget->getColumn('status_name'); + $column = $this->listsWidget->getColumn('status_id'); expect($column)->toBeInstanceOf(ListColumn::class); }); -it('gets visible column', function() { +it('returns visible column', function() { + LocationFacade::setModel(Location::factory()->create()); + $this->listsWidget->columns['notify_customer'] = 'Notify Customer'; + $this->listsWidget->columns['status_color'] = [ + 'label' => 'Status Color', + 'type' => 'text', + 'permissions' => ['Admin.Statuses'], + ]; + $this->listsWidget->columns['status_comment'] = [ + 'label' => 'Status Color', + 'type' => 'text', + 'locationAware' => true, + ]; $visibleColumns = $this->listsWidget->getVisibleColumns(); - expect($visibleColumns)->toBeArray()->not->toHaveKey('created_at'); + expect($visibleColumns)->toBeArray() + ->not->toHaveKeys(['created_at', 'status_color', 'status_comment']); }); it('adds column', function() { @@ -133,7 +300,7 @@ public function refreshList($alias) expect($this->listsWidget->getColumns())->not->toHaveKey('testColumn'); }); -it('gets button attributes', function() { +it('returns button attributes', function() { $listColumn = new ListColumn('testColumn', 'Test Column'); $listColumn->displayAs('text', ['attributes' => ['class' => 'btn btn-primary', 'href' => 'model/edit']]); @@ -145,7 +312,20 @@ public function refreshList($alias) ->toBe(' class="btn btn-primary" href="http://localhost/admin/model/edit"'); }); -it('gets text column value', function($columnName, $type, $value, $expected, $config) { +it('overrides list header value using event', function() { + $this->listsWidget->bindEvent('list.overrideHeaderValue', function($column, $value) { + return 'Overridden Value'; + }); + + $listColumn = new ListColumn('status_name', 'Test Column'); + $listColumn->displayAs('text', []); + + $columnValue = $this->listsWidget->getHeaderValue($listColumn); + + expect($columnValue)->toBe('Overridden Value'); +}); + +it('returns list column value', function($columnName, $type, $value, $expected, $config) { $listColumn = new ListColumn($columnName, 'Test Column'); $listColumn->displayAs($type, $config); @@ -157,16 +337,134 @@ public function refreshList($alias) expect($columnValue)->toBe($expected); })->with([ + ['status_comment', 'text', null, 'Test Value', ['default' => 'Test Value']], ['status_name', 'text', 'Test Value', 'Test Value', []], - ['status_name', 'partial', 'This is a test partial view', 'This is a test partial view', ['path' => 'tests.admin::_partials.test-partial']], + ['status_name', 'partial', "This is a test partial content\n", "This is a test partial content\n", ['path' => 'tests.admin::_partials.test-partial']], ['status_name', 'money', 100.1234567, '100.12', []], ['status_name', 'switch', 1, 'Yes', ['onText' => 'Yes']], ['updated_at', 'datetime', '2022-12-31 23:59:59', '31 December 2022 23:59', ['format' => 'DD MMMM YYYY HH:mm']], + ['updated_at', 'datesince', '2022-12-31 23:59:59', '31 Dec 2022', []], + ['updated_at', 'timesince', '2022-12-31 23:59:59', '2 years ago', []], + ['updated_at', 'timetense', '2022-12-31 23:59:59', '31 Dec 2022 at 11:59 pm', []], ['updated_at', 'time', '23:59:59', '23:59', ['format' => 'HH:mm']], ['updated_at', 'date', '2022-12-31', '31 December 2022', ['format' => 'DD MMMM YYYY']], ['status_name', 'currency', 100, '£100.00', []], + ['updated_at', 'datetime', null, null, []], + ['updated_at', 'date', null, null, []], + ['updated_at', 'time', null, null, []], + ['updated_at', 'datesince', null, null, []], + ['updated_at', 'timesince', null, null, []], + ['updated_at', 'timetense', null, null, []], ]); +it('returns column value from formatter', function() { + $listColumn = new ListColumn('status_name', 'Test Column'); + $listColumn->formatter = function($value) { + return 'Formatted Value'; + }; + $record = Status::factory()->create([ + 'status_name' => 'Test Value', + ]); + + $columnValue = $this->listsWidget->getColumnValue($record, $listColumn); + + expect($columnValue)->toBe('Formatted Value'); +}); + +it('returns null list column value from unloaded model relation', function() { + $listColumn = new ListColumn('status_name', 'Test Column'); + $listColumn->displayAs('text', ['relation' => 'status_history']); + + $record = Status::factory()->create(); + + expect($this->listsWidget->getColumnValue($record, $listColumn))->toBeNull(); +}); + +it('returns list column value from model relation', function() { + $listColumn = new ListColumn('status_id', 'Test Column'); + $listColumn->displayAs('text', ['relation' => 'status_history']); + + $status = Status::factory()->create(); + $status->setRelation('status_history', StatusHistory::factory()->create([ + 'status_id' => 1, + ])); + + $columnValue = $this->listsWidget->getColumnValue($status, $listColumn); + + expect($columnValue)->toBe('1'); +}); + +it('returns list column value from model pivot relation', function() { + $this->listsWidget->model = new User(); + $listColumn = new ListColumn('location_name', 'Test Column'); + $listColumn->displayAs('text', ['relation' => 'pivot']); + + $user = User::factory()->create(); + $location = Location::factory()->create(); + $user->setRelation('pivot', $location); + $user->locations()->attach($location); + + $columnValue = $this->listsWidget->getColumnValue($user, $listColumn); + + expect($columnValue)->toBe($location->location_name); +}); + +it('overrides list column value using event', function() { + $this->listsWidget->bindEvent('list.overrideColumnValue', function($column, $record, $value) { + return 'Overridden Value'; + }); + $listColumn = new ListColumn('status_name', 'Test Column'); + $listColumn->displayAs('text'); + + $record = Status::factory()->create(); + $columnValue = $this->listsWidget->getColumnValue($record, $listColumn); + + expect($columnValue)->toBe('Overridden Value'); +}); + +it('overrides button attributes using event', function() { + $this->listsWidget->bindEvent('list.overrideColumnValue', function($column, $record, $attributes) { + return [ + 'title' => 'Overridden Title', + 'url' => 'model/edit', + ]; + }); + $listColumn = new ListColumn('status_name', 'Test Column'); + $listColumn->displayAs('text', ['attributes' => ['class' => 'btn btn-primary', 'href' => 'model/edit']]); + + $record = Status::factory()->create(); + $buttonAttributes = $this->listsWidget->getButtonAttributes($record, $listColumn); + + expect($buttonAttributes)->toBe(' title="Overridden Title" href="model/edit"'); +}); + +it('throws exception with datetime value is invalid', function() { + $listColumn = new ListColumn('status_name', 'Test Column'); + $listColumn->displayAs('datetime'); + $record = Status::factory()->create([ + 'status_name' => 'Invalid Date', + ]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid date value supplied to DateTime helper.'); + + $this->listsWidget->getColumnValue($record, $listColumn); +}); + +it('throws exception when model does not have relation', function() { + $listColumn = new ListColumn('status_id', 'Test Column'); + $listColumn->displayAs('text', ['relation' => 'invalid_relation']); + $status = Status::factory()->create(); + $status->setRelation('invalid_relation', StatusHistory::factory()->create([ + 'status_id' => 1, + ])); + + $this->expectException(SystemException::class); + $this->expectExceptionMessage(sprintf(lang('igniter::admin.alert_missing_model_definition'), Status::class, 'invalid_relation')); + + $this->listsWidget->getColumnValue($status, $listColumn); +}); + it('adds filter', function() { $calledFilter = false; @@ -180,6 +478,10 @@ public function refreshList($alias) }); it('handles onSort action', function() { + $this->listsWidget->defaultSort = 'status_id desc'; + $this->listsWidget->columns['status_id']['sortable'] = true; + expect($this->listsWidget->onSort())->toBeArray(); + request()->query->add(['sort_by' => 'status_id']); $this->listsWidget->prepareVars(); @@ -196,6 +498,7 @@ public function refreshList($alias) }); it('handles onLoadSetup action', function() { + $this->listsWidget->pageLimit = 200; $loadSetupResult = $this->listsWidget->onLoadSetup(); expect($loadSetupResult) @@ -205,7 +508,7 @@ public function refreshList($alias) it('handles onApplySetup action', closure: function() { request()->request->add([ - 'visible_columns' => $visibleColumns = ['status_id', 'status_name'], + 'visible_columns' => $visibleColumns = ['status_id', 'name'], 'page_limit' => $pageLimit = 20, ]); @@ -223,6 +526,19 @@ public function refreshList($alias) ->and($this->listsWidget->getSession('page_limit'))->toBe($pageLimit); }); +it('throws an exception when override column is defined', function() { + request()->request->add([ + 'visible_columns' => ['status_name'], + ]); + + $this->listsWidget->prepareVars(); + + $this->expectException(SystemException::class); + $this->expectExceptionMessage(sprintf(lang('igniter::admin.list.invalid_column_override'), 'status_name')); + + $this->listsWidget->onApplySetup(); +}); + it('handles onResetSetup action', function() { $this->listsWidget->putSession('visible', ['status_id', 'status_name']); $this->listsWidget->putSession('order', ['status_id', 'status_name']); @@ -237,18 +553,98 @@ public function refreshList($alias) }); it('handles onBulkAction action', function() { - request()->query->add(['code' => 'delete']); - request()->query->add(['checked' => [ - $status1 = Status::factory()->create()->getKey(), - $status2 = Status::factory()->create()->getKey(), - $status3 = Status::factory()->create()->getKey(), - ]]); + request()->query->add([ + 'code' => 'delete', + 'checked' => [ + $status1 = Status::factory()->create()->getKey(), + $status2 = Status::factory()->create()->getKey(), + $status3 = Status::factory()->create()->getKey(), + ], + ]); + + $this->listsWidget->prepareVars(); + + $this->listsWidget->onBulkAction(); + + expect($this->listsWidget->renderBulkActionButton('checked'))->toBe('checked'); + + $this->assertDatabaseMissing('statuses', ['status_id' => $status1]); + $this->assertDatabaseMissing('statuses', ['status_id' => $status2]); + $this->assertDatabaseMissing('statuses', ['status_id' => $status3]); +}); + +it('filters restricted and location aware bulk action buttons', function() { + LocationFacade::setModel(Location::factory()->create()); + $this->listsWidget->bulkActions = [ + 'delete' => [ + 'label' => 'Delete', + 'permissions' => ['Admin.Statuses'], + ], + 'status' => [ + 'label' => 'Status', + 'locationAware' => true, + ], + ]; + + $this->listsWidget->prepareVars(); + + expect($this->listsWidget->vars['bulkActions'])->toHaveCount(0); +}); + +it('throws exception when code is missing in request', function() { + $this->expectException(FlashException::class); + $this->expectExceptionMessage(lang('igniter::admin.list.missing_action_code')); + + $this->listsWidget->onBulkAction(); +}); + +it('throws exception when bulk action code is invalid', function() { + request()->query->add(['code' => 'invalid']); + + $this->expectException(FlashException::class); + $this->expectExceptionMessage(sprintf(lang('igniter::admin.list.action_not_found'), 'invalid')); + + $this->listsWidget->onBulkAction(); +}); + +it('throws exception when checked value is not an array', function() { + request()->query->add(['code' => 'delete', 'checked' => 'invalid']); + + $this->expectException(FlashException::class); + $this->expectExceptionMessage(lang('igniter::admin.list.delete_empty')); + + $this->listsWidget->onBulkAction(); +}); + +it('handles onBulkAction action deletes all records', function() { + request()->query->add([ + 'code' => 'delete', + 'select_all' => '1', + 'checked' => [ + $status1 = Status::factory()->create()->getKey(), + ], + ]); $this->listsWidget->prepareVars(); $this->listsWidget->onBulkAction(); - $this->assertDatabaseMissing('statuses', ['status_id' => $status1]) - ->assertDatabaseMissing('statuses', ['status_id' => $status2]) - ->assertDatabaseMissing('statuses', ['status_id' => $status3]); + $this->assertDatabaseMissing('statuses', ['status_id' => $status1]); +}); + +it('throws exception when bulk action widget class does not exists', function() { + $this->widgetConfig['bulkActions'] = [ + 'invalidAction' => [ + 'label' => 'Invalid Action', + 'type' => 'invalid', + ], + ]; + + $this->expectException(SystemException::class); + $this->expectExceptionMessage(sprintf(lang('igniter::admin.alert_widget_class_name'), 'invalidAction')); + + $listsWidget = new Lists($this->controller, $this->widgetConfig); + $listsWidget->bindToController(); + + $listsWidget->prepareVars(); }); diff --git a/tests/src/Admin/Widgets/MenuTest.php b/tests/src/Admin/Widgets/MenuTest.php index 570231c4..5ebcc0d4 100644 --- a/tests/src/Admin/Widgets/MenuTest.php +++ b/tests/src/Admin/Widgets/MenuTest.php @@ -3,21 +3,34 @@ namespace Igniter\Tests\Admin\Widgets; use Igniter\Admin\Classes\MainMenuItem; +use Igniter\Admin\Models\Status; use Igniter\Admin\Widgets\Menu; +use Igniter\Flame\Exception\FlashException; use Igniter\System\Facades\Assets; use Igniter\Tests\Fixtures\Controllers\TestController; use Igniter\User\Facades\AdminAuth; +use Igniter\User\MainMenuWidgets\UserPanel; use Igniter\User\Models\User; -use Illuminate\View\Factory; beforeEach(function() { $this->controller = resolve(TestController::class); + $this->controller->setUser(User::factory()->create()); $this->menuWidget = new Menu($this->controller, [ 'items' => [ 'item1' => [ 'path' => 'tests.admin::_partials.test-partial', ], - 'item2' => MainMenuItem::link('item2'), + 'item2' => [ + 'path' => 'tests.admin::_partials.test-partial', + 'type' => 'dropdown', + 'options' => [Status::class, 'getDropdownOptionsForOrder'], + ], + 'item3' => MainMenuItem::dropdown('item3'), + 'item4' => MainMenuItem::widget('user', UserPanel::class), + 'out-of-context' => [ + 'path' => 'tests.admin::_partials.test-partial', + 'context' => ['another-context'], + ], ], 'context' => 'test-context', ]); @@ -25,11 +38,8 @@ }); it('renders correctly', function() { - app()->instance('view', $viewMock = $this->createMock(Factory::class)); - $viewMock->method('exists')->with($this->stringContains('menu/top_menu')); - expect($this->menuWidget->render())->toBeString(); -})->throws(\Exception::class); +}); it('loads assets correctly', function() { Assets::shouldReceive('addJs')->once()->with('mainmenu.js', 'mainmenu-js'); @@ -43,12 +53,6 @@ it('renders item element', function() { $item = $this->menuWidget->getItem('item1'); - app()->instance('view', $viewMock = $this->createMock(Factory::class)); - - $viewMock->method('exists')->with($this->stringContains('menu/item_'.$item->type)); - - $this->expectException(\Exception::class); - expect($this->menuWidget->renderItemElement($item))->toBeString(); }); @@ -60,7 +64,7 @@ $this->menuWidget->addItems($items); - expect($this->menuWidget->getItems())->toHaveCount(4); + expect($this->menuWidget->getItems())->toHaveCount(5); }); it('gets logged user correctly', function() { @@ -75,11 +79,30 @@ }); it('handles onGetDropdownOptions method', function() { - request()->query->add(['item' => 'item1']); + request()->query->add(['item' => 'item2']); expect($this->menuWidget->onGetDropdownOptions())->toBeArray(); }); +it('onGetDropdownOptions throws exception when missing request data', function() { + $this->expectException(FlashException::class); + $this->expectExceptionMessage(lang('igniter::admin.side_menu.alert_invalid_menu')); + + $this->menuWidget->onGetDropdownOptions(); +}); + it('gets context', function() { expect($this->menuWidget->getContext())->toBe('test-context'); }); + +it('throws exception when retrieving invalid menu item', function() { + expect(fn() => $this->menuWidget->getItem('invalid-item')) + ->toThrow(sprintf(lang('igniter::admin.side_menu.alert_no_definition'), 'invalid-item')); +}); + +it('returns null when making widget with invalid type', function() { + $item = new MainMenuItem('item1', 'Item 1'); + $item->type = 'invalid'; + + expect($this->menuWidget->makeMenuItemWidget($item))->toBeNull(); +}); diff --git a/tests/src/Admin/Widgets/TableTest.php b/tests/src/Admin/Widgets/TableTest.php index cfb0215a..3ddbc4b2 100644 --- a/tests/src/Admin/Widgets/TableTest.php +++ b/tests/src/Admin/Widgets/TableTest.php @@ -3,36 +3,91 @@ namespace Igniter\Tests\Admin\Widgets; use Igniter\Admin\Classes\TableDataSource; +use Igniter\Admin\Models\Status; use Igniter\Admin\Widgets\Table; use Igniter\Flame\Exception\SystemException; use Igniter\System\Facades\Assets; use Igniter\Tests\Fixtures\Controllers\TestController; -use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\View\Factory; beforeEach(function() { $this->controller = resolve(TestController::class); $this->tableWidget = new Table($this->controller, [ - 'dataSource' => 'TestDataSource', 'columns' => [ 'column1' => [ - 'label' => 'Column 1', + 'title' => 'Column 1', 'type' => 'text', ], - 'column2' => [ + 'status_name' => [ 'label' => 'Column 2', 'type' => 'text', + 'partial' => 'test-partial', ], ], ]); }); -it('initialize method throws exception when dataSource is not specified', function() { - $this->tableWidget->setConfig(['dataSource' => null]); - +it('throws exception when intializing with invalid dataSource class', function() { $this->expectException(SystemException::class); + $this->expectExceptionMessage(sprintf(lang('igniter::admin.error_table_widget_data_class_not_found'), 'InvalidDataSourceClass')); + + $tableWidget = new class($this->controller, [ + 'columns' => [ + 'column1' => [ + 'title' => 'Column 1', + 'type' => 'text', + ], + ], + ]) extends Table + { + protected string $dataSourceAliases = 'InvalidDataSourceClass'; + }; + + + $tableWidget->initialize(); +}); + +it('initializes correctly with existing request data', function() { + $expected = [ + 'search' => 'search', + 'offset' => 'offset', + 'limit' => 'limit', + 'column1' => 'limit', + 'status_name' => 'limit', + ]; + request()->setMethod('POST'); + request()->request->add([ + 'tableTableData' => $expected, + ]); $this->tableWidget->initialize(); + + expect($this->tableWidget->getDataSource())->toBeInstanceOf(TableDataSource::class) + ->and($this->tableWidget->getDataSource()->getRecords(0, 10))->toBe($expected); +}); + +it('initializes correctly with existing nested request data', function() { + $expected = [ + 'search' => 'search', + 'offset' => 'offset', + 'limit' => 'limit', + 'column1' => 'limit', + 'status_name' => 'limit', + ]; + request()->setMethod('POST'); + request()->request->add([ + 'customer' => [ + 'table' => [ + 'TableData' => $expected, + ], + ], + ]); + + $this->tableWidget->alias = 'customer[table]'; + $this->tableWidget->initialize(); + + expect($this->tableWidget->getDataSource())->toBeInstanceOf(TableDataSource::class) + ->and($this->tableWidget->getDataSource()->getRecords(0, 10))->toBe($expected); }); it('getDataSource method returns TableDataSource instance', function() { @@ -92,7 +147,7 @@ ]); $this->tableWidget->bindEvent('table.getRecords', function() { - return new LengthAwarePaginator([], 0, 10, 1); + return Status::paginate(10); }); expect($this->tableWidget->onGetRecords()) @@ -107,13 +162,15 @@ 'rowData' => [], ]); - $eventFired = false; + $this->tableWidget->bindEvent('table.getDropdownOptions', function() { + return Status::getDropdownOptionsForOrder(); + }); - $this->tableWidget->bindEvent('table.getDropdownOptions', function() use (&$eventFired) { - $eventFired = true; + $this->tableWidget->bindEvent('table.getDropdownOptions', function() { + return Status::getDropdownOptionsForReservation(); }); - expect($this->tableWidget->onGetDropdownOptions()) - ->toBeArray()->toHaveKey('options') - ->and($eventFired)->toBeTrue(); + $options = $this->tableWidget->onGetDropdownOptions(); + expect($options)->toBeArray()->toHaveKey('options') + ->and($options['options'])->not->toBeEmpty(); }); diff --git a/tests/src/Admin/Widgets/ToolbarTest.php b/tests/src/Admin/Widgets/ToolbarTest.php index b4d4aecd..a83fc3b2 100644 --- a/tests/src/Admin/Widgets/ToolbarTest.php +++ b/tests/src/Admin/Widgets/ToolbarTest.php @@ -2,11 +2,10 @@ namespace Igniter\Tests\Admin\Widgets; -use Igniter\Admin\Classes\ToolbarButton; +use Igniter\Admin\Facades\Template; use Igniter\Admin\Widgets\Toolbar; use Igniter\Tests\Fixtures\Controllers\TestController; use Illuminate\Support\Facades\Event; -use Illuminate\View\Factory; beforeEach(function() { $this->controller = resolve(TestController::class); @@ -23,6 +22,30 @@ 'context' => ['save'], 'class' => 'btn btn-primary', ], + 'save-restricted' => [ + 'label' => 'Save', + 'context' => ['save'], + 'class' => 'btn btn-primary', + 'permission' => 'test.permission', + ], + 'dropdown' => [ + 'label' => 'Dropdown', + 'context' => ['save'], + 'class' => 'btn btn-primary', + 'type' => 'dropdown', + 'menuItems' => [ + 'save' => [ + 'label' => 'Save', + 'context' => ['save'], + 'class' => 'btn btn-primary', + ], + 'saveClose' => [ + 'label' => 'Save & Close', + 'context' => ['save'], + 'class' => 'btn btn-primary', + ], + ], + ], ], ]); }); @@ -44,16 +67,27 @@ }); it('renders without errors', function() { - app()->instance('view', $viewMock = $this->createMock(Factory::class)); - $viewMock->method('exists')->with($this->stringContains('toolbar/toolbar')); + expect($this->toolbarWidget->render())->toBeString(); +}); + +it('renders container without errors', function() { + $this->toolbarWidget->reInitialize([ + 'container' => 'test-partial', + ]); expect($this->toolbarWidget->render())->toBeString(); -})->throws(\Exception::class); +}); it('prepares variables without errors', function() { - Event::fake(); + Event::fake([ + 'admin.toolbar.extendButtonsBefore', + 'admin.toolbar.extendButtons', + ]); + + Template::setButton('test', ['class' => 'btn btn-default']); $this->toolbarWidget->prepareVars(); + $this->toolbarWidget->prepareVars(); // define buttons only once Event::assertDispatched('admin.toolbar.extendButtonsBefore'); Event::assertDispatched('admin.toolbar.extendButtons'); @@ -66,16 +100,11 @@ }); it('renders button markup without errors', function() { - $buttonObj = new ToolbarButton('test'); - $buttonObj->displayAs('button', []); - - app()->instance('view', $viewMock = $this->createMock(Factory::class)); - - $viewMock->method('exists')->with($this->stringContains('toolbar/button_'.$buttonObj->type)); - - $this->expectExceptionMessageMatches('/toolbar\/button_button/'); + $this->toolbarWidget->prepareVars(); + $buttonObj = $this->toolbarWidget->getButtonList(); - $this->toolbarWidget->renderButtonMarkup($buttonObj); + expect($this->toolbarWidget->renderButtonMarkup($buttonObj['save']))->toBeString() + ->and($this->toolbarWidget->renderButtonMarkup('test'))->toEqual('test'); }); it('gets context without errors', function() { @@ -92,7 +121,7 @@ ], ]); - expect($this->toolbarWidget->allButtons)->toHaveCount(3)->toHaveKey('test'); + expect($this->toolbarWidget->allButtons)->toHaveCount(4)->toHaveKey('test'); }); it('adds button without errors', function() { @@ -125,7 +154,7 @@ $buttons = $this->toolbarWidget->getButtonList(); - expect($buttons)->toHaveCount(2)->toHaveKey('save')->toHaveKey('saveClose'); + expect($buttons)->toHaveCount(3)->toHaveKey('save')->toHaveKey('saveClose'); }); it('gets active save action without errors', function() { diff --git a/tests/src/Fixtures/Controllers/FormExtendableTestController.php b/tests/src/Fixtures/Controllers/FormExtendableTestController.php new file mode 100644 index 00000000..b23a47fc --- /dev/null +++ b/tests/src/Fixtures/Controllers/FormExtendableTestController.php @@ -0,0 +1,18 @@ + 'Test Form', + 'model' => 'Igniter\Tests\Fixtures\Models\TestModel', + 'configFile' => 'test_form', + ]; + + public function index() {} +} diff --git a/tests/src/Fixtures/Controllers/ListExtendableTestController.php b/tests/src/Fixtures/Controllers/ListExtendableTestController.php new file mode 100644 index 00000000..eb38f570 --- /dev/null +++ b/tests/src/Fixtures/Controllers/ListExtendableTestController.php @@ -0,0 +1,20 @@ + [ + 'model' => Status::class, + 'configFile' => 'test_list', + ], + ]; + + public function index() {} +} diff --git a/tests/src/Fixtures/Controllers/ThemeTestController.php b/tests/src/Fixtures/Controllers/ThemeTestController.php new file mode 100644 index 00000000..2482aecb --- /dev/null +++ b/tests/src/Fixtures/Controllers/ThemeTestController.php @@ -0,0 +1,49 @@ + [ + 'label' => lang('igniter.main::default.theme_website_label'), + 'rules' => 'nullable|string', + ], + 'theme_background' => [ + 'label' => lang('igniter.main::default.theme_background_label'), + 'rules' => 'required|string', + ], + 'social' => [ + 'type' => 'repeater', + 'commentAbove' => 'Add full URL for your social network profiles', + 'form' => [ + 'fields' => [ + 'class' => [ + 'label' => 'Icon css class', + 'type' => 'text', + 'rules' => 'required', + 'default' => 'fab fa-facebook', + ], + ], + ], + ], + ]; + } + }; + } +} diff --git a/tests/src/Fixtures/Requests/TestRequest.php b/tests/src/Fixtures/Requests/TestRequest.php new file mode 100644 index 00000000..9d1c5f02 --- /dev/null +++ b/tests/src/Fixtures/Requests/TestRequest.php @@ -0,0 +1,22 @@ + 'required|string', + ]; + } + + public function attributes() + { + return [ + 'name' => 'full name', + ]; + } +} diff --git a/tests/src/Fixtures/Widgets/TestDashboardWidget.php b/tests/src/Fixtures/Widgets/TestDashboardWidget.php new file mode 100644 index 00000000..bb2d99df --- /dev/null +++ b/tests/src/Fixtures/Widgets/TestDashboardWidget.php @@ -0,0 +1,26 @@ + [ + 'label' => 'igniter::admin.dashboard.label_widget_title', + 'default' => 'igniter::admin.dashboard.text_news', + ], + 'newsCount' => [ + 'label' => 'igniter::admin.dashboard.text_news_count', + 'default' => 6, + 'type' => 'repeater', + 'validationRule' => 'required|integer', + ], + ]; + } +} diff --git a/tests/src/Fixtures/Widgets/TestFormWidget.php b/tests/src/Fixtures/Widgets/TestFormWidget.php new file mode 100644 index 00000000..126b4cda --- /dev/null +++ b/tests/src/Fixtures/Widgets/TestFormWidget.php @@ -0,0 +1,20 @@ +fillFromConfig([ + 'property', + ]); + } + + public function getFormField() + { + return $this->formField; + } +} diff --git a/tests/src/Fixtures/extension/composer.json b/tests/src/Fixtures/extension/composer.json new file mode 100644 index 00000000..7e29d703 --- /dev/null +++ b/tests/src/Fixtures/extension/composer.json @@ -0,0 +1,17 @@ +{ + "name": "test/extension", + "description": "description", + "require": { + }, + "autoload": { + "psr-4": { + "Igniter\\Tests\\Fixtures\\Extension\\src\\": "src/" + } + }, + "extra": { + "tastyigniter-extension": { + "code": "module", + "name": "Test Extension" + } + } +} diff --git a/tests/src/Fixtures/extension/src/Extension.php b/tests/src/Fixtures/extension/src/Extension.php new file mode 100644 index 00000000..46fba4b5 --- /dev/null +++ b/tests/src/Fixtures/extension/src/Extension.php @@ -0,0 +1,10 @@ +getPackageVersion('some-package'); + + expect($version)->toBeNull(); +}); + +it('loads package name correctly', function() { + $name = resolve(Manager::class)->getPackageName('some-package'); + expect($name)->toBeNull(); +}); + +it('lists installed packages correctly', function() { + $manager = resolve(Manager::class); + + $packages = $manager->listInstalledPackages(); + + expect($packages)->toBeCollection() + ->and($packages->isEmpty())->toBeFalse(); +}); + +it('formats extension manifest correctly', function() { + $manager = resolve(Manager::class); + + $manifest = $manager->getExtensionManifest('/path/to/extension'); + + expect($manifest)->toBeArray() + ->and($manifest['type'])->toBe('tastyigniter-extension'); +}); + +it('formats theme manifest correctly', function() { + $manager = resolve(Manager::class); + + $manifest = $manager->getThemeManifest('/path/to/theme'); + + expect($manifest)->toBeArray() + ->and($manifest['type'])->toBe('tastyigniter-theme'); +}); + +it('adds repository to composer.json repositories config', function() {})->skip(); + +it('removes repository config from composer.json repositories config', function() {})->skip(); + +it('checks composer.json repositories config has a hostname', function() {})->skip(); + +it('loads required repository and auth config', function() {})->skip(); + +it('requires core package', function() {})->skip(); + +it('requires package', function() {})->skip(); + +it('updates package', function() {})->skip(); + +it('removes package', function() {})->skip(); diff --git a/tests/src/Main/Classes/MainControllerTest.php b/tests/src/Main/Classes/MainControllerTest.php index e0f200a7..2f79c073 100644 --- a/tests/src/Main/Classes/MainControllerTest.php +++ b/tests/src/Main/Classes/MainControllerTest.php @@ -2,20 +2,312 @@ namespace Igniter\Tests\Main\Classes; -it('finds assets file in active theme directory', function() {})->skip(); +use Igniter\Flame\Exception\AjaxException; +use Igniter\Flame\Exception\FlashException; +use Igniter\Flame\Pagic\Router; +use Igniter\Main\Classes\MainController; +use Igniter\Main\Classes\Theme; +use Igniter\Main\Classes\ThemeManager; +use Igniter\Main\Template\Code\LayoutCode; +use Igniter\Main\Template\Code\PageCode; +use Igniter\Main\Template\Layout; +use Igniter\Main\Template\Page; +use Illuminate\Http\RedirectResponse; +use Illuminate\Routing\Route; +use Illuminate\Support\Facades\Event; -it('runs the requested page', function() {})->skip(); +it('throws exception when theme is not set', function() { + $themeManager = mock(ThemeManager::class); + app()->instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('getActiveTheme')->andReturn(null); -it('runs the requested page handler', function() {})->skip(); + $mainController = new MainController(); -it('renders the requested page contents', function() {})->skip(); + expect(fn() => $mainController->callAction('index', [])) + ->toThrow(FlashException::class, lang('igniter::main.not_found.active_theme')); +}); -it('renders a defined component', function() {})->skip(); +it('loads assets from parent theme', function() { + $theme = mock(Theme::class); + $theme->shouldReceive('hasParent')->andReturnTrue(); + $theme->shouldReceive('getParent')->andReturnSelf(); + $theme->shouldReceive('getAssetPath')->andReturn('path/to/style.css'); -it('renders a component partial contents', function() {})->skip(); + expect((new MainController($theme))->assetPath)->toContain('path/to/style.css'); +}); -it('renders a theme partial contents', function() {})->skip(); +it('returns response with correct status code in remap', function() { + preparePage(); -it('renders a theme content contents', function() {})->skip(); + $mainController = new MainController(); + $response = $mainController->remap('index', []); -it('rewrites page path to page url', function() {})->skip(); + expect($response->getStatusCode())->toBe(200) + ->and($mainController->getLayoutObj())->toBeInstanceOf(LayoutCode::class) + ->and($mainController->getPageObj())->toBeInstanceOf(PageCode::class) + ->and($mainController->getTheme())->toBeInstanceOf(Theme::class) + ->and($mainController->getRouter())->toBeInstanceOf(Router::class) + ->and($mainController->getPage())->toBeInstanceOf(Page::class) + ->and($mainController->getLayout())->toBeInstanceOf(Layout::class); +}); + +it('returns response using event in remap', function() { + preparePage(); + Event::listen('main.controller.beforeResponse', function($controller, $url, $page, $output) { + return 'test-path'; + }); + + expect((new MainController())->remap('index', []))->toContain('test-path'); +}); + +it('throws exception when page layout is not found in runPage', function() { + $page = mock(Page::class)->makePartial(); + $page->layout = 'nonexistent-layout'; + + expect(fn() => (new MainController())->runPage($page)) + ->toThrow(FlashException::class, sprintf( + lang('igniter::main.not_found.layout_name'), $page->layout, + )); +}); + +it('returns rendered page content in runPage', function() { + $page = mock(Page::class)->makePartial(); + + expect((new MainController())->runPage($page))->toBeString(); +}); + +it('returns rendered page content using page.init event in runPage', function() { + Event::listen('main.page.init', function($controller, $page) { + return 'test-page-content'; + }); + + $page = mock(Page::class)->makePartial(); + + expect((new MainController())->runPage($page))->toContain('test-page-content'); +}); + +it('returns rendered page content using page.beforeRenderPage event in runPage', function() { + Event::listen('main.page.beforeRenderPage', function($controller, $page) { + return 'test-page-content'; + }); + + $page = mock(Page::class)->makePartial(); + + expect((new MainController())->runPage($page))->toContain('test-page-content'); +}); + +it('returns response using page.start event in pageCycle', function() { + Event::listen('main.page.start', function($controller) { + return 'test-page-content'; + }); + + expect((new MainController())->pageCycle())->toContain('test-page-content'); +}); + +it('returns response using page.end event in pageCycle', function() { + Event::listen('main.page.end', function($controller) { + return 'test-page-content'; + }); + + $page = mock(Page::class)->makePartial(); + + expect((new MainController())->runPage($page))->toContain('test-page-content'); +}); + +it('returns response from layout component lifecycle method', function() { + $page = Page::resolveRouteBinding('layout-with-lifecycle'); + + expect((new MainController())->runPage($page))->toBeInstanceOf(RedirectResponse::class); +}); + +it('returns response from page component lifecycle method', function() { + $page = Page::resolveRouteBinding('page-with-lifecycle'); + + expect((new MainController())->runPage($page))->toBeInstanceOf(RedirectResponse::class); +}); + +it('returns response with correct status code when in ajax handlers', function() { + preparePage(); + request()->headers->set('X-Requested-With', 'XMLHttpRequest'); + request()->headers->set('X-IGNITER-REQUEST-HANDLER', 'testComponent::onAjaxHandler'); + + $response = (new MainController())->remap('components', []); + + expect($response->getStatusCode())->toBe(200) + ->and($response->getContent())->toContain('handler-result'); +}); + +it('returns response with rendered partials when in ajax handlers', function() { + preparePage(); + request()->headers->set('X-Requested-With', 'XMLHttpRequest'); + request()->headers->set('X-IGNITER-REQUEST-HANDLER', 'onAjaxHandlerWithStringResponse'); + request()->headers->set('X-IGNITER-REQUEST-PARTIALS', 'test-partial'); + + $response = (new MainController())->remap('components', []); + + expect($response->getStatusCode())->toBe(200) + ->and($response->getContent())->toContain('handler-result')->toContain('This is a test partial content'); +}); + +it('returns json response when in ajax handlers', function() { + preparePage(); + request()->headers->set('X-Requested-With', 'XMLHttpRequest'); + request()->headers->set('X-IGNITER-REQUEST-HANDLER', 'testComponent::onAjaxHandlerWithObjectResponse'); + + $response = (new MainController())->remap('components', []); + + expect($response->getStatusCode())->toBe(200) + ->and(json_decode($response->getContent()))->toHaveKey('json', 'handler-result'); +}); + +it('returns redirect response when in ajax handlers', function() { + preparePage(); + request()->headers->set('X-Requested-With', 'XMLHttpRequest'); + request()->headers->set('X-IGNITER-REQUEST-HANDLER', 'testComponent::onAjaxHandlerWithRedirect'); + + $response = (new MainController())->remap('components', []); + + expect($response->getStatusCode())->toBe(200) + ->and(json_decode($response->getContent()))->toHaveKey('X_IGNITER_REDIRECT', 'http://localhost'); +}); + +it('returns flash message in response header when in ajax handlers', function() { + preparePage(); + request()->headers->set('X-Requested-With', 'XMLHttpRequest'); + request()->headers->set('X-IGNITER-REQUEST-HANDLER', 'testComponent::onAjaxHandlerWithFlash'); + request()->headers->set('X-IGNITER-REQUEST-FLASH', '1'); + + $response = (new MainController())->remap('components', []); + + expect($response->getStatusCode())->toBe(200) + ->and(json_decode($response->getContent()))->toHaveKey('X_IGNITER_FLASH_MESSAGES'); +}); + +it('throws exception when validation fails in ajax handler', function() { + preparePage(); + request()->headers->set('X-Requested-With', 'XMLHttpRequest'); + request()->headers->set('X-IGNITER-REQUEST-HANDLER', 'testComponent::onAjaxHandlerWithValidationError'); + + expect(fn() => (new MainController())->remap('components', [])) + ->toThrow(AjaxException::class); +}); + +it('throws exception when ajax handler does not exists', function() { + preparePage(); + request()->headers->set('X-Requested-With', 'XMLHttpRequest'); + request()->headers->set('X-IGNITER-REQUEST-HANDLER', 'testComponent::onInvalidHandler'); + + expect(fn() => (new MainController())->remap('components', [])) + ->toThrow(FlashException::class, sprintf(lang('igniter::main.not_found.ajax_handler'), 'testComponent::onInvalidHandler')); +}); + +it('returns rendered page content in renderPage', function() { + expect((new MainController())->renderPage())->toBeString(); +}); + +it('returns page content using event in renderPage', function() { + Event::listen('main.page.render', function($controller, $contents) { + return 'test-page-content'; + }); + + expect((new MainController())->renderPage())->toContain('test-page-content'); +}); + +it('throws exception when component partial is not found in renderPartial', function() { + expect(fn() => (new MainController())->renderPartial('testComponent::nonexistent-partial')) + ->toThrow(FlashException::class, sprintf(lang('igniter::main.not_found.component'), 'testComponent')); +}); + +it('returns false when component partial is not found in renderPartial', function() { + expect((new MainController())->renderPartial('testComponent::nonexistent-partial', [], false))->toBeFalse(); +}); + +it('returns false when theme partial is not found in renderPartial', function() { + expect((new MainController())->renderPartial('nonexistent-partial', [], false))->toBeFalse(); +}); + +it('returns rendered partial content in renderPartial', function() { + expect((new MainController())->renderPartial('test-partial'))->toContain('This is a test partial content'); +}); + +it('returns rendered partial content using page.beforeRenderPartial event in renderPartial', function() { + Event::listen('main.page.beforeRenderPartial', function($controller, $contents) { + return 'test-partial-content'; + }); + + expect((new MainController())->renderPartial('custom-partial'))->toContain('test-partial-content'); +}); + +it('returns rendered partial content using page.renderPartial event in renderPartial', function() { + Event::listen('main.page.renderPartial', function($controller, $name, $partialContent) { + return 'test-partial-content'; + }); + + expect((new MainController())->renderPartial('test-partial'))->toContain('test-partial-content'); +}); + +it('returns rendered component partial content in renderPartial', function() { + $page = Page::resolveRouteBinding('components'); + $mainController = new MainController(); + $mainController->runPage($page); + $mainController->setComponentContext($page->loadedComponents['testComponent']); + $result = $mainController->renderPartial('@default'); + + expect($result)->toContain('This is a test component partial content'); +}); + +it('returns rendered content in renderContent', function() { + expect((new MainController())->renderContent('test-content'))->toContain('This is a test content'); +}); + +it('returns rendered content using page.beforeRenderContent event in renderContent', function() { + Event::listen('main.page.beforeRenderContent', function($controller, $name) { + return 'test-content'; + }); + + expect((new MainController())->renderContent('custom-content'))->toContain('test-content'); +}); + +it('returns rendered content using page.renderContent event in renderContent', function() { + Event::listen('main.page.renderContent', function($controller, $name, $fileContent) { + return $fileContent.'test-content'; + }); + + expect((new MainController())->renderContent('test-content'))->toContain("test-content"); +}); + +it('throws exception when content is not found in renderContent', function() { + expect(fn() => (new MainController())->renderContent('nonexistent-content')) + ->toThrow(FlashException::class, sprintf(lang('igniter::main.not_found.content'), 'nonexistent-content')); +}); + +it('returns true when component partial is found', function() { + $page = Page::resolveRouteBinding('components'); + $mainController = new MainController(); + $mainController->runPage($page); + $mainController->setComponentContext($page->loadedComponents['testComponent']); + + expect($mainController->hasPartial('@default'))->toBeTrue(); +}); + +it('returns true when theme partial is found', function() { + expect((new MainController())->hasPartial('test-partial'))->toBeTrue(); +}); + +it('returns false when component partial is not found', function() { + expect((new MainController())->hasPartial('testComponent::nonexistent-component-partial'))->toBeFalse(); +}); + +it('returns false when theme partial is not found', function() { + expect((new MainController())->hasPartial('nonexistent-theme-partial'))->toBeFalse(); +}); + +function preparePage(): void +{ + $route = new Route('GET', 'test-path', function() { + return 'test-path'; + }); + request()->setRouteResolver(fn() => $route); + $route->bind(request()); + $route->setParameter('_file_', Page::resolveRouteBinding('components')); +} diff --git a/tests/src/Main/Classes/MediaItemTest.php b/tests/src/Main/Classes/MediaItemTest.php new file mode 100644 index 00000000..ab528e81 --- /dev/null +++ b/tests/src/Main/Classes/MediaItemTest.php @@ -0,0 +1,59 @@ +isFile())->toBeTrue(); +}); + +it('returns correct file type for image', function() { + $mediaItem = new MediaItem('path/to/file.jpg', 1024, time(), MediaItem::TYPE_FILE, 'http://example.com/file.jpg'); + expect($mediaItem->getFileType())->toBe(MediaItem::FILE_TYPE_IMAGE); +}); + +it('returns correct file type for audio', function() { + $mediaItem = new MediaItem('path/to/file.mp3', 1024, time(), MediaItem::TYPE_FILE, 'http://example.com/file.mp3'); + expect($mediaItem->getFileType())->toBe(MediaItem::FILE_TYPE_AUDIO); +}); + +it('returns correct file type for video', function() { + $mediaItem = new MediaItem('path/to/file.mp4', 1024, time(), MediaItem::TYPE_FILE, 'http://example.com/file.mp4'); + expect($mediaItem->getFileType())->toBe(MediaItem::FILE_TYPE_VIDEO); +}); + +it('returns false when item is missing extension', function() { + $mediaItem = new MediaItem('path/to/file-with-no-extension', null, time(), MediaItem::TYPE_FOLDER, 'http://example.com/file-with-no-extension'); + expect($mediaItem->isFile())->toBeFalse(); +}); + +it('returns document file type for unknown extension', function() { + $mediaItem = new MediaItem('path/to/file.unknown', 1024, time(), MediaItem::TYPE_FILE, 'http://example.com/file.unknown'); + expect($mediaItem->getFileType())->toBe(MediaItem::FILE_TYPE_DOCUMENT); +}); + +it('returns formatted size string for file', function() { + File::shouldReceive('sizeToString')->with(1024)->andReturn('1 KB'); + $mediaItem = new MediaItem('path/to/file.jpg', 1024, time(), MediaItem::TYPE_FILE, 'http://example.com/file.jpg'); + expect($mediaItem->sizeToString())->toBe('1 KB'); +}); + +it('returns formatted size string for folder', function() { + $mediaItem = new MediaItem('path/to/folder', 10, time(), MediaItem::TYPE_FOLDER, 'folder'); + expect($mediaItem->sizeToString())->toBe('10 Items'); +}); + +it('returns formatted last modified date string', function() { + $timestamp = time(); + $mediaItem = new MediaItem('path/to/file.jpg', 1024, $timestamp, MediaItem::TYPE_FILE, 'http://example.com/file.jpg'); + expect($mediaItem->lastModifiedAsString())->toBe(Carbon::now()->toFormattedDateString()); +}); + +it('returns null for last modified date when not set', function() { + $mediaItem = new MediaItem('path/to/file.jpg', 1024, null, MediaItem::TYPE_FILE, 'http://example.com/file.jpg'); + expect($mediaItem->lastModifiedAsString())->toBeNull(); +}); diff --git a/tests/src/Main/Classes/MediaLibraryTest.php b/tests/src/Main/Classes/MediaLibraryTest.php new file mode 100644 index 00000000..b140286e --- /dev/null +++ b/tests/src/Main/Classes/MediaLibraryTest.php @@ -0,0 +1,268 @@ + ['file-ignore.png'], + 'igniter-system.assets.media.ignorePatterns' => ['file-pattern.*'], + ]); + + $this->mediaLibrary = resolve(MediaLibrary::class); +}); + +it('lists all folders recursively', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('directories')->andReturn(['', 'folder1', 'folder2/subfolder', 'exclude/folder']); + + expect($this->mediaLibrary->listAllFolders(null, ['exclude']))->toBe(['/', 'folder1', 'folder2/subfolder']); +}); + +it('ensures root path exists in listed folders', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('directories')->andReturn(['folder1', 'folder2/subfolder', 'exclude/folder']); + + expect($this->mediaLibrary->listAllFolders(null, ['exclude']))->toBe(['/', 'folder1', 'folder2/subfolder']); +}); + +it('lists files from cached', function() { + Cache::put('main.media.contents', base64_encode(serialize([ + 'single.directories./' => ['file1.jpg', 'file2.jpg'], + ]))); + + expect($this->mediaLibrary->listFolderContents('/', 'directories'))->toBe(['file1.jpg', 'file2.jpg']); +}); + +it('fetches files with search and filter options', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('files')->andReturn(['file1.jpg', 'file2.mp3']); + $filesystem->shouldReceive('lastModified')->andReturn(time()); + $filesystem->shouldReceive('size')->andReturn(1024, 2048); + $filesystem->shouldReceive('url')->andReturn('http://example.com/file1.jpg', 'http://example.com/file2.mp3'); + + $result = $this->mediaLibrary->fetchFiles('/', ['name', 'ascending'], 'file1'); + + expect($result)->toHaveCount(1)->and($result[0]->name)->toBe('file1.jpg'); +}); + +it('skips ignored files and sorts by date when fetching files', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('files')->andReturn(['file2.jpg', 'file1.jpg', 'file3.jpg', 'file-ignore.png', 'file-pattern.jpg']); + $filesystem->shouldReceive('lastModified')->andReturn( + now()->subMinutes(5)->timestamp, + now()->subMinutes(3)->timestamp, + now()->subMinutes(2)->timestamp, + ); + $filesystem->shouldReceive('size')->andReturn(1024, 2048, 208); + $filesystem->shouldReceive('url')->andReturn('http://example.com/file1.jpg'); + + $result = $this->mediaLibrary->fetchFiles('/', ['date', 'ascending'], ['filter' => 'image']); + + expect($result[0])->name->toBe('file3.jpg'); +}); + +it('sorts by date when fetching files', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('files')->andReturn(['file2.jpg', 'file1.jpg', 'file3.jpg', 'file-ignore.png', 'file-pattern.jpg']); + $filesystem->shouldReceive('lastModified')->andReturn(now()->timestamp); + $filesystem->shouldReceive('size')->andReturn(1024, 2048, 208); + $filesystem->shouldReceive('url')->andReturn('http://example.com/file1.jpg'); + + $result = $this->mediaLibrary->fetchFiles('/', ['size', 'descending'], ['filter' => 'image']); + + expect($result[0])->name->toBe('file3.jpg'); +}); + +it('returns file contents when get is called', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('get')->andReturn('file-contents'); + + expect($this->mediaLibrary->get('file.jpg'))->toBe('file-contents'); +}); + +it('returns file stream when get is called with stream true', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('readStream')->andReturn('file-stream'); + + expect($this->mediaLibrary->get('file.jpg', true))->toBe('file-stream'); +}); + +it('saves file contents when put is called', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('put')->andReturnTrue(); + + expect($this->mediaLibrary->put('file.jpg', 'file-contents'))->toBeTrue(); +}); + +it('creates a folder when makeFolder is called', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('makeDirectory')->andReturnTrue(); + + expect($this->mediaLibrary->makeFolder('new-folder'))->toBeTrue(); +}); + +it('copies a file when copyFile is called', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('copy')->andReturnTrue(); + + expect($this->mediaLibrary->copyFile('src-file.jpg', 'dest-file.jpg'))->toBeTrue(); +}); + +it('moves a file when moveFile is called', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('move')->andReturnTrue(); + + expect($this->mediaLibrary->moveFile('src-file.jpg', 'new-file.jpg'))->toBeTrue(); +}); + +it('renames a file when rename is called', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('move')->andReturnTrue(); + + expect($this->mediaLibrary->rename('old-file.jpg', 'new-file.jpg'))->toBeTrue(); +}); + +it('deletes multiple files when deleteFiles is called', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('delete')->andReturnTrue(); + + expect($this->mediaLibrary->deleteFiles(['file1.jpg', 'file2.jpg']))->toBeTrue(); +}); + +it('deletes a folder when deleteFolder is called', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('deleteDirectory')->andReturnTrue(); + + expect($this->mediaLibrary->deleteFolder('folder'))->toBeTrue(); +}); + +it('returns media URL for a given path', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('url')->andReturn('http://example.com/media/uploads/file.jpg'); + + $mediaLibrary = $this->mediaLibrary; + + expect($mediaLibrary->getMediaUrl('file.jpg'))->toBe('http://example.com/media/uploads/file.jpg') + ->and($mediaLibrary->getMediaUrl('http://example.com/file.jpg'))->toBe('http://example.com/file.jpg'); +}); + +it('returns true if file exists', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('exists')->andReturnTrue(); + + expect($this->mediaLibrary->exists('file.jpg'))->toBeTrue(); +}); + +it('returns uploads path when path starts with storage folder', function() { + expect($this->mediaLibrary->getUploadsPath('media/uploads/file.jpg'))->toBe('media/uploads/file.jpg'); +}); + +it('returns validated uploads path when path does not start with storage folder', function() { + expect($this->mediaLibrary->getUploadsPath('file.jpg'))->toBe('media/uploads/file.jpg'); +}); + +it('returns media thumb URL if thumb file exists', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('exists')->andReturnTrue(); + $filesystem->shouldReceive('url')->andReturn('http://example.com/thumb.jpg'); + + expect($this->mediaLibrary->getMediaThumb('file.jpg'))->toBe('http://example.com/thumb.jpg'); +}); + +it('returns default thumb path if original file does not exist', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('exists')->andReturn(false, false, false); + $filesystem->shouldReceive('makeDirectory')->andReturnTrue(); + $filesystem->shouldReceive('getDefaultThumbPath')->andReturn('default-thumb.jpg'); + $filesystem->shouldReceive('put')->andReturnSelf(); + app()->instance(Manipulator::class, $manipulator = mock(Manipulator::class)->makePartial()); + $manipulator->shouldReceive('manipulate')->andReturnSelf(); + $manipulator->shouldReceive('save')->andReturnSelf(); + $filesystem->shouldReceive('url')->andReturn('http://example.com/default-thumb.jpg'); + $filesystem->shouldReceive('path')->andReturn('/path/to/default-thumb.jpg'); + + expect($this->mediaLibrary->getMediaThumb('file.jpg', ['default' => 'default-thumb.jpg'])) + ->toBe('http://example.com/default-thumb.jpg'); +}); + +it('creates and returns default thumb path if original file does not exist', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('exists')->andReturn(false, true, false); + $filesystem->shouldReceive('getDefaultThumbPath')->andReturn('default-thumb.jpg'); + $filesystem->shouldReceive('put')->andReturnSelf(); + app()->instance(Manipulator::class, $manipulator = mock(Manipulator::class)->makePartial()); + $manipulator->shouldReceive('manipulate')->andReturnSelf(); + $manipulator->shouldReceive('save')->andReturnSelf(); + $filesystem->shouldReceive('url')->andReturn('http://example.com/default-thumb.jpg'); + + expect($this->mediaLibrary->getMediaThumb('file.jpg'))->toBe('http://example.com/default-thumb.jpg'); +}); + +it('returns media relative path when path starts with storage folder', function() { + expect($this->mediaLibrary->getMediaRelativePath('media/uploads/file.jpg'))->toBe('file.jpg'); +}); + +it('returns original path when path does not start with storage folder', function() { + expect($this->mediaLibrary->getMediaRelativePath('file.jpg'))->toBe('file.jpg'); +}); + +it('returns config value for given key', function() { + expect($this->mediaLibrary->getConfig())->toBeArray() + ->and($this->mediaLibrary->getConfig('path'))->toBe('media/uploads/'); +}); + +it('returns default value when config key is not found', function() { + expect($this->mediaLibrary->getConfig('nonexistent', 'default'))->toBe('default'); +}); + +it('returns allowed extensions from settings', function() { + expect($this->mediaLibrary->getAllowedExtensions())->toContain('jpg', 'png', 'gif'); +}); + +it('returns true if extension is allowed', function() { + expect($this->mediaLibrary->isAllowedExtension('jpg'))->toBeTrue(); +}); + +it('returns false if extension is not allowed', function() { + expect($this->mediaLibrary->isAllowedExtension('exe'))->toBeFalse(); +}); + +it('resets cache by forgetting cache key', function() { + expect($this->mediaLibrary->resetCache())->toBeNull(); +}); + +it('returns folder size', function() { + $filesystem = mock(Filesystem::class); + Storage::shouldReceive('disk')->andReturn($filesystem); + $filesystem->shouldReceive('files')->andReturn(['file1.jpg', 'file2.jpg']); + $filesystem->shouldReceive('lastModified')->andReturn(time()); + $filesystem->shouldReceive('size')->andReturn(1024, 2048); + $filesystem->shouldReceive('url')->andReturn('http://example.com/file1.jpg', 'http://example.com/file2.jpg'); + + expect($this->mediaLibrary->folderSize('/'))->toBe(3072); +}); diff --git a/tests/src/Main/Classes/RouteRegistrarTest.php b/tests/src/Main/Classes/RouteRegistrarTest.php new file mode 100644 index 00000000..c3d65444 --- /dev/null +++ b/tests/src/Main/Classes/RouteRegistrarTest.php @@ -0,0 +1,31 @@ +instance(Router::class, $router = mock(Router::class)); + $router->shouldReceive('getRouteMap')->once()->andReturn(collect([ + [ + 'uri' => 'test', + 'route' => 'test', + 'file' => 'test', + 'defaults' => ['test' => 'test'], + 'constraints' => ['test' => 'test'], + ], + ])); + + (new RouteRegistrar(new IlluminateRouter(new Dispatcher)))->forThemePages(); +}); + +it('returns empty array when theme routes are disabled', function() { + Igniter::shouldReceive('disableThemeRoutes')->andReturn(true)->once(); + Igniter::shouldReceive('uri')->andReturn('/'); + + (new RouteRegistrar(new IlluminateRouter(new Dispatcher)))->all(); +}); diff --git a/tests/src/Main/Classes/SupportConfigurableComponentTest.php b/tests/src/Main/Classes/SupportConfigurableComponentTest.php new file mode 100644 index 00000000..0d6c8fec --- /dev/null +++ b/tests/src/Main/Classes/SupportConfigurableComponentTest.php @@ -0,0 +1,96 @@ + 'value']; + } + }; +}); + +it('fills component properties when component uses ConfigurableComponent trait', function() { + $component = new class extends Component + { + use ConfigurableComponent; + + public $properties = []; + + public function getName() + { + return 'componentName'; + } + + public function getAlias() + { + return null; + } + + public function fill($values) + { + $this->properties = $values; + } + }; + + $hook = new SupportConfigurableComponent(); + $hook->setComponent($component); + $hook->mount([], 'key'); + + expect($component->properties)->toBe(['property' => 'value']); +}); + +it('does not fill component properties when component does not use ConfigurableComponent trait', function() { + $component = new class extends Component + { + public $properties = []; + + public function fill($values) + { + $this->properties = $values; + } + }; + + $hook = new SupportConfigurableComponent(); + $hook->setComponent($component); + $hook->mount([], 'key'); + + expect($component->properties)->toBeEmpty(); +}); + +it('fills component properties with alias when component uses ConfigurableComponent trait', function() { + $component = new class extends Component + { + use ConfigurableComponent; + + public $properties = []; + + public function getName() + { + return 'componentName'; + } + + public function getAlias() + { + return 'alias'; + } + + public function fill($values) + { + $this->properties = $values; + } + }; + + $hook = new SupportConfigurableComponent(); + $hook->setComponent($component); + $hook->mount([], 'key'); + + expect($component->properties)->toBe(['property' => 'value']); +}); diff --git a/tests/src/Main/Classes/ThemeManagerTest.php b/tests/src/Main/Classes/ThemeManagerTest.php index 48b8f314..f42a52c4 100644 --- a/tests/src/Main/Classes/ThemeManagerTest.php +++ b/tests/src/Main/Classes/ThemeManagerTest.php @@ -2,53 +2,350 @@ namespace Igniter\Tests\Main\Classes; +use Igniter\Flame\Composer\Manager as ComposerManager; +use Igniter\Flame\Exception\SystemException; +use Igniter\Flame\Pagic\Contracts\TemplateInterface; +use Igniter\Flame\Support\Facades\File; +use Igniter\Flame\Support\Facades\Igniter; +use Igniter\Main\Classes\Theme; use Igniter\Main\Classes\ThemeManager; +use Igniter\System\Classes\PackageManifest; +use Igniter\System\Classes\UpdateManager; +use Illuminate\Support\Facades\View; +use Illuminate\Support\ServiceProvider; -it('loads test theme', function() { - expect(resolve(ThemeManager::class)->findTheme('tests-theme')) - ->getPath() - ->toStartWith(testThemePath()); +beforeEach(function() { + $this->themePath = testThemePath(); + $this->themeManager = resolve(ThemeManager::class); }); -it('has active theme', function() { - expect(resolve(ThemeManager::class)->getActiveTheme()) +it('lists all themes in the system', function() { + expect($this->themeManager->listThemes()) + ->toBeArray() + ->toHaveCount(2) + ->and($this->themeManager->bootThemes())->toBeNull(); +}); + +it('throws exception when theme meta file is missing', function() { + expect(fn() => $this->themeManager->loadTheme('/path/to/missing-theme')) + ->toThrow(SystemException::class, 'Theme does not have a registration file in: /path/to/missing-theme'); +}); + +it('returns null when loading theme with invalid theme code', function() { + $themePath = '/path/to/theme-with-invalid-code'; + File::shouldReceive('exists')->with($themePath.'/theme.json')->andReturnTrue(); + File::shouldReceive('json')->with($themePath.'/theme.json')->andReturn([ + 'code' => 'invalid code', + ]); + + expect($this->themeManager->loadTheme($themePath))->toBeNull(); +}); + +it('returns null when loading theme with invalid theme path', function() { + $themePath = '/path/to/theme-with invalid path'; + File::shouldReceive('exists')->with($themePath.'/theme.json')->andReturnTrue(); + File::shouldReceive('json')->with($themePath.'/theme.json')->andReturn([]); + + expect($this->themeManager->loadTheme($themePath))->toBeNull(); +}); + +it('boots theme correctly', function() { + $oldPublishGroups = ServiceProvider::$publishGroups['igniter-assets']; + unset(ServiceProvider::$publishGroups['igniter-assets']); + $this->themeManager->themes['parentTheme'] = $parentTheme = new Theme($this->themePath, ['code' => 'parentTheme']); + $parentTheme->active = true; + + $theme = new Theme($this->themePath, [ + 'code' => 'tests-theme', + 'name' => 'Tests Theme', + 'publish-paths' => ['/public'], + 'source-path' => '/', + 'parent' => 'parentTheme', + ]); + $theme->active = true; + + $this->themeManager->bootTheme($theme); + + expect(ServiceProvider::$publishGroups['igniter-assets'])->toContain(public_path('vendor/tests-theme')) + ->and(View::exists('tests-theme::_layouts/default'))->toBeTrue() + ->and(View::exists('parentTheme::_layouts/default'))->toBeTrue(); + + ServiceProvider::$publishGroups['igniter-assets'] = $oldPublishGroups; +}); + +it('checks active theme', function() { + expect($this->themeManager->getActiveTheme()) ->getName() - ->toEqual('tests-theme'); + ->toEqual('tests-theme') + ->and($this->themeManager->isActive('invalid code'))->toBeFalse(); +}); + +it('finds test theme', function() { + expect($this->themeManager->findTheme('tests-theme')->getPath())->toStartWith($this->themePath); +}); + +it('finds parent theme using child theme code', function() { + $this->themeManager->themes['parentTheme'] = new Theme(dirname($this->themePath), ['code' => 'parentTheme']); + $this->themeManager->themes['childTheme'] = new Theme($this->themePath, [ + 'code' => 'tests-theme', + 'parent' => 'parentTheme', + ]); + + expect($this->themeManager->findParent('childTheme')->getPath())->toStartWith(dirname($this->themePath)); +}); + +it('lists theme within themes directory', function() { + $themesPath = '/path/to/themes'; + $customPath = '/path/to/custom-themes'; + $this->themeManager->clearDirectory(); + $this->themeManager->addDirectory($customPath); + Igniter::shouldReceive('themesPath')->andReturn($themesPath); + File::shouldReceive('isDirectory')->with($themesPath)->andReturnTrue(); + File::shouldReceive('glob')->with($themesPath.'/*/theme.json')->andReturn([ + $themesPath.'/theme1/theme.json', + ]); + File::shouldReceive('glob')->with($customPath.'/*/theme.json')->andReturn([ + $customPath.'/theme2/theme.json', + ]); + + expect($this->themeManager->folders($customPath)) + ->toContain($themesPath.'/theme1', $customPath.'/theme2'); +}); + +it('does not allow errors as a theme code', function() { + expect($this->themeManager->checkName('errors'))->toBeNull(); +}); + +it('checks a theme is locked correctly', function() { + $theme = new Theme($this->themePath, ['code' => 'tests-theme']); + $theme->locked = true; + $this->themeManager->themes['lockedTheme'] = $theme; + + expect($this->themeManager->isLocked('lockedTheme'))->toBeTrue(); +}); + +it('checks a theme path is locked', function() { + $this->themeManager->themes['parentTheme'] = $parentTheme = new Theme($this->themePath, [ + 'code' => 'parentTheme', + ]); + $theme = new Theme(__DIR__, ['code' => 'tests-theme', 'parent' => 'parentTheme']); + $parentTheme->locked = true; + $this->themeManager->themes['lockedTheme'] = $theme; + + expect($this->themeManager->isLockedPath($this->themePath.'/path/to/check', $theme))->toBeTrue(); + + $theme = new Theme(__DIR__, ['code' => 'tests-theme']); + + expect($this->themeManager->isLockedPath($this->themePath.'/path/to/check', $theme))->toBeTrue(); + + $theme = new Theme(__DIR__, ['code' => 'tests-theme']); + $theme->locked = true; + + expect($this->themeManager->isLockedPath(__DIR__.'/path/to/check', $theme))->toBeTrue(); +}); + +it('checks a parent theme has child theme', function() { + $this->themeManager->themes['parentTheme'] = new Theme($this->themePath, ['code' => 'parentTheme']); + $this->themeManager->themes['childTheme'] = new Theme($this->themePath, [ + 'code' => 'tests-theme', + 'parent' => 'parentTheme', + ]); + + expect($this->themeManager->checkParent('parentTheme'))->toBeTrue() + ->and($this->themeManager->checkParent('childTheme'))->toBeFalse(); }); it('finds a theme file', function() { - expect(resolve(ThemeManager::class) - ->findFile('_pages/components.blade.php', 'tests-theme')) - ->toStartWith(testThemePath()); + $file = '_pages/components.blade.php'; + + expect($this->themeManager->findFile($file, 'tests-theme'))->toStartWith($this->themePath) + ->and($this->themeManager->findFile($file, 'tests-theme', 'base'))->toBeFalse(); }); it('fails when theme file does not exist', function() { - expect(resolve(ThemeManager::class) - ->findFile('_pages/compone.blade.php', 'tests-theme')) - ->toBeFalse(); + expect($this->themeManager->findFile('_pages/compone.blade.php', 'tests-theme'))->toBeFalse(); }); -it('writes a theme file', function() {})->skip(); +it('reads an existing theme file successfully', function() { + expect($this->themeManager->readFile('_pages/components', 'tests-theme')) + ->toBeInstanceOf(TemplateInterface::class); +}); + +it('throws exception when reading a non-existent theme file', function() { + expect(fn() => $this->themeManager->readFile('_pages/compo', 'tests-theme')) + ->toThrow(SystemException::class, 'Theme template file not found: _pages/compo'); +}); -it('renames a theme file', function() { - $manager = resolve(ThemeManager::class); +it('creates a new theme file successfully', function() { + File::shouldReceive('extension')->with($this->themePath.'/_pages/fileName')->andReturn(false); + File::shouldReceive('isFile')->with($this->themePath.'/_pages/fileName.blade.php')->andReturn(false); + File::shouldReceive('isDirectory')->with($this->themePath.'/_pages')->andReturn(false); + File::shouldReceive('dirname')->with($this->themePath.'/_pages/fileName.blade.php')->andReturn($this->themePath.'/_pages'); + File::shouldReceive('makeDirectory')->with($this->themePath.'/_pages', 0777, true, true); + File::shouldReceive('put')->with($this->themePath.'/_pages/fileName.blade.php', "\n")->andReturn(true); + + expect($this->themeManager->newFile('_pages/fileName', 'tests-theme')) + ->toBe($this->themePath.'/_pages/fileName.blade.php'); +}); +it('throws exception when creating a file that already exists', function() { + File::shouldReceive('extension')->with($this->themePath.'/_pages/fileName')->andReturn(false); + File::shouldReceive('isFile')->with($this->themePath.'/_pages/fileName.blade.php')->andReturn(true); + File::shouldReceive('put')->with($this->themePath.'/_pages/fileName.blade.php', "\n")->andReturn(true); + + expect(fn() => $this->themeManager->newFile('_pages/fileName', 'tests-theme')) + ->toThrow(SystemException::class, 'Theme template file already exists: _pages/fileName'); +}); + +it('writes to an existing theme file successfully', function() { + expect($this->themeManager->writeFile('_content/test-content', [ + 'markup' => 'This is a test content', + ], 'tests-theme'))->toBeTrue(); +}); + +it('throws exception when writing to a non-existent theme file', function() { + expect(fn() => $this->themeManager->writeFile('_pages/fileName', [ + 'key' => 'value', + ], 'tests-theme'))->toThrow(SystemException::class, 'Theme template file not found: _pages/fileName'); +}); + +it('renames an existing theme file successfully', function() { $oldFile = '_pages/components'; $newFile = '_pages/compon'; - expect($manager->renameFile($oldFile, $newFile, 'tests-theme'))->toBeTrue(); + expect($this->themeManager->renameFile($oldFile, $newFile, 'tests-theme'))->toBeTrue() + ->and($this->themeManager->renameFile($newFile, $oldFile, 'tests-theme'))->toBeTrue(); +}); + +it('throws exception when renaming a non-existent theme file', function() { + expect(fn() => $this->themeManager->renameFile('_pages/fileName', '_pages/newFileName', 'tests-theme')) + ->toThrow(SystemException::class, 'Theme template file not found: _pages/fileName'); +}); + +it('throws exception when renaming a locked theme file', function() { + $theme = $this->themeManager->findTheme('tests-theme'); + $theme->locked = true; + + expect(fn() => $this->themeManager->renameFile('_pages/components', '_pages/newFileName', 'tests-theme')) + ->toThrow(SystemException::class, lang('igniter::system.themes.alert_theme_path_locked')); +}); + +it('throws exception when renaming to an existing theme file', function() { + $oldFile = '_pages/components'; + $newFile = '_pages/components'; + + expect(fn() => $this->themeManager->renameFile($oldFile, $newFile, 'tests-theme')) + ->toThrow(SystemException::class, 'Theme template file already exists: _pages/components'); +}); + +it('deletes an existing theme file successfully', function() { + File::put($this->themePath.'/_pages/fileName.blade.php', "\n"); + + expect($this->themeManager->deleteFile('_pages/fileName', 'tests-theme'))->toBeTrue(); +}); + +it('throws exception when deleting a non-existent theme file', function() { + expect(fn() => $this->themeManager->deleteFile('_pages/fileName', 'tests-theme')) + ->toThrow(SystemException::class, 'Theme template file not found: _pages/fileName'); +}); + +it('throws exception when deleting a locked theme file', function() { + $theme = $this->themeManager->findTheme('tests-theme'); + $theme->locked = true; + + expect(fn() => $this->themeManager->deleteFile('_pages/components', 'tests-theme')) + ->toThrow(SystemException::class, lang('igniter::system.themes.alert_theme_path_locked')); +}); + +it('removes theme folder successfully', function() { + File::shouldReceive('isDirectory')->with($this->themePath)->andReturn(true); + File::shouldReceive('deleteDirectory')->with($this->themePath)->andReturn(true); + + expect($this->themeManager->removeTheme('tests-theme'))->toBeTrue(); +}); + +it('returns false when removing non-existent theme folder', function() { + expect($this->themeManager->removeTheme('themeCode'))->toBeFalse(); +}); + +it('deletes theme and its data successfully', function() { + app()->instance(ComposerManager::class, $composerManager = mock(ComposerManager::class)); + $composerManager->shouldReceive('getPackageName')->with('tests-theme')->andReturn('packageName'); + $composerManager->shouldReceive('uninstall')->andReturnSelf(); + + File::shouldReceive('isDirectory')->with($this->themePath)->andReturn(true); + File::shouldReceive('deleteDirectory')->with($this->themePath)->andReturn(true); + + app()->instance(UpdateManager::class, $updateManager = mock(UpdateManager::class)); + $updateManager->shouldReceive('purgeExtension')->with('tests-theme')->andReturnSelf(); - $manager->renameFile($newFile, $oldFile, 'tests-theme'); + expect($this->themeManager->deleteTheme('tests-theme', true))->toBeNull(); }); -it('deletes a theme file', function() {})->skip(); +it('installs theme successfully', function() { + \Igniter\Main\Models\Theme::create([ + 'code' => 'tests-theme', + 'name' => 'Theme Name', + ]); -it('extracts a theme archive', function() {})->skip(); + app()->instance(PackageManifest::class, $packageManifest = mock(PackageManifest::class)); + $packageManifest->shouldReceive('getVersion')->with('tests-theme')->andReturn('1.0.0'); -it('deletes a theme directory', function() {})->skip(); + expect($this->themeManager->installTheme('tests-theme'))->toBeTrue(); +}); + +it('returns false when install theme can not be found', function() { + \Igniter\Main\Models\Theme::create([ + 'code' => 'invalid-theme', + 'name' => 'Theme Name', + ]); + + expect($this->themeManager->installTheme('invalid-theme'))->toBeFalse(); +}); + +it('updates installed themes config value', function() { + app()->instance(PackageManifest::class, $packageManifest = mock(PackageManifest::class)); + $packageManifest->shouldReceive('writeDisabled'); + $this->themeManager->disabledThemes = ['themeCode' => true]; + + $this->themeManager->updateInstalledThemes('tests-theme'); -it('installs a theme', function() {})->skip(); + expect($this->themeManager->disabledThemes)->not->toHaveKey('tests-theme'); -it('creates a child theme', function() {})->skip(); + $this->themeManager->updateInstalledThemes('tests-theme', false); -it('validates a theme configuration', function() {})->skip(); + expect($this->themeManager->disabledThemes)->toHaveKey('tests-theme'); +}); + +it('creates child theme successfully', function() { + $this->themeManager->clearDirectory(); + File::shouldReceive('isDirectory')->with(base_path('/themes/child-theme'))->andReturn(false); + File::shouldReceive('makeDirectory')->with(base_path('/themes/child-theme'), 0777, true, true); + File::shouldReceive('put')->withSomeOfArgs(base_path('/themes/child-theme/theme.json'))->andReturnTrue(); + File::shouldReceive('isDirectory')->with($this->themePath.'/_meta')->andReturn(true); + File::shouldReceive('isDirectory')->with($this->themePath.'/meta')->andReturn(false); + app()->instance(PackageManifest::class, $packageManifest = mock(PackageManifest::class)); + $packageManifest->shouldReceive('themes')->andReturn([]); + File::shouldReceive('isDirectory')->with(base_path('/themes'))->andReturn(true); + File::shouldReceive('glob')->with(base_path('/themes/*/theme.json'))->andReturn([ + base_path('/themes/child-theme/theme.json'), + ]); + File::shouldReceive('exists')->with(base_path('/themes/child-theme/theme.json'))->andReturn(true); + File::shouldReceive('exists')->with(base_path('/themes/child-theme/theme.json'))->andReturn(true); + File::shouldReceive('json')->with(base_path('/themes/child-theme/theme.json'))->andReturn([ + 'code' => 'childThemeCode', + 'name' => 'Child Theme', + 'description' => 'Description', + 'author' => 'Author', + ]); + File::shouldReceive('localToPublic')->andReturn('public'); + File::shouldReceive('isDirectory')->andReturn(false); + File::shouldReceive('exists')->andReturn(false); + + $childTheme = $this->themeManager->createChildTheme('tests-theme', 'child-theme'); + + expect($childTheme->code)->toBe('child-theme') + ->and($childTheme->name)->toBe('Tests Theme [child]') + ->and($childTheme->description)->toBe('A Test theme for TastyIgniter front-end') + ->and($childTheme->data)->toBe([]); +}); diff --git a/tests/src/Main/Classes/ThemeTest.php b/tests/src/Main/Classes/ThemeTest.php new file mode 100644 index 00000000..2745a670 --- /dev/null +++ b/tests/src/Main/Classes/ThemeTest.php @@ -0,0 +1,313 @@ +themePath = testThemePath(); +}); + +it('returns the correct theme name', function() { + $theme = new Theme('/path/to/theme', ['code' => 'themeCode', 'require' => 'requires']); + expect($theme->getName())->toBe('themeCode'); +}); + +it('returns the correct theme path', function() { + $theme = new Theme($this->themePath); + expect($theme->getPath())->toBe($this->themePath) + ->and($theme->getDirName())->toBe(basename($this->themePath)); +}); + +it('returns the correct source path', function() { + $theme = new Theme($this->themePath, ['source-path' => '/source']); + expect($theme->getSourcePath())->toBe($this->themePath.'/source'); +}); + +it('returns the correct meta path', function() { + File::shouldReceive('localToPublic')->andReturn('/public'); + File::shouldReceive('isDirectory')->andReturn(true, false); + $theme = new Theme($this->themePath); + expect($theme->getMetaPath())->toBe($this->themePath.'/_meta'); +}); + +it('returns the correct asset file path', function() { + File::shouldReceive('localToPublic')->andReturn('/public'); + File::shouldReceive('isDirectory')->andReturn(true, false); + $theme = new Theme($this->themePath, ['asset-path' => '/assets']); + expect($theme->getAssetsFilePath())->toBe($this->themePath.'/_meta/assets.json'); +}); + +it('returns the correct asset path', function() { + $theme = new Theme($this->themePath, ['asset-path' => '/assets']); + expect($theme->getAssetPath())->toBe($this->themePath.'/assets'); +}); + +it('returns the correct paths to publish', function() { + File::shouldReceive('localToPublic')->andReturn('/public'); + File::shouldReceive('exists')->andReturn(true); + $theme = new Theme($this->themePath, ['code' => 'themeCode', 'asset-path' => '/assets']); + expect($theme->getPathsToPublish())->toBe([$this->themePath.'/assets' => public_path('vendor/themeCode')]); +}); + +it('returns the correct parent path', function() { + $parentTheme = new Theme($this->themePath); + $theme = new Theme(dirname($this->themePath, 2), ['parent' => 'parentTheme']); + app()->instance(ThemeManager::class, $themeManager = mock(ThemeManager::class)); + $themeManager->shouldReceive('findTheme')->andReturn($parentTheme); + expect($theme->getParentPath())->toBe($this->themePath) + ->and($theme->getParentPath())->toBe($this->themePath); +}); + +it('returns the correct screenshot data', function() { + File::shouldReceive('localToPublic')->andReturn('/public'); + File::shouldReceive('isFile')->andReturn(true); + File::shouldReceive('get')->andReturn('file content'); + File::shouldReceive('exists')->andReturn(true); + $theme = new Theme(dirname($this->themePath, 2), ['parent' => 'parentTheme']); + app()->instance(ThemeManager::class, $themeManager = mock(ThemeManager::class)); + $themeManager->shouldReceive('findTheme')->andReturn(new Theme($this->themePath)); + + $theme->screenshot('screenshot'); + expect($theme->getScreenshotData())->toBe('data:image/png;base64,'.base64_encode('file content')); +}); + +it('throws exception for invalid screenshot file type', function() { + File::shouldReceive('localToPublic')->andReturn('/public'); + File::shouldReceive('isFile')->andReturn(true); + File::shouldReceive('exists')->andReturn(true); + $theme = new Theme($this->themePath); + $theme->fillFromConfig(); + $theme->screenshot = '/path/to/theme/screenshot.jpg'; + expect(fn() => $theme->getScreenshotData())->toThrow(FlashException::class); +}); + +it('returns empty screenshot data when screenshot does not exist', function() { + File::shouldReceive('localToPublic')->andReturn('/public'); + File::shouldReceive('isFile')->andReturn(true); + File::shouldReceive('exists')->andReturn(false); + $theme = new Theme($this->themePath); + expect($theme->getScreenshotData())->toBe('') + ->and($theme->getScreenshotData())->toBe(''); +}); + +it('loads theme file if exists', function() { + $parentTheme = new Theme($this->themePath); + $theme = new Theme(dirname($this->themePath, 2), ['parent' => 'parentTheme']); + app()->instance(ThemeManager::class, $themeManager = mock(ThemeManager::class)); + $themeManager->shouldReceive('findTheme')->andReturn($parentTheme); + File::shouldReceive('exists')->with(dirname($this->themePath, 2).'/theme.php')->andReturn(true, false); + File::shouldReceive('exists')->with($this->themePath.'/theme.php')->andReturn(true); + + expect(fn() => $theme->loadThemeFile())->toThrow(\ErrorException::class) + ->and(fn() => $theme->loadThemeFile())->toThrow(\ErrorException::class); +}); + +it('returns active theme code from event', function() { + Event::listen(ThemeGetActiveEvent::class, function() { + return 'activeThemeCode'; + }); + expect(Theme::getActiveCode())->toBe('activeThemeCode'); +}); + +it('returns the correct active theme code', function() { + expect(Theme::getActiveCode())->toBe('tests-theme'); +}); + +it('returns default theme code from config when no active theme', function() { + Event::fake(); + Igniter::shouldReceive('hasDatabase')->andReturn(true); + \Igniter\Main\Models\Theme::clearDefaultModel(); + \Igniter\Main\Models\Theme::create([ + 'name' => 'Default Theme', + 'code' => 'defaultThemeCode', + 'status' => 1, + 'is_default' => 1, + ]); + expect(Theme::getActiveCode())->toBe('defaultThemeCode'); +}); + +it('returns the correct form config', function() { + $theme = new Theme('/path/to/theme', ['code' => 'themeCode']); + $config = [ + 'form' => [ + 'general' => [ + 'fields' => [ + 'field1' => [ + 'label' => 'Field 1', + 'type' => 'text', + ], + ], + ], + ], + ]; + File::shouldReceive('getRequire')->andReturn($config); + File::shouldReceive('isDirectory')->andReturn(true); + File::shouldReceive('exists')->andReturn(true); + + $formConfig = $theme->getFormConfig(); + + expect($formConfig)->toBe($config['form']) + ->and($theme->hasFormConfig())->toBeTrue(); +}); + +it('returns correct config array', function() { + $theme = new Theme('/path/to/theme', ['code' => 'themeCode']); + File::shouldReceive('exists')->andReturn(true); + File::shouldReceive('getRequire')->andReturn(['form' => ['fields' => ['field1' => 'value1']]]); + File::shouldReceive('isDirectory')->andReturn(true); + + $config = $theme->getConfig(); + expect($config)->toBe(['form' => ['fields' => ['field1' => 'value1']]]) + ->and($config)->toBe($theme->getConfig()); +}); + +it('returns the correct custom data', function() { + $themeData = ['field1' => 'value1']; + \Igniter\Main\Models\Theme::create([ + 'code' => 'themeCode', + 'data' => $themeData, + ]); + $theme = new Theme('/path/to/theme', ['code' => 'themeCode']); + $config = [ + 'form' => [ + 'general' => [ + 'fields' => [ + 'field1' => [ + 'label' => 'Field 1', + 'type' => 'text', + ], + ], + ], + ], + ]; + File::shouldReceive('getRequire')->andReturn($config); + File::shouldReceive('isDirectory')->andReturn(true); + File::shouldReceive('exists')->andReturn(true); + + expect($theme->getCustomData())->toBe($themeData) + ->and($theme->hasCustomData())->toBeTrue() + ->and($theme->field1)->toBe('value1') + ->and(isset($theme->field1))->toBeTrue() + ->and(isset($theme->invalid))->toBeFalse(); +}); + +it('returns empty array when theme data is not available', function() { + $theme = new Theme('/path/to/theme', ['code' => 'themeCode']); + app()->instance(ThemeManager::class, $themeManager = mock(ThemeManager::class)); + $themeManager->shouldReceive('findTheme')->andReturn($theme); + + expect($theme->getAssetVariables())->toBe([]); +}); + +it('returns empty array when no asset variables are defined', function() { + \Igniter\Main\Models\Theme::clearThemeInstances(); + \Igniter\Main\Models\Theme::create([ + 'code' => 'themeCode', + 'data' => ['field1' => 'value1'], + ]); + File::shouldReceive('getRequire')->andReturn([ + 'form' => [ + 'general' => [ + 'title' => 'General', + 'fields' => [ + 'field1' => [ + 'label' => 'Field 1', + 'type' => 'text', + ], + ], + ], + ], + ]); + File::shouldReceive('isDirectory')->andReturn(true); + File::shouldReceive('exists')->andReturn(true); + File::shouldReceive('localToPublic')->andReturn('/public'); + $theme = new Theme('/path/to/theme', ['code' => 'themeCode']); + app()->instance(ThemeManager::class, $themeManager = mock(ThemeManager::class)); + $themeManager->shouldReceive('findTheme')->andReturn($theme); + + expect($theme->getAssetVariables())->toBe([]); +}); + +it('returns correct asset variables when defined', function() { + \Igniter\Main\Models\Theme::clearThemeInstances(); + \Igniter\Main\Models\Theme::create([ + 'code' => 'themeCode', + 'data' => ['field1' => 'value1'], + ]); + File::shouldReceive('localToPublic')->andReturn('/public'); + File::shouldReceive('getRequire')->andReturn([ + 'form' => [ + 'general' => [ + 'title' => 'General', + 'fields' => [ + 'field1' => [ + 'label' => 'Field 1', + 'type' => 'text', + 'assetVar' => 'var1', + ], + ], + ], + ], + ]); + File::shouldReceive('isDirectory')->andReturn(true); + File::shouldReceive('exists')->andReturn(true); + $theme = new Theme('/path/to/theme', ['code' => 'themeCode']); + app()->instance(ThemeManager::class, $themeManager = mock(ThemeManager::class)); + $themeManager->shouldReceive('findTheme')->andReturn($theme); + + expect($theme->getAssetVariables())->toBe(['var1' => 'value1']); +}); + +it('returns a collection of pages in the theme', function() { + expect((new Theme($this->themePath, ['code' => 'themeCode']))->listPages())->toBeCollection(); +}); + +it('returns a collection of partials in the theme', function() { + expect((new Theme($this->themePath, ['code' => 'themeCode']))->listPartials())->toBeCollection(); +}); + +it('returns a collection of layouts in the theme', function() { + expect((new Theme($this->themePath, ['code' => 'themeCode']))->listLayouts())->toBeCollection(); +}); + +it('creates new file source when no parent theme exists', function() { + $theme = new Theme($this->themePath); + $fileSource = $theme->makeFileSource(); + + expect($fileSource)->toBe($theme->makeFileSource()); +}); + +it('creates chain file source when parent theme exists', function() { + $theme = new Theme($this->themePath, ['parent' => 'parentTheme']); + app()->instance(ThemeManager::class, $themeManager = mock(ThemeManager::class)); + $themeManager->shouldReceive('findTheme')->andReturn(new Theme($this->themePath)); + + $fileSource = $theme->makeFileSource(); + expect($fileSource)->toBeInstanceOf(ChainFileSource::class); +}); + +it('creates a new template instance for a valid directory name', function() { + $theme = new Theme($this->themePath); + $template = $theme->newTemplate('_pages'); + expect($template)->toBeInstanceOf(PageTemplate::class); +}); + +it('returns the correct template class for a valid directory name', function() { + $theme = new Theme($this->themePath); + expect($theme->getTemplateClass('_pages'))->toBe(PageTemplate::class); +}); + +it('throws exception when getting template class for an invalid directory name', function() { + $theme = new Theme($this->themePath); + expect(fn() => $theme->getTemplateClass('_invalid'))->toThrow(RuntimeException::class, 'Source Model not found for [_invalid].'); +}); diff --git a/tests/src/Main/Components/BlankComponentTest.php b/tests/src/Main/Components/BlankComponentTest.php new file mode 100644 index 00000000..485317f0 --- /dev/null +++ b/tests/src/Main/Components/BlankComponentTest.php @@ -0,0 +1,12 @@ +isHidden)->toBeTrue() + ->and($component->onRender())->toBe('An error occurred'); +}); diff --git a/tests/src/Main/Components/ViewBagTest.php b/tests/src/Main/Components/ViewBagTest.php new file mode 100644 index 00000000..d59a1ea0 --- /dev/null +++ b/tests/src/Main/Components/ViewBagTest.php @@ -0,0 +1,34 @@ + 'customValue']; + + expect((new ViewBag())->validateProperties($properties))->toBe($properties); +}); + +it('returns property value when accessed via magic getter', function() { + $viewBag = new ViewBag(null, ['customField' => 'customValue']); + expect($viewBag->customField)->toBe('customValue') + ->and($viewBag->nonExistentField)->toBeNull(); +}); + +it('returns true when property exists via magic isset', function() { + $viewBag = new ViewBag(null, ['customField' => 'customValue']); + expect(isset($viewBag->customField))->toBeTrue() + ->and(isset($viewBag->nonExistentField))->toBeFalse(); +}); + +it('defines properties and returns them with title and type', function() { + $viewBag = new ViewBag(null, ['customField' => 'customValue']); + $expected = [ + 'customField' => [ + 'title' => 'customField', + 'type' => 'text', + ], + ]; + expect($viewBag->defineProperties())->toBe($expected); +}); diff --git a/tests/src/Main/FormWidgets/ComponentsTest.php b/tests/src/Main/FormWidgets/ComponentsTest.php new file mode 100644 index 00000000..b04aad5e --- /dev/null +++ b/tests/src/Main/FormWidgets/ComponentsTest.php @@ -0,0 +1,279 @@ +controller = resolve(TestController::class); + $this->formField = new FormField('components', 'Components'); + $this->formField->displayAs('components'); + $this->formField->arrayName = 'theme'; + $this->componentsWidget = new Components($this->controller, $this->formField, [ + 'model' => Theme::create(['code' => 'tests-theme']), + 'form' => [ + 'fields' => [ + 'alias' => [ + 'label' => 'igniter::system.themes.label_component_alias', + 'type' => 'text', + 'context' => ['edit', 'partial'], + 'comment' => 'igniter::system.themes.help_component_alias', + 'attributes' => [ + 'data-toggle' => 'disabled', + ], + ], + 'partial' => [ + 'label' => 'igniter::system.themes.label_override_partial', + 'type' => 'select', + 'context' => 'partial', + 'placeholder' => 'lang:igniter::admin.text_please_select', + ], + 'pageLimit' => [ + 'label' => 'igniter::system.label_page_limit', + 'type' => 'number', + 'context' => 'edit', + ], + ], + ], + ]); +}); + +it('initializes correctly', function() { + expect($this->componentsWidget->form)->toBeArray() + ->and($this->componentsWidget->prompt)->toBe('igniter::admin.text_please_select') + ->and($this->componentsWidget->addTitle)->toBe('igniter::main.components.button_new') + ->and($this->componentsWidget->editTitle)->toBe('igniter::main.components.button_edit') + ->and($this->componentsWidget->copyPartialTitle)->toBe('igniter::main.components.button_copy_partial'); +}); + +it('loads assets correctly', function() { + Assets::shouldReceive('addJs')->once()->with('formwidgets/recordeditor.modal.js', 'recordeditor-modal-js'); + Assets::shouldReceive('addCss')->once()->with('components.css', 'components-css'); + Assets::shouldReceive('addJs')->once()->with('components.js', 'components-js'); + + $this->componentsWidget->assetPath = []; + + $this->componentsWidget->loadAssets(); +}); + +it('renders correctly', function() { + $this->formField->value = [ + 'blankComponent' => [ + 'property' => 'value', + ], + 'invalidComponent' => [ + 'property' => 'value', + ], + ]; + + expect($this->componentsWidget->render())->toBeString(); +}); + +it('returns no save data constant when value is not an array', function() { + expect($this->componentsWidget->getSaveValue('stringValue'))->toBe(FormField::NO_SAVE_DATA); +}); + +it('sorts components and returns no save data constant when value is an array', function() { + $this->componentsWidget->data = (object)['fileSource' => $template = mock(TemplateInterface::class)]; + $template->shouldReceive('sortComponents')->with(['component1', 'component2']); + + expect($this->componentsWidget->getSaveValue(['component1', 'component2']))->toBe(FormField::NO_SAVE_DATA); +}); + +it('throws exception when loading hidden component', function() { + request()->request->add([ + 'alias' => 'blankComponent', + 'context' => 'edit', + ]); + + expect(fn() => $this->componentsWidget->onLoadRecord())->toThrow(FlashException::class, 'Selected component is hidden'); +}); + +it('throws exception when loading override partial for configurable component', function() { + request()->request->add([ + 'alias' => 'test::livewire-component', + 'context' => 'partial', + ]); + + expect(fn() => $this->componentsWidget->onLoadRecord()) + ->toThrow(FlashException::class, 'Selected component is not configurable, hence cannot override partial.'); +}); + +it('loads edit component form correctly', function() { + request()->request->add([ + 'alias' => 'testComponent', + 'context' => 'edit', + ]); + + expect($this->componentsWidget->onLoadRecord())->toBeString(); +}); + +it('loads override partial form correctly', function() { + request()->request->add([ + 'alias' => 'testComponent', + 'context' => 'partial', + ]); + + expect($this->componentsWidget->onLoadRecord())->toBeString(); +}); + +it('throws exception when saving with locked theme', function() { + app()->instance(ThemeManager::class, $themeManager = mock(ThemeManager::class)); + $themeManager->shouldReceive('isLocked')->with('tests-theme')->andReturn(true); + + expect(fn() => $this->componentsWidget->onSaveRecord()) + ->toThrow(FlashException::class, 'This is a locked theme, changes are restricted, create a child theme to make changes.'); +}); + +it('throws exception when saving with invalid component in post request', function() { + request()->request->add(['recordId' => 'invalidComponent']); + request()->setMethod('post'); + + expect(fn() => $this->componentsWidget->onSaveRecord())->toThrow(FlashException::class, 'Invalid component selected'); +}); + +it('throws exception when saving with invalid component', function() { + request()->request->add([ + 'recordId' => 'invalidComponent', + ]); + + expect(fn() => $this->componentsWidget->onSaveRecord())->toThrow(FlashException::class, 'Invalid component selected'); +}); + +it('throws exception when saving with invalid template file', function() { + request()->setMethod('post'); + request()->request->add([ + 'recordId' => 'testComponent', + ]); + $this->componentsWidget->data = (object)['fileSource' => null]; + + expect(fn() => $this->componentsWidget->onSaveRecord())->toThrow(FlashException::class, 'Template file not found'); +}); + +it('throws exception when overriding partial with non existence partial', function() { + request()->setMethod('post'); + request()->request->add([ + 'recordId' => 'testComponent', + 'theme' => [ + 'componentData' => [ + 'alias' => 'testComponent', + 'partial' => 'invalidPartial', + ], + ], + ]); + $this->componentsWidget->data = (object)['fileSource' => mock(TemplateInterface::class)]; + + expect(fn() => $this->componentsWidget->onSaveRecord()) + ->toThrow(lang('igniter::system.themes.alert_component_partial_not_found')); +}); + +it('overrides component partial successfully', function() { + request()->setMethod('post'); + request()->request->add([ + 'recordId' => 'testComponent', + 'theme' => [ + 'componentData' => [ + 'alias' => 'testComponent', + 'partial' => 'testPartial', + ], + ], + ]); + $this->componentsWidget->data = (object)['fileSource' => mock(TemplateInterface::class)]; + $fileMock = File::partialMock(); + $fileMock->shouldReceive('exists') + ->withArgs(fn($path) => ends_with($path, '_components/testcomponent/testPartial.blade.php')) + ->andReturn(true); + $fileMock->shouldReceive('isDirectory')->andReturnFalse(); + $fileMock->shouldReceive('makeDirectory')->andReturnTrue(); + $fileMock->shouldReceive('copy')->andReturnTrue(); + + expect($this->componentsWidget->onSaveRecord())->toBeInstanceOf(RedirectResponse::class); +}); + +it('adds component correctly', function() { + request()->setMethod('post'); + request()->request->add([ + 'recordId' => 'testComponent', + ]); + $this->formField->value = [ + 'testComponent' => [ + 'property' => 'value', + ], + ]; + $this->componentsWidget->data = (object)['fileSource' => $template = mock(TemplateInterface::class)]; + $template->shouldReceive('updateComponent')->once(); + $template->settings = ['components' => []]; + + $this->componentsWidget->bindEvent('updated', function($codeAlias) { + expect($codeAlias)->toBe('testComponent testComponentCopy'); + }); + + expect($this->componentsWidget->onSaveRecord())->toBeArray(); +}); + +it('updates component correctly', function() { + request()->request->add([ + 'recordId' => 'testComponent', + 'theme' => [ + 'componentData' => [ + 'alias' => 'testComponent', + 'pageLimit' => '10', + ], + ], + ]); + $this->formField->value = [ + 'testComponent' => [ + 'pageLimit' => '10', + ], + ]; + $this->componentsWidget->data = (object)['fileSource' => $template = mock(TemplateInterface::class)]; + $template->shouldReceive('updateComponent')->once(); + $template->settings = ['components' => ['testComponent' => []]]; + + expect($this->componentsWidget->onSaveRecord())->toBeArray(); +}); + +it('throws exception when removing component with invalid code', function() { + expect(fn() => $this->componentsWidget->onRemoveComponent())->toThrow(FlashException::class, 'Invalid component selected'); +}); + +it('throws exception when removing component with missing template file', function() { + request()->request->add([ + 'code' => 'testComponent', + ]); + $this->componentsWidget->data = (object)['fileSource' => null]; + + expect(fn() => $this->componentsWidget->onRemoveComponent()) + ->toThrow(FlashException::class, 'Template file not found'); +}); + +it('removes component successfully', function() { + request()->request->add([ + 'code' => 'validComponent', + ]); + $this->componentsWidget->data = (object)['fileSource' => $template = mock(TemplateInterface::class)]; + $template->shouldReceive('getAttributes')->andReturn([ + 'settings' => [ + 'components' => [ + 'validComponent' => [], + 'testComponent' => [], + ], + ], + ]); + $template->shouldReceive('setRawAttributes'); + $template->shouldReceive('setAttribute'); + $template->shouldReceive('save'); + $template->settings = ['components' => ['testComponent' => []]]; + + expect($this->componentsWidget->onRemoveComponent())->toBeArray() + ->and($this->formField->value)->toBe(['testComponent' => []]); +}); diff --git a/tests/src/Main/FormWidgets/MediaFinderTest.php b/tests/src/Main/FormWidgets/MediaFinderTest.php new file mode 100644 index 00000000..8f4066d7 --- /dev/null +++ b/tests/src/Main/FormWidgets/MediaFinderTest.php @@ -0,0 +1,272 @@ +controller = resolve(TestController::class); + $this->formField = new FormField('image', 'Image'); + $this->formField->displayAs('image'); + $this->formField->arrayName = 'theme'; + $this->mediaModel = new class extends Model + { + use HasMedia; + + public $mediable = ['thumb']; + + public function findMedia($mediaId) + { + return new Media(['id' => $mediaId]); + } + + public function deleteMedia($mediaId) + { + return null; + } + }; + $this->mediaFinderWidget = new MediaFinder($this->controller, $this->formField, [ + 'model' => $this->mediaModel, + ]); +}); + +it('initializes correctly', function() { + expect($this->mediaFinderWidget->prompt)->toBe('lang:igniter::admin.text_empty') + ->and($this->mediaFinderWidget->mode)->toBe('grid') + ->and($this->mediaFinderWidget->isMulti)->toBeFalse() + ->and($this->mediaFinderWidget->thumbOptions)->toBe([ + 'fit' => 'contain', + 'width' => 122, + 'height' => 122, + ]) + ->and($this->mediaFinderWidget->useAttachment)->toBeFalse(); +}); + +it('loads assets correctly', function() { + Assets::shouldReceive('addJs')->once()->with('formwidgets/repeater.js', 'repeater-js'); + Assets::shouldReceive('addJs')->once()->with('mediafinder.js', 'mediafinder-js'); + Assets::shouldReceive('addCss')->once()->with('mediafinder.css', 'mediafinder-css'); + + $this->mediaFinderWidget->config['useAttachment'] = true; + $this->mediaFinderWidget->assetPath = []; + + $this->mediaFinderWidget->loadAssets(); +}); + +it('renders correctly', function() { + expect($this->mediaFinderWidget->render())->toBeString(); +}); + +it('returns media identifier when media is an instance of Media', function() { + $media = new Media(['id' => 1]); + + expect($this->mediaFinderWidget->getMediaIdentifier($media))->toBe(1) + ->and($this->mediaFinderWidget->getMediaIdentifier(null))->toBeNull() + ->and($this->mediaFinderWidget->getMediaIdentifier('stringMedia'))->toBeNull(); +}); + +it('returns media name when media is an instance of Media', function() { + $media = new Media(['id' => 1, 'file_name' => 'filename.jpg']); + expect($this->mediaFinderWidget->getMediaName($media))->toBe('filename.jpg') + ->and($this->mediaFinderWidget->getMediaName('/path/to/media.jpg'))->toBe('path/to/media.jpg'); +}); + +it('returns media path when media is an instance of Media', function() { + $media = new Media(['id' => 1, 'name' => 'mediafilename.jpg']); + expect($this->mediaFinderWidget->getMediaPath($media))->toBe('media/attachments/public/med/iaf/ile/mediafilename.jpg') + ->and($this->mediaFinderWidget->getMediaPath('/path/to/media.jpg'))->toBe('path/to/media.jpg'); +}); + +it('returns media thumb when media is an instance of Media', function() { + $media = new Media(['id' => 1, 'name' => 'mediafilename.jpg']); + expect($this->mediaFinderWidget->getMediaThumb($media))->toEndWith('media/attachments/public/med/iaf/ile/mediafilename.jpg') + ->and($this->mediaFinderWidget->getMediaThumb('path/to/media.jpg'))->toEndWith('_122x122_contain.jpg') + ->and($this->mediaFinderWidget->getMediaThumb('/'))->toBe(''); +}); + +it('returns file type document when media has no extension', function() { + $media = new Media(['id' => 1, 'file_name' => 'path/to/media']); + expect($this->mediaFinderWidget->getMediaFileType($media))->toBe(MediaItem::FILE_TYPE_DOCUMENT); +}); + +it('returns file type image when media has image extension', function() { + expect($this->mediaFinderWidget->getMediaFileType('path/to/media.jpg'))->toBe(MediaItem::FILE_TYPE_IMAGE); +}); + +it('returns file type audio when media has audio extension', function() { + expect($this->mediaFinderWidget->getMediaFileType('path/to/media.mp3'))->toBe(MediaItem::FILE_TYPE_AUDIO); +}); + +it('returns file type video when media has video extension', function() { + expect($this->mediaFinderWidget->getMediaFileType('path/to/media.mp4'))->toBe(MediaItem::FILE_TYPE_VIDEO); +}); + +it('returns file type document when media has unknown extension', function() { + expect($this->mediaFinderWidget->getMediaFileType('path/to/media.txt'))->toBe(MediaItem::FILE_TYPE_DOCUMENT); +}); + +it('returns empty array when useAttachment is false on load attachment config', function() { + $this->mediaFinderWidget->useAttachment = false; + expect($this->mediaFinderWidget->onLoadAttachmentConfig())->toBeArray()->toBeEmpty(); + + $this->mediaFinderWidget->useAttachment = true; + expect($this->mediaFinderWidget->onLoadAttachmentConfig())->toBeArray()->toBeEmpty(); +}); + +it('returns empty array when model does not use HasMedia trait on load attachment config', function() { + $this->mediaFinderWidget->useAttachment = true; + $this->mediaFinderWidget->model = new Page(); + request()->request->add(['media_id' => 1]); + + expect($this->mediaFinderWidget->onLoadAttachmentConfig())->toBeArray()->toBeEmpty(); +}); + +it('loads attachment config form', function() { + $this->mediaFinderWidget->useAttachment = true; + request()->request->add(['media_id' => 123]); + + expect($this->mediaFinderWidget->onLoadAttachmentConfig())->toBeArray()->not()->toBeEmpty(); +}); + +it('returns empty array when useAttachment is false on save attachment config', function() { + $this->mediaFinderWidget->useAttachment = false; + expect($this->mediaFinderWidget->onSaveAttachmentConfig())->toBeArray()->toBeEmpty(); + + $this->mediaFinderWidget->useAttachment = true; + expect($this->mediaFinderWidget->onSaveAttachmentConfig())->toBeArray()->toBeEmpty(); +}); + +it('returns empty array when model does not use HasMedia trait on save attachment config', function() { + $this->mediaFinderWidget->useAttachment = true; + $this->mediaFinderWidget->model = new Page(); + request()->request->add(['media_id' => 1]); + + expect($this->mediaFinderWidget->onSaveAttachmentConfig())->toBeArray()->toBeEmpty(); +}); + +it('saves attachment config form', function() { + $this->mediaFinderWidget->useAttachment = true; + request()->request->add(['media_id' => 123]); + + expect($this->mediaFinderWidget->onSaveAttachmentConfig())->toBeArray()->not()->toBeEmpty(); +}); + +it('returns empty array when useAttachment is false on remove attachment', function() { + $this->mediaFinderWidget->useAttachment = false; + expect($this->mediaFinderWidget->onRemoveAttachment())->toBeNull(); +}); + +it('returns empty array when model does not use HasMedia trait on remove attachment', function() { + $this->mediaFinderWidget->useAttachment = true; + $this->mediaFinderWidget->model = new Page(); + request()->request->add(['media_id' => 1]); + + expect($this->mediaFinderWidget->onRemoveAttachment())->toBeNull(); +}); + +it('removes attachment correctly', function() { + $this->mediaFinderWidget->useAttachment = true; + request()->request->add(['media_id' => 123]); + + expect($this->mediaFinderWidget->onRemoveAttachment())->toBeNull(); +}); + +it('returns empty array when useAttachment is false on add attachment', function() { + $this->mediaFinderWidget->useAttachment = false; + expect($this->mediaFinderWidget->onAddAttachment())->toBeArray()->toBeEmpty(); +}); + +it('returns empty array when model does not use HasMedia trait on add attachment', function() { + $this->mediaFinderWidget->useAttachment = true; + $this->mediaFinderWidget->model = new Page(); + request()->request->add(['media_id' => 1]); + + expect($this->mediaFinderWidget->onAddAttachment())->toBeArray()->toBeEmpty(); +}); + +it('throws exception when field is missing in mediable config', function() { + $this->mediaFinderWidget->useAttachment = true; + $this->mediaFinderWidget->model = new class extends Model + { + use HasMedia; + + public $mediable = []; + }; + request()->request->add(['media_id' => 1]); + + expect(fn() => $this->mediaFinderWidget->onAddAttachment()) + ->toThrow(FlashException::class); +}); + +it('throws exception when adding attachment on a non existing model', function() { + $this->mediaFinderWidget->useAttachment = true; + $this->mediaFinderWidget->model = new class extends Model + { + use HasMedia; + + public $mediable = ['image']; + }; + request()->merge([ + 'media_id' => 1, + 'items' => [ + [ + 'name' => 'media.jpg', + 'path' => 'path/to/media.jpg', + ], + ], + ]); + + expect(fn() => $this->mediaFinderWidget->onAddAttachment()) + ->toThrow(FlashException::class); +}); + +it('adds attachment correctly', function() { + $this->mediaFinderWidget->useAttachment = true; + $this->mediaFinderWidget->model = new class extends Model + { + use HasMedia; + + public $mediable = ['image']; + }; + $this->mediaFinderWidget->model->exists = true; + request()->merge([ + 'media_id' => 1, + 'items' => [ + [ + 'name' => 'media.jpg', + 'path' => 'path/to/media.jpg', + ], + ], + ]); + + expect($this->mediaFinderWidget->onAddAttachment())->toBeArray()->not()->toBeEmpty(); +}); + +it('returns array with null when getLoadValue is called with isMulti set to true', function() { + $this->mediaFinderWidget->isMulti = true; + $this->formField->value = []; + expect($this->mediaFinderWidget->getLoadValue())->toBe([null]); +}); + +it('returns no save data constant when getSaveValue is called with formField hidden', function() { + $this->mediaFinderWidget->useAttachment = true; + $this->formField = new FormField('image', 'Image'); + $this->formField->hidden = true; + expect($this->mediaFinderWidget->getSaveValue('anyValue'))->toBe(FormField::NO_SAVE_DATA); +}); + +it('returns value when getSaveValue is called with valid value', function() { + $this->formField = new FormField('image', 'Image'); + $this->formField->disabled = false; + $this->formField->hidden = false; + expect($this->mediaFinderWidget->getSaveValue('validValue'))->toBe('validValue'); +}); diff --git a/tests/src/Main/FormWidgets/TemplateEditorTest.php b/tests/src/Main/FormWidgets/TemplateEditorTest.php new file mode 100644 index 00000000..2de920dd --- /dev/null +++ b/tests/src/Main/FormWidgets/TemplateEditorTest.php @@ -0,0 +1,207 @@ +instance(ThemeManager::class, $this->themeManager = mock(ThemeManager::class)); + $this->themeManager->shouldReceive('isLocked')->with('tests-theme')->andReturnFalse()->byDefault(); + $this->themeManager->shouldReceive('readFile')->andReturn(new PageTemplate([ + 'file_name' => 'components', + 'content' => 'new content', + ]))->byDefault(); + $this->controller = resolve(TestController::class); + $this->formField = new FormField('template', 'Template Editor'); + $this->formField->displayAs('templateeditor'); + $this->formField->arrayName = 'theme'; + $this->templateEditorWidget = new TemplateEditor($this->controller, $this->formField, [ + 'model' => Theme::create(['code' => 'tests-theme']), + ]); +}); + +it('initializes correctly', function() { + expect($this->templateEditorWidget->form)->toBeNull() + ->and($this->templateEditorWidget->placeholder)->toBe('igniter::system.themes.text_select_file') + ->and($this->templateEditorWidget->formName)->toBe('igniter::system.themes.label_template') + ->and($this->templateEditorWidget->addLabel)->toBe('igniter::system.themes.button_new_source') + ->and($this->templateEditorWidget->editLabel)->toBe('igniter::system.themes.button_rename_source') + ->and($this->templateEditorWidget->deleteLabel)->toBe('igniter::system.themes.button_delete_source'); +}); + +it('renders correctly', function() { + $this->themeManager->shouldReceive('findTheme')->with('tests-theme')->andReturn($theme = mock(ThemeClass::class)); + $theme->shouldReceive('getTemplateClass')->andReturn(PageTemplate::class); + $theme->shouldReceive('getName')->andReturn('tests-theme'); + + expect($this->templateEditorWidget->render())->toBeString(); + + $formWidget = $this->templateEditorWidget->vars['templateWidget']->getFormWidget('settings[components]'); + $formWidget->fireEvent('partialCopied', ['partialName']); + $formWidget->fireEvent('updated', ['partialName']); +}); + +it('throws exception when rendering widget with invalid theme', function() { + $this->themeManager->shouldReceive('findTheme')->with('tests-theme')->andReturnNull(); + + expect(fn() => $this->templateEditorWidget->render())->toThrow(FlashException::class); +}); + +it('reloads widget correctly', function() { + $this->themeManager->shouldReceive('findTheme')->with('tests-theme')->andReturn($theme = mock(ThemeClass::class)); + $theme->shouldReceive('getTemplateClass')->andReturn(PageTemplate::class); + $theme->shouldReceive('getName')->andReturn('tests-theme'); + + expect($this->templateEditorWidget->reload())->toBeArray() + ->toHaveKey('#'.$this->templateEditorWidget->getId('container')); +}); + +it('throws exception when reloading widget with invalid template file', function() { + $this->themeManager->shouldReceive('readFile')->andThrow(Exception::class); + $this->themeManager->shouldReceive('findTheme')->with('tests-theme')->andReturn($theme = mock(ThemeClass::class)); + $theme->shouldReceive('getTemplateClass')->andReturn(PageTemplate::class); + $theme->shouldReceive('getName')->andReturn('tests-theme'); + + expect($this->templateEditorWidget->reload())->toBeArray(); +}); + +it('chooses template file correctly', function() { + request()->request->add([ + 'Theme' => [ + 'source' => [ + 'template' => [ + 'type' => '_pages', + 'file' => 'components', + ], + ], + ], + ]); + + expect($this->templateEditorWidget->onChooseFile())->toBeInstanceOf(RedirectResponse::class) + ->and($this->templateEditorWidget->getTemplateValue('type'))->toBe('_pages') + ->and($this->templateEditorWidget->getTemplateValue('file'))->toBe('components'); +}); + +it('throws exception when renaming, deleting or creating template file of locked theme', function() { + $this->themeManager->shouldReceive('isLocked')->with('tests-theme')->andReturnTrue(); + + expect(fn() => $this->templateEditorWidget->onManageSource()) + ->toThrow(FlashException::class, lang('igniter::system.themes.alert_theme_locked')); +}); + +it('renames template file', function() { + $this->themeManager->shouldReceive('renameFile')->once()->with( + '_pages/components', + '_pages/new-components', + 'tests-theme', + ); + + $this->templateEditorWidget->setTemplateValue('type', '_pages'); + $this->templateEditorWidget->setTemplateValue('file', 'components'); + + request()->request->add([ + 'action' => 'rename', + 'name' => 'new-components', + ]); + + expect($this->templateEditorWidget->onManageSource())->toBeInstanceOf(RedirectResponse::class) + ->and($this->templateEditorWidget->getTemplateValue('file'))->toBe('new-components'); +}); + +it('creates new template file', function() { + $this->themeManager->shouldReceive('newFile')->once()->with('_pages/new-file', 'tests-theme'); + + $this->templateEditorWidget->setTemplateValue('type', '_pages'); + + request()->request->add([ + 'action' => 'new', + 'name' => 'new-file', + ]); + + expect($this->templateEditorWidget->onManageSource())->toBeInstanceOf(RedirectResponse::class) + ->and($this->templateEditorWidget->getTemplateValue('file'))->toBe('new-file'); +}); + +it('deletes template file', function() { + $this->themeManager->shouldReceive('deleteFile')->once()->with('_pages/components', 'tests-theme'); + + $this->templateEditorWidget->setTemplateValue('type', '_pages'); + $this->templateEditorWidget->setTemplateValue('file', 'components'); + + request()->request->add([ + 'action' => 'delete', + 'name' => '', + ]); + + expect($this->templateEditorWidget->onManageSource())->toBeInstanceOf(RedirectResponse::class) + ->and($this->templateEditorWidget->getTemplateValue('file'))->toBe(''); +}); + +it('throws exception when updating template file content of locked theme', function() { + $this->themeManager->shouldReceive('isLocked')->with('tests-theme')->andReturnTrue(); + + expect(fn() => $this->templateEditorWidget->onSaveSource()) + ->toThrow(FlashException::class, lang('igniter::system.themes.alert_theme_locked')); +}); + +it('fails validation when template file has been modified in a different session', function() { + $this->templateEditorWidget->setTemplateValue('type', '_pages'); + $this->templateEditorWidget->setTemplateValue('file', 'components'); + $this->templateEditorWidget->setTemplateValue('mTime', 'components'); + + request()->request->add([ + 'Theme' => [ + 'source' => [ + 'type' => '_pages', + 'file' => 'components', + 'content' => 'new content', + ], + ], + ]); + + expect(fn() => $this->templateEditorWidget->onSaveSource())->toThrow(ValidationException::class); +}); + +it('updates template file content', function() { + $this->themeManager->shouldReceive('findTheme')->with('tests-theme')->andReturn($theme = mock(ThemeClass::class)); + $theme->shouldReceive('getTemplateClass')->andReturn(PageTemplate::class); + $theme->shouldReceive('getName')->andReturn('tests-theme'); + + $this->templateEditorWidget->prepareVars(); + $this->templateEditorWidget->vars['templateWidget']->data->fileSource = $pageTemplate = mock(PageTemplate::class); + $pageTemplate->shouldReceive('fill')->andReturnSelf(); + $pageTemplate->shouldReceive('save')->andReturnTrue(); + $pageTemplate->shouldReceive('getAttribute')->with('mTime')->andReturnNull(); + $pageTemplate->shouldReceive('getAttribute')->with('file_name')->andReturn('_pages/components'); + + $this->templateEditorWidget->setTemplateValue('type', '_pages'); + $this->templateEditorWidget->setTemplateValue('file', 'components'); + + request()->request->add([ + 'Theme' => [ + 'source' => [ + 'markup' => 'new content', + 'codeSection' => 'components', + 'settings' => [ + 'components' => [], + 'title' => 'New Title', + 'description' => 'New Description', + 'layout' => 'default', + 'permalink' => '/new-permalink', + ], + ], + ], + ]); + + expect($this->templateEditorWidget->onSaveSource())->toBeNull(); +}); diff --git a/tests/src/Main/Helpers/ImageHelperTest.php b/tests/src/Main/Helpers/ImageHelperTest.php new file mode 100644 index 00000000..ed68eff4 --- /dev/null +++ b/tests/src/Main/Helpers/ImageHelperTest.php @@ -0,0 +1,44 @@ +set('igniter-system.assets.media.folder', 'data'); + app()->instance(MediaLibrary::class, $mediaLibrary = mock(MediaLibrary::class)); + $mediaLibrary->shouldReceive('getMediaThumb')->with('path/to/image.jpg', [ + 'width' => 100, + 'height' => 200, + ])->andReturn('resized/image.jpg'); + + expect(ImageHelper::resize('path/to/image.jpg', 100, 200))->toBe('resized/image.jpg'); +}); + +it('returns resized image path with given options array', function() { + config()->set('igniter-system.assets.media.folder', 'data'); + app()->instance(MediaLibrary::class, $mediaLibrary = mock(MediaLibrary::class)); + $mediaLibrary->shouldReceive('getMediaThumb')->with('path/to/image.jpg', [ + 'width' => 150, + 'height' => 150, + 'fit' => 'crop', + ])->andReturn('resized/image.jpg'); + + expect(ImageHelper::resize('path/to/image.jpg', [ + 'width' => 150, + 'height' => 150, + 'fit' => 'crop', + ]))->toBe('resized/image.jpg'); +}); + +it('returns resized image path with root folder stripped from path', function() { + config()->set('igniter-system.assets.media.folder', 'data'); + app()->instance(MediaLibrary::class, $mediaLibrary = mock(MediaLibrary::class)); + $mediaLibrary->shouldReceive('getMediaThumb')->with('image.jpg', [ + 'width' => 100, + 'height' => 200, + ])->andReturn('resized/image.jpg'); + + expect(ImageHelper::resize('data/image.jpg', 100, 200))->toBe('resized/image.jpg'); +}); diff --git a/tests/src/Main/Http/Controllers/MediaManagerTest.php b/tests/src/Main/Http/Controllers/MediaManagerTest.php new file mode 100644 index 00000000..a70b80aa --- /dev/null +++ b/tests/src/Main/Http/Controllers/MediaManagerTest.php @@ -0,0 +1,10 @@ +get(route('igniter.main.media_manager')) + ->assertStatus(200) + ->assertSee('Media Manager'); +}); diff --git a/tests/src/Main/Http/Controllers/ThemesTest.php b/tests/src/Main/Http/Controllers/ThemesTest.php new file mode 100644 index 00000000..687ed754 --- /dev/null +++ b/tests/src/Main/Http/Controllers/ThemesTest.php @@ -0,0 +1,141 @@ +get(route('igniter.main.themes')) + ->assertStatus(200); +}); + +it('loads customise theme page', function() { + ThemeModel::create(['code' => 'tests-theme', 'name' => 'Tests Theme']); + $theme = resolve(ThemeManager::class)->findTheme('tests-theme'); + $theme->locked = true; + + actingAsSuperUser() + ->get(route('igniter.main.themes', ['slug' => 'edit/tests-theme'])) + ->assertStatus(200); +}); + +it('loads edit theme template file page', function() { + ThemeModel::create(['code' => 'tests-theme', 'name' => 'Tests Theme', 'data' => ['field1' => 'value1']]); + $theme = resolve(ThemeManager::class)->findTheme('tests-theme'); + $theme->locked = true; + + actingAsSuperUser() + ->get(route('igniter.main.themes', ['slug' => 'source/tests-theme'])) + ->assertStatus(200); +}); + +it('redirects when unable to delete an active theme', function() { + ThemeModel::create(['code' => 'tests-theme', 'name' => 'Tests Theme', 'status' => 1, 'is_default' => 1]); + + actingAsSuperUser() + ->get(route('igniter.main.themes', ['slug' => 'delete/tests-theme'])) + ->assertRedirect(); +}); + +it('deletes and redirect when theme exists in database and not filesystem', function() { + app()->instance(ThemeManager::class, $themeManager = mock(ThemeManager::class)); + $themeManager->shouldReceive('findTheme')->andReturnNull(); + ThemeModel::create(['code' => 'no-theme', 'name' => 'Tests Theme']); + + actingAsSuperUser() + ->get(route('igniter.main.themes', ['slug' => 'delete/no-theme'])) + ->assertRedirect(); +}); + +it('loads delete theme page', function() { + ThemeModel::create(['code' => 'tests-theme', 'name' => 'Tests Theme']); + + actingAsSuperUser() + ->get(route('igniter.main.themes', ['slug' => 'delete/tests-theme'])) + ->assertStatus(200); +}); + +it('sets default theme correctly', function() { + ThemeModel::create(['code' => 'tests-theme', 'name' => 'Tests Theme', 'status' => 1]); + + actingAsSuperUser() + ->post(route('igniter.main.themes'), ['code' => 'tests-theme'], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSetDefault', + ]) + ->assertSee('X_IGNITER_REDIRECT'); + + expect(ThemeModel::where('is_default', 1)->first()->code)->toBe('tests-theme'); +}); + +it('resets theme customisation settings', function() { + config(['igniter-system.buildThemeAssetsBundle' => true]); + ThemeModel::create(['code' => 'tests-theme', 'name' => 'Tests Theme', 'data' => ['field1' => 'value1']]); + Assets::partialMock()->shouldReceive('buildBundles')->once(); + + actingAsSuperUser() + ->post(route('igniter.main.themes', ['slug' => 'edit/tests-theme']), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onReset', + ]) + ->assertSee('X_IGNITER_REDIRECT'); + + expect(ThemeModel::where('code', 'tests-theme')->first()->data)->toBe([]); +}); + +it('edits theme template file content', function() { + ThemeModel::create(['code' => 'tests-theme', 'name' => 'Tests Theme', 'status' => 1]); + + actingAsSuperUser() + ->post(route('igniter.main.themes', ['slug' => 'source/tests-theme']), ['markup' => 'new content'], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]) + ->assertSee('X_IGNITER_REDIRECT'); +}); + +it('creates child theme', function() { + ThemeModel::create(['code' => 'tests-theme', 'name' => 'Tests Theme', 'status' => 1]); + $childTheme = ThemeModel::create(['code' => 'child-theme', 'name' => 'Child Theme', 'status' => 1]); + app()->instance(ThemeManager::class, $themeManager = mock(ThemeManager::class)); + $themeManager->shouldReceive('createChildTheme')->andReturn($childTheme); + $themeManager->shouldReceive('paths')->andReturn(['child-theme' => testThemePath()]); + $themeManager->shouldReceive('findTheme')->andReturn(new Theme(__DIR__)); + + actingAsSuperUser() + ->post(route('igniter.main.themes', ['slug' => 'source/tests-theme']), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onCreateChild', + ]) + ->assertSee('X_IGNITER_REDIRECT'); + + expect(ThemeModel::where('is_default', 1)->first()->code)->toBe('child-theme'); +}); + +it('redirects when deleting an active theme', function() { + ThemeModel::create(['code' => 'tests-theme', 'name' => 'Tests Theme', 'status' => 1, 'is_default' => 1]); + + actingAsSuperUser() + ->post(route('igniter.main.themes', ['slug' => 'delete/tests-theme']), ['delete_data' => 1], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]) + ->assertSee('X_IGNITER_REDIRECT'); +}); + +it('deletes theme correctly', function() { + app()->instance(ThemeManager::class, $themeManager = mock(ThemeManager::class)); + $themeManager->shouldReceive('deleteTheme')->once(); + ThemeModel::create(['code' => 'tests-theme', 'name' => 'Tests Theme']); + + actingAsSuperUser() + ->post(route('igniter.main.themes', ['slug' => 'delete/tests-theme']), ['delete_data' => 1], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]) + ->assertSee('X_IGNITER_REDIRECT'); +}); diff --git a/tests/src/Main/Http/Middleware/CheckMaintenanceTest.php b/tests/src/Main/Http/Middleware/CheckMaintenanceTest.php new file mode 100644 index 00000000..347efe3d --- /dev/null +++ b/tests/src/Main/Http/Middleware/CheckMaintenanceTest.php @@ -0,0 +1,44 @@ +set(['maintenance_mode' => false]); + + expect((new CheckMaintenance())->handle($request, fn($req) => 'next'))->toBe('next'); +}); + +it('allows request to proceed when in admin area', function() { + $request = Request::create('/admin/some-url', 'GET'); + Igniter::partialMock()->shouldReceive('runningInAdmin')->andReturn(true); + setting()->set(['maintenance_mode' => true]); + + expect((new CheckMaintenance())->handle($request, fn($req) => 'next'))->toBe('next'); +}); + +it('allows request to proceed when admin is logged in', function() { + $request = Request::create('/some-url', 'GET'); + Igniter::partialMock()->shouldReceive('runningInAdmin')->andReturn(false); + AdminAuth::shouldReceive('isLogged')->andReturn(true); + setting()->set(['maintenance_mode' => true]); + + expect((new CheckMaintenance())->handle($request, fn($req) => 'next'))->toBe('next'); +}); + +it('returns maintenance response when in maintenance mode and not admin', function() { + $request = Request::create('/some-url', 'GET'); + Igniter::partialMock()->shouldReceive('runningInAdmin')->andReturn(false); + AdminAuth::shouldReceive('isLogged')->andReturn(false); + setting()->set(['maintenance_mode' => true, 'maintenance_message' => 'Maintenance mode']); + + $response = (new CheckMaintenance())->handle($request, fn($req) => 'next'); + + expect($response->getStatusCode())->toBe(503) + ->and($response->getContent())->toContain('Maintenance mode'); +}); diff --git a/tests/src/Main/Http/Requests/ThemeRequestTest.php b/tests/src/Main/Http/Requests/ThemeRequestTest.php new file mode 100644 index 00000000..40eef11d --- /dev/null +++ b/tests/src/Main/Http/Requests/ThemeRequestTest.php @@ -0,0 +1,55 @@ +themeRequest = new ThemeRequest(); + $this->themeRequest->setRouteResolver(fn() => Route::get('/users/{user}', [ThemeTestController::class, 'index'])); +}); + +it('returns empty attributes when form context is not edit', function() { + ThemeTestController::$context = 'create'; + + $attributes = $this->themeRequest->attributes(); + + expect($attributes)->toBe([]); +}); + +it('returns correct attribute labels', function() { + ThemeTestController::$context = 'edit'; + $attributes = $this->themeRequest->attributes(); + + expect($attributes)->toBe([ + 'theme_website' => lang('igniter.main::default.theme_website_label'), + 'theme_background' => lang('igniter.main::default.theme_background_label'), + ]); +}); + +it('returns empty rules when form context is not edit', function() { + ThemeTestController::$context = 'create'; + + $rules = $this->themeRequest->rules(); + + expect($rules)->toBe([]); +}); + +it('returns correct validation rules', function() { + ThemeTestController::$context = 'edit'; + $rules = $this->themeRequest->rules(); + + expect($rules)->toBe([ + 'theme_website' => 'nullable|string', + 'theme_background' => 'required|string', + 'social.*.class' => 'required', + ]); +}); + +it('returns validation data correctly', function() { + ThemeTestController::$context = 'edit'; + + expect($this->themeRequest->validationData())->toBeArray(); +}); diff --git a/tests/src/Main/Models/ThemeTest.php b/tests/src/Main/Models/ThemeTest.php new file mode 100644 index 00000000..b5e39dd8 --- /dev/null +++ b/tests/src/Main/Models/ThemeTest.php @@ -0,0 +1,144 @@ + 'test-theme']); + $theme = Theme::forTheme($themeData); + expect($theme->code)->toBe('test-theme'); +}); + +it('returns false when onboarding is not complete', function() { + Theme::truncate(); + expect(Theme::onboardingIsComplete())->toBeFalse(); +}); + +it('returns true when onboarding is complete', function() { + $theme = Theme::create(['code' => 'test-theme', 'name' => 'Test Theme', 'data' => ['key' => 'value'], 'status' => 1]); + $theme->makeDefault(); + Theme::clearDefaultModel(); + + expect(Theme::onboardingIsComplete())->toBeTrue(); +}); + +it('returns layout options for the theme', function() { + $themeData = new ThemeData(testThemePath(), ['code' => 'test-theme']); + $theme = Theme::create(['code' => 'test-theme']); + resolve(ThemeManager::class)->themes['test-theme'] = $themeData; + + expect($theme->getLayoutOptions())->toBe([]); +}); + +it('returns component options', function() { + expect(Theme::getComponentOptions())->toHaveKeys([ + 'testComponent', + 'testComponentWithLifecycle', + 'test::livewire-component', + ]); +}); + +it('returns default theme name when not set', function() { + $theme = new Theme(['code' => 'test-theme', 'name' => 'Test Theme']); + expect($theme->name)->toBe('Test Theme'); +}); + +it('returns theme description', function() { + $theme = new Theme(['code' => 'test-theme', 'description' => 'Test Description']); + expect($theme->description)->toBe('Test Description'); +}); + +it('returns theme locked mode', function() { + $theme = new Theme(['code' => 'test-theme']); + $themeData = new ThemeData(testThemePath(), ['code' => 'test-theme', 'author' => 'Test Author', 'locked' => true]); + resolve(ThemeManager::class)->themes['test-theme'] = $themeData; + + expect($theme->locked)->toBeTrue(); +}); + +it('returns default version when not set', function() { + $theme = new Theme(['code' => 'test-theme']); + expect($theme->version)->toBe('0.1.0'); +}); + +it('returns theme author', function() { + $theme = new Theme(['code' => 'test-theme']); + $themeData = new ThemeData(testThemePath(), ['code' => 'test-theme', 'author' => 'Test Author']); + resolve(ThemeManager::class)->themes['test-theme'] = $themeData; + + expect($theme->author)->toBe('Test Author'); +}); + +it('returns theme screenshot', function() { + $theme = new Theme(['code' => 'test-theme']); + $themeData = new ThemeData(testThemePath(), ['code' => 'test-theme', 'author' => 'Test Author']); + resolve(ThemeManager::class)->themes['test-theme'] = $themeData; + + expect($theme->screenshot)->toBe(''); +}); + +it('returns false when activating non existence theme', function() { + expect(Theme::activateTheme('non-existence'))->toBeFalse(); +}); + +it('returns fields config from theme form config', function() { + $theme = new Theme(['code' => 'test-theme']); + $themeData = new ThemeData(testThemePath(), [ + 'code' => 'test-theme', + 'form' => [ + 'general' => [ + 'title' => 'General', + 'fields' => [ + 'field1' => [ + 'label' => 'Field 1', + ], + ], + ], + ], + ]); + resolve(ThemeManager::class)->themes['test-theme'] = $themeData; + + expect($theme->getFieldsConfig())->toHaveKey('field1') + ->and($theme->getFieldsConfig())->toHaveKey('field1'); +}); + +it('returns field values when data is set', function() { + $theme = new Theme(['data' => ['field1' => 'value1']]); + expect($theme->getFieldValues())->toBe(['field1' => 'value1']); +}); + +it('saves theme customizer attributes', function() { + $theme = Theme::create(['code' => 'test-theme-suffix', 'name' => 'Test Theme']); + $theme->name = 'New Test Theme'; + $theme->background_color = '#ffffff'; + $theme->save(); + $theme = $theme->fresh(); + + expect($theme->data)->toHaveKey('background_color', '#ffffff'); +})->skip(); + +it('activates a theme and installs required extensions', function() { + $theme = Theme::create(['code' => 'test-theme', 'name' => 'Test Theme', 'status' => 1]); + $themeData = mock(ThemeData::class); + $themeData->shouldReceive('listRequires')->andReturn(['extension1' => '1.0.0']); + $themeData->shouldReceive('hasParent')->andReturnFalse(); + $themeData->shouldReceive('getMetaPath')->andReturn('/path/to/theme/meta'); + resolve(ThemeManager::class)->themes['test-theme'] = $themeData; + app()->instance(ExtensionManager::class, $extensionManager = mock(ExtensionManager::class)); + $extensionManager->shouldReceive('getExtensions')->andReturn([]); + $extensionManager->shouldReceive('hasExtension')->with('extension1')->andReturn(true); + $extensionManager->shouldReceive('installExtension')->with('extension1'); + + expect(Theme::activateTheme('test-theme')->getKey())->toBe($theme->getKey()); +}); + +it('generates unique theme code', function() { + Theme::create(['code' => 'test-theme-suffix', 'name' => 'Test Theme']); + + $themeCode = Theme::generateUniqueCode('test-theme', 'suffix'); + expect($themeCode)->toStartWith('test-theme-'); +}); diff --git a/tests/src/System/Actions/ModelActionTest.php b/tests/src/System/Actions/ModelActionTest.php new file mode 100644 index 00000000..993aba08 --- /dev/null +++ b/tests/src/System/Actions/ModelActionTest.php @@ -0,0 +1,29 @@ +requiredProperty)->toBe('value'); +}); + +it('throws exception if required property is missing', function() { + expect(fn() => new class(new Status) extends ModelAction + { + protected array $requiredProperties = ['requiredProperty']; + })->toThrow(\LogicException::class, sprintf( + 'Class %s must define property %s used by %s', + Status::class, 'requiredProperty', ModelAction::class, + )); +}); + +it('initializes with null model', function() { + expect(new ModelAction(null))->not->toBeNull(); +}); diff --git a/tests/src/System/Actions/SettingsModelTest.php b/tests/src/System/Actions/SettingsModelTest.php new file mode 100644 index 00000000..2112d620 --- /dev/null +++ b/tests/src/System/Actions/SettingsModelTest.php @@ -0,0 +1,91 @@ +settingsModel = new class extends Model + { + public array $implement = [SettingsModel::class]; + public string $settingsCode = 'test_settings'; + public string $settingsFieldsConfig = 'igniter.tests::/models/test_settings'; + + public function getMutatedKeyAttribute() + { + return 'mutated_value'; + } + }; +}); + +it('resets settings to defaults by deleting the record', function() { + $this->settingsModel->set('key', 'value'); + + $this->settingsModel->resetDefault(); + + expect($this->settingsModel->get('key'))->toBeNull(); +}); + +it('returns true if model is configured', function() { + Igniter::shouldReceive('hasDatabase')->andReturn(true); + $this->settingsModel->create([ + 'item' => 'test_settings', + 'data' => ['key' => 'value'], + ]); + + expect($this->settingsModel->isConfigured())->toBeTrue(); +}); + +it('returns false if database is not available', function() { + Igniter::shouldReceive('hasDatabase')->andReturn(false); + + expect($this->settingsModel->isConfigured())->toBeFalse(); +}); + +it('sets single & multiple key value pair correctly', function() { + $this->settingsModel->set('key', 'value'); + $this->settingsModel->set(['key1' => 'value1', 'key2' => 'value2']); + + expect($this->settingsModel->get('key'))->toBe('value') + ->and($this->settingsModel->get('key1'))->toBe('value1') + ->and($this->settingsModel->get('key2'))->toBe('value2'); +}); + +it('returns value if key exists', function() { + $this->settingsModel->set(['key' => 'value']); + $this->settingsModel->afterModelFetch(); + + expect($this->settingsModel->get('key'))->toBe('value'); +}); + +it('returns default value if key does not exist in fieldValues', function() { + $result = $this->settingsModel->get('nonexistent_key', 'default_value'); + + expect($result)->toBe('default_value'); +}); + +it('returns value from model attribute if get mutator exists', function() { + $result = $this->settingsModel->get('mutated_key'); + + expect($result)->toBe('mutated_value'); +}); + +it('loads and returns fieldConfig', function() { + $this->settingsModel->getFieldConfig(); // test cache for code coverage + $result = $this->settingsModel->getFieldConfig(); + + expect($result)->toBe([ + 'toolbar' => [], + 'fields' => [ + 'name' => [ + 'label' => 'Name', + 'type' => 'text', + 'span' => 'left', + ], + ], + ]); + + SettingsModel::clearInternalCache(); +}); diff --git a/tests/src/System/Classes/BaseComponentTest.php b/tests/src/System/Classes/BaseComponentTest.php new file mode 100644 index 00000000..7767ba97 --- /dev/null +++ b/tests/src/System/Classes/BaseComponentTest.php @@ -0,0 +1,110 @@ +setAlias('testAlias'); + + expect($baseComponent->getProperties())->toBeArray() + ->and($baseComponent->getPath())->toContain('igniter.tests::views/_components/testcomponent') + ->and($baseComponent->initialize())->toBeNull() + ->and($baseComponent->onRun())->toBeNull() + ->and($baseComponent->onRender())->toBeNull() + ->and($baseComponent->getEventHandler('onTest'))->toBe('testAlias::onTest') + ->and($baseComponent->isHidden())->toBeFalse(); +}); + +it('renders component partial', function() { + $baseComponent = createBaseComponent(); + $baseComponent->setAlias('testAlias'); + + expect(fn() => $baseComponent->renderPartial('test')) + ->toThrow(\Exception::class, 'The partial [test] is not found.'); +})->note('Fails because of the missing component partial file'); + +it('runs component event handler', function() { + Event::fake([ + 'main.component.afterRunEventHandler', + ]); + $baseComponent = createBaseComponent(); + $baseComponent->setAlias('testAlias'); + + expect($baseComponent->runEventHandler('onRun'))->toBeNull(); + + Event::assertDispatched('main.component.afterRunEventHandler'); +}); + +it('sets and gets alias', function() { + $baseComponent = createBaseComponent(); + + $baseComponent->setAlias('testAlias'); + expect($baseComponent->getAlias())->toBe('testAlias'); +}); + +it('resolves component', function() { + $baseComponent = createBaseComponent(); + $component = $baseComponent::resolve('testComponent'); + + expect($component)->toBeInstanceOf(BaseComponent::class) + ->and($component->name)->toBe('testComponent'); +}); + +it('returns component parameter', function() { + $route = new Route('GET', '/', []); + request()->setRouteResolver(fn() => $route); + $route->bind(request()); + $route->setParameter('location', 'value'); + $baseComponent = createBaseComponent(); + + expect($baseComponent->param('location'))->toBe('value') + ->and($baseComponent->param('invalid'))->toBeNull(); +}); + +it('handles dynamic method calls', function() { + $controller = new class extends MainController + { + public function testMethod() + { + return 'test'; + } + }; + $baseComponent = createBaseComponent($controller); + + expect($baseComponent->testMethod())->toBe('test'); +}); + +it('throws exception for undefined method calls', function() { + $baseComponent = createBaseComponent(); + + expect(fn() => $baseComponent->undefinedMethod())->toThrow(BadMethodCallException::class); +}); + +it('converts to string', function() { + $component = new class extends BaseComponent + { + public function initialize() {} + }; + + $component->setAlias('stringAlias'); + expect((string)$component)->toBe('stringAlias'); +}); diff --git a/tests/src/System/Classes/BaseExtensionTest.php b/tests/src/System/Classes/BaseExtensionTest.php new file mode 100644 index 00000000..a7f2478c --- /dev/null +++ b/tests/src/System/Classes/BaseExtensionTest.php @@ -0,0 +1,70 @@ + 'TestNamespace', 'code' => 'test.code']; + $extension->extensionMeta($config); + + expect($extension->extensionMeta())->toBe($config); +}); + +it('returns extension meta from file if config not set', function() { + $extension = createExtension(); + $config = SystemHelper::extensionConfigFromFile(dirname(File::fromClass(get_class($extension)))); + + expect($extension->extensionMeta())->toBe($config); +}); + +it('disables extension if disabled property is true', function() { + $extension = createExtension(); + $extension->disabled = true; + + expect($extension->bootingExtension())->toBeNull() + ->and(app()->call([$extension, 'boot']))->toBeNull(); +}); + +it('loads resources if directory exists', function() { + $extension = createExtension(); + File::partialMock()->shouldReceive('isDirectory')->with(Mockery::on(function($path) { + return str_contains($path, '/resources'); + }))->andReturn(true); + + Igniter::shouldReceive('loadResourcesFrom')->once(); + $extension->bootingExtension(); +}); + +it('loads migrations if directory exists', function() { + $extension = createExtension(); + File::partialMock()->shouldReceive('isDirectory')->with(Mockery::on(function($path) { + return str_contains($path, '/database/migrations'); + }))->andReturn(true); + + Igniter::shouldReceive('loadMigrationsFrom')->once(); + $extension->bootingExtension(); +}); + +it('defines registration methods', function() { + $extension = createExtension(); + + expect($extension->registerPaymentGateways())->toBeArray() + ->and($extension->registerNavigation())->toBeArray() + ->and($extension->registerSchedule(new Schedule()))->toBeNull() + ->and($extension->registerDashboardWidgets())->toBeArray() + ->and($extension->registerFormWidgets())->toBeArray() + ->and($extension->registerValidationRules())->toBeArray() + ->and($extension->registerSettings())->toBeArray(); +}); diff --git a/tests/src/System/Classes/ComponentManagerTest.php b/tests/src/System/Classes/ComponentManagerTest.php new file mode 100644 index 00000000..b2b4883b --- /dev/null +++ b/tests/src/System/Classes/ComponentManagerTest.php @@ -0,0 +1,169 @@ +registerComponents([ + TestComponent::class, + TestComponent::class => 'test2.component', + TestBladeComponent::class, + TestLivewireComponent::class, + ]); + + expect($manager->listComponentObjects())->toBeGreaterThanOrEqual(4) // Test caching + ->and($manager->listComponentObjects())->toHaveKeys([ + 'testComponent', 'test2.component', 'test::blade-component', 'test::livewire-component', + ]); +}); + +it('registers a component with valid definition', function() { + $manager = resolve(ComponentManager::class); + $manager->registerComponent(TestComponent::class, ['code' => 'testComponent', 'name' => 'Test Component']); + + $component = $manager->findComponent('testComponent'); + + expect($component)->toBeArray() + ->and($component['code'])->toBe('testComponent') + ->and($component['name'])->toBe('Test Component'); +}); + +it('throws exception when making unregistered component', function() { + $manager = resolve(ComponentManager::class); + $manager->registerComponents([ + 'UnregisteredComponent', + ]); + + expect(fn() => $manager->makeComponent('UnregisteredComponent')) + ->toThrow(SystemException::class, sprintf('Component "%s" is not registered.', 'UnregisteredComponent')); +}); + +it('throws an exception when component class does not exists', function() { + $manager = resolve(ComponentManager::class); + $manager->registerComponents([ + 'NonExistentComponent' => 'component', + ]); + + expect(fn() => $manager->makeComponent(['component', 'alias'])) + ->toThrow(SystemException::class, sprintf('Component class "%s" not found.', 'NonExistentComponent')); +}); + +it('throws an exception when component class is invalid', function() { + $component = new class extends Component + { + public static function componentMeta() + { + return [ + 'code' => 'test3.component', + 'name' => 'Test Component', + ]; + } + + public function render() + { + return ''; + } + }; + $manager = resolve(ComponentManager::class); + $manager->registerComponents([ + $component::class => 'component', + ]); + + expect(fn() => $manager->makeComponent(['component', 'alias'])) + ->toThrow(sprintf('Component class "%s" is not a valid component.', $component::class)); +}); + +it('returns null when resolving unregistered component', function() { + $manager = resolve(ComponentManager::class); + + expect($manager->resolve('unregistered.component'))->toBeNull(); +}); + +it('returns true if component is registered', function() { + $manager = resolve(ComponentManager::class); + $manager->registerComponent(TestComponent::class, ['code' => 'testComponent']); + + expect($manager->hasComponent('testComponent'))->toBeTrue(); +}); + +it('returns false if component is not registered', function() { + $manager = resolve(ComponentManager::class); + + expect($manager->hasComponent('unregistered.component'))->toBeFalse() + ->and($manager->findComponent('unregistered.component'))->toBeNull(); +}); + +it('returns component code by class name', function() { + $manager = resolve(ComponentManager::class); + $manager->registerComponent(TestComponent::class, ['code' => 'testComponent']); + + expect($manager->findComponentCodeByClass(TestComponent::class))->toBe('testComponent') + ->and($manager->getCodeAlias('component alias'))->toBe(['component', 'alias']) + ->and($manager->getCodeAlias('component'))->toBe(['component', 'component']); +}); + +it('returns null if class name is not registered', function() { + $manager = resolve(ComponentManager::class); + + expect($manager->findComponentCodeByClass('UnregisteredComponent'))->toBeNull(); +}); + +it('checks if component configurable', function() { + $manager = resolve(ComponentManager::class); + $manager->registerComponent(TestBladeComponent::class, ['code' => 'testComponent']); + + expect($manager->isConfigurableComponent('testComponent'))->toBeTrue() + ->and($manager->isConfigurableComponent('nonexistence.component'))->toBeFalse(); +}); + +it('returns component property configuration', function() { + $component = mock(BaseComponent::class)->makePartial(); + $component->shouldReceive('defineProperties')->andReturn([ + 'property1' => ['type' => 'text', 'label' => 'Property 1'], + 'property2' => ['type' => 'select', 'label' => 'Property 2'], + 'property3' => ['type' => 'radiotoggle', 'label' => 'Property 3'], + ]); + + $manager = resolve(ComponentManager::class); + $config = $manager->getComponentPropertyConfig($component); + + expect($config)->toBeArray() + ->and($config['property1']['type'])->toBe('text') + ->and($config['property1']['label'])->toBe('Property 1'); +}); + +it('returns component property values', function() { + $component = mock(BaseComponent::class)->makePartial(); + $component->shouldReceive('defineProperties')->andReturn([ + 'property1' => ['type' => 'text'], + ]); + $component->shouldReceive('property')->with('property1')->andReturn('value1'); + + $manager = resolve(ComponentManager::class); + $values = $manager->getComponentPropertyValues($component); + + expect($values)->toBeArray() + ->and($values['property1'])->toBe('value1'); +}); + +it('returns component property rules', function() { + $component = mock(BaseComponent::class)->makePartial(); + $component->shouldReceive('defineProperties')->andReturn([ + 'property1' => ['validationRule' => 'required', 'label' => 'Property 1'], + ]); + + $manager = resolve(ComponentManager::class); + [$rules, $attributes] = $manager->getComponentPropertyRules($component); + + expect($rules)->toBeArray() + ->and($rules['property1'])->toBe('required') + ->and($attributes['property1'])->toBe('Property 1'); +}); diff --git a/tests/src/System/Classes/ComposerManagerTest.php b/tests/src/System/Classes/ComposerManagerTest.php deleted file mode 100644 index 74136549..00000000 --- a/tests/src/System/Classes/ComposerManagerTest.php +++ /dev/null @@ -1,19 +0,0 @@ -skip(); - -it('removes repository config from composer.json repositories config', function() {})->skip(); - -it('checks composer.json repositories config has a hostname', function() {})->skip(); - -it('loads required repository and auth config', function() {})->skip(); - -it('requires core package', function() {})->skip(); - -it('requires package', function() {})->skip(); - -it('updates package', function() {})->skip(); - -it('removes package', function() {})->skip(); diff --git a/tests/src/System/Classes/ControllerActionTest.php b/tests/src/System/Classes/ControllerActionTest.php new file mode 100644 index 00000000..06b58cbe --- /dev/null +++ b/tests/src/System/Classes/ControllerActionTest.php @@ -0,0 +1,60 @@ +configPath = ['/path/to/config']; + $controller->partialPath = ['/path/to/partials']; + + $action = new ControllerAction($controller); + + expect($action->configPath)->toBe(['/path/to/config']) + ->and($action->partialPath)->toBe(['/path/to/partials']); +}); + +it('throws exception if required property is missing', function() { + $controller = resolve(TestController::class); + + expect(fn() => new class($controller) extends ControllerAction + { + protected array $requiredProperties = ['missingProperty']; + })->toThrow(\LogicException::class); +}); + +it('sets and gets config correctly', function() { + $controller = resolve(TestController::class); + $action = new ControllerAction($controller); + + $config = [ + 'key' => 'value', + 'nested' => ['key' => 'nested-value'], + ]; + $action->setConfig($config); + + expect($action->getConfig())->toBe($config) + ->and($action->getConfig('key'))->toBe('value') + ->and($action->getConfig('nested[key]'))->toBe('nested-value') + ->and($action->getConfig('nonexistent', 'default'))->toBe('default') + ->and($action->getConfig('nested[nonexistent]', 'default'))->toBe('default'); +}); + +it('hides action methods correctly', function() { + $controller = resolve(TestController::class); + $controller->hiddenActions = []; + + $action = new class($controller) extends ControllerAction + { + public function testHideAction($action) + { + $this->hideAction($action); + } + }; + + $action->testHideAction('someMethod'); + + expect($controller->hiddenActions)->toContain('someMethod'); +}); diff --git a/tests/src/System/Classes/ExtensionManagerTest.php b/tests/src/System/Classes/ExtensionManagerTest.php index 870e15a7..957298a6 100644 --- a/tests/src/System/Classes/ExtensionManagerTest.php +++ b/tests/src/System/Classes/ExtensionManagerTest.php @@ -2,10 +2,332 @@ namespace Igniter\Tests\System\Classes; -it('registers an extension', function() {})->skip(); +use Igniter\Flame\Composer\Manager as ComposerManager; +use Igniter\Flame\Exception\SystemException; +use Igniter\Flame\Support\Facades\File; +use Igniter\System\Classes\BaseExtension; +use Igniter\System\Classes\ExtensionManager; +use Igniter\System\Classes\PackageManifest; +use Igniter\System\Classes\UpdateManager; +use Igniter\System\Models\Extension; +use LogicException; +use ZipArchive; -it('loads an extension from path', function() {})->skip(); +it('returns correct path for extension with folder', function() { + $manager = resolve(ExtensionManager::class); -it('loads an extension from package manifest', function() {})->skip(); + expect($manager->path('igniter.user', 'subfolder'))->toEndWith('/tastyigniter/ti-ext-user/subfolder') + ->and($manager->path('test-extension'))->toBe('/'); +}); -it('resolves an extension', function() {})->skip(); +it('returns list of all extensions', function() { + $manager = resolve(ExtensionManager::class); + + $extensions = $manager->listExtensions(); + expect($extensions)->toContain('igniter.user'); +}); + +it('returns extension lookup folders with added directory', function() { + File::shouldReceive('isDirectory')->andReturnTrue(); + File::shouldReceive('glob')->andReturn([ + '/path/to/extensions/igniter/user/composer.json', + '/path/to/extensions/igniter/blog/composer.json', + ]); + $manager = resolve(ExtensionManager::class); + ExtensionManager::addDirectory('/path/to/extensions'); + + $folders = $manager->folders(); + expect($folders)->toContain( + '/path/to/extensions/igniter/user', + '/path/to/extensions/igniter/blog', + ) + ->and($manager->namespaces())->toHaveKeys(['igniter\user']); +}); + +it('loads extensions correctly', function() { + $manager = resolve(ExtensionManager::class); + $manager->loadExtensions(); + + expect($manager->getExtensions())->toBeArray(); +}); + +it('throws exception if extension namespace is missing', function() { + $manager = resolve(ExtensionManager::class); + + expect(fn() => $manager->loadExtension('/invalid/path'))->toThrow(SystemException::class); +}); + +it('returns existing extension if already loaded', function() { + $manager = resolve(ExtensionManager::class); + $path = __DIR__.'/../../Fixtures/extension'; + + expect($manager->loadExtension($path))->toBe($manager->loadExtension($path)); +}); + +it('loads extension and sets PSR-4 autoloading', function() { + $composerManager = mock(ComposerManager::class)->makePartial(); + $composerManager->shouldReceive('getLoader')->andReturnSelf(); + $composerManager->shouldReceive('getPrefixesPsr4')->andReturn([]); + $composerManager->shouldReceive('setPsr4')->andReturnTrue(); + $manager = resolve(ExtensionManager::class, [ + 'composerManager' => $composerManager, + 'packageManifest' => resolve(PackageManifest::class), + ]); + + $path = __DIR__.'/../../Fixtures/extension'; + expect($manager->loadExtension($path))->toBeInstanceOf(BaseExtension::class); +}); + +it('throws exception when extension config validation fails', function() { + File::shouldReceive('isDirectory')->andReturnTrue(); + File::shouldReceive('exists')->andReturnFalse(); + File::shouldReceive('glob')->andReturn([ + '/path/to/extensions/igniter/user/composer.json', + '/path/to/extensions/igniter/blog/composer.json', + ]); + $manager = resolve(ExtensionManager::class); + ExtensionManager::addDirectory('/path/to/extensions'); + + expect(fn() => $manager->loadExtensions()) + ->toThrow(SystemException::class); +}); + +it('returns null for invalid extension name', function() { + $manager = resolve(ExtensionManager::class); + $name = $manager->checkName('invalid name'); + expect($name)->toBeNull(); +}); + +it('returns identifier from namespace', function() { + $manager = resolve(ExtensionManager::class); + $identifier = $manager->getIdentifier('Test\\Namespace'); + expect($identifier)->toBe('test.namespace'); +}); + +it('returns correct extension path', function() { + $manager = resolve(ExtensionManager::class); + + $path = $manager->getExtensionPath('igniter.user', '/subfolder'); + + expect($path)->toEndWith('/tastyigniter/ti-ext-user/subfolder') + ->and($manager->getNamePath('test.extension'))->toEndWith('test/extension') + ->and($manager->hasVendor('test.extension'))->toBeFalse(); +}); + +it('updates installed extensions correctly', function() { + $manager = resolve(ExtensionManager::class); + + $result = $manager->updateInstalledExtensions('igniter.user', false); + expect($result)->toBeTrue() + ->and($manager->isDisabled('igniter.user'))->toBeTrue(); +}); + +it('removes extension correctly', function() { + File::shouldReceive('isDirectory')->andReturnTrue(); + File::shouldReceive('deleteDirectory')->andReturnTrue(); + File::shouldReceive('directories')->andReturn([]); + + $manager = resolve(ExtensionManager::class); + + expect($manager->removeExtension('igniter.user'))->toBeTrue(); +}); + +it('throws exception if extension directory is not found', function() { + $manager = resolve(ExtensionManager::class); + + expect(fn() => $manager->resolveExtension('test.identifier', '/invalid/path', 'Test\\Namespace\\Extension')) + ->toThrow(SystemException::class, 'Extension directory not found: /invalid/path'); +}); + +it('throws exception if extension class is missing', function() { + $manager = resolve(ExtensionManager::class); + + expect(fn() => $manager->resolveExtension('test.identifier', __DIR__, null)) + ->toThrow(LogicException::class, "Missing Extension class '' in 'test.identifier', create the Extension class to override extensionMeta() method."); +}); + +it('throws exception if extension class does not exist', function() { + $manager = resolve(ExtensionManager::class); + + expect(fn() => $manager->resolveExtension('test.identifier', __DIR__, 'NonExistentClass')) + ->toThrow(LogicException::class, "Missing Extension class 'NonExistentClass' in 'test.identifier', create the Extension class to override extensionMeta() method."); +}); + +it('throws exception if extension class does not extend BaseExtension', function() { + $manager = resolve(ExtensionManager::class); + + expect(fn() => $manager->resolveExtension('test.identifier', __DIR__, \stdClass::class)) + ->toThrow(LogicException::class, "Extension class 'stdClass' must extend 'Igniter\System\Classes\BaseExtension'."); +}); + +it('checks if extension is required', function() { + $manager = resolve(ExtensionManager::class); + + expect($manager->isRequired('test.extension'))->toBeFalse() + ->and($manager->isRequired('igniter.api'))->toBeTrue() + ->and($manager->isRequired('igniter.automation'))->toBeTrue() + ->and($manager->isRequired('igniter.broadcast'))->toBeTrue() + ->and($manager->isRequired('igniter.cart'))->toBeTrue() + ->and($manager->isRequired('igniter.local'))->toBeTrue() + ->and($manager->isRequired('igniter.payregister'))->toBeTrue() + ->and($manager->isRequired('igniter.reservation'))->toBeTrue() + ->and($manager->isRequired('igniter.user'))->toBeTrue() + ->and($manager->isRequired('igniter.orange'))->toBeTrue(); +}); + +it('returns cached registration method values if available', function() { + $manager = resolve(ExtensionManager::class); + + expect($manager->getRegistrationMethodValues('registerPermissions')) + ->toBe($manager->getRegistrationMethodValues('registerPermissions')); +}); + +it('extracts extension zip folder correctly', function() { + $manager = resolve(ExtensionManager::class); + $zipPath = '/path/to/valid/extension.zip'; + $zip = mock(ZipArchive::class); + $zip->shouldReceive('open')->with($zipPath)->andReturnTrue(); + $zip->shouldReceive('getNameIndex')->with(0)->andReturn('/path/to/valid/extension/'); + $zip->shouldReceive('locateName')->with('/path/to/valid/extension/Extension.php')->andReturnTrue(); + $zip->shouldReceive('getFromName')->andReturn(json_encode(['code' => 'valid.extension'])); + $zip->shouldReceive('extractTo')->withArgs(fn($path) => ends_with($path, '/extensions/valid/extension'))->andReturnTrue(); + $zip->shouldReceive('close')->andReturnTrue(); + app()->instance(ZipArchive::class, $zip); + File::shouldReceive('exists')->with('/path/to/valid/extension/extension.json')->andReturn(false); + File::shouldReceive('exists')->with('/path/to/valid/extension/composer.json')->andReturn(true); + + $extensionCode = $manager->extractExtension($zipPath); + expect($extensionCode)->toBe('valid.extension'); +}); + +it('extractExtension throws exception if extension name has spaces', function() { + $manager = resolve(ExtensionManager::class); + $zipPath = '/path/to/invalid/extension.zip'; + $zip = mock(ZipArchive::class); + $zip->shouldReceive('open')->with($zipPath)->andReturnTrue(); + $zip->shouldReceive('getNameIndex')->with(0)->andReturn('/path/to/invalid/extens ion/'); + app()->instance(ZipArchive::class, $zip); + File::shouldReceive('exists')->with('/path/to/invalid/extension/extension.json')->andReturn(false); + File::shouldReceive('exists')->with('/path/to/invalid/extension/composer.json')->andReturn(true); + File::shouldReceive('isDirectory')->with('/path/to/invalid/extension')->andReturn(true); + File::shouldReceive('deleteDirectory')->with('/path/to/invalid/extension')->andReturn(true); + + expect(fn() => $manager->extractExtension($zipPath))->toThrow(SystemException::class, 'Extension name can not have spaces.'); +}); + +it('extractExtension throws exception if extension registration class is not found', function() { + $manager = resolve(ExtensionManager::class); + $zipPath = '/path/to/invalid/extension.zip'; + $zip = mock(ZipArchive::class); + $zip->shouldReceive('open')->with($zipPath)->andReturnTrue(); + $zip->shouldReceive('getNameIndex')->with(0)->andReturn('/path/to/invalid/extension/'); + $zip->shouldReceive('locateName')->with('/path/to/invalid/extension/Extension.php')->andReturnFalse(); + app()->instance(ZipArchive::class, $zip); + File::shouldReceive('exists')->with('/path/to/invalid/extension/extension.json')->andReturn(false); + File::shouldReceive('exists')->with('/path/to/invalid/extension/composer.json')->andReturn(true); + File::shouldReceive('isDirectory')->with('/path/to/invalid/extension')->andReturn(true); + File::shouldReceive('deleteDirectory')->with('/path/to/invalid/extension')->andReturn(true); + + expect(fn() => $manager->extractExtension($zipPath))->toThrow(SystemException::class, 'Extension registration class was not found.'); +}); + +it('extractExtension throws exception if extension.json file is found', function() { + $manager = resolve(ExtensionManager::class); + $zipPath = '/path/to/invalid/extension.zip'; + $zip = mock(ZipArchive::class); + $zip->shouldReceive('open')->with($zipPath)->andReturnTrue(); + $zip->shouldReceive('getNameIndex')->with(0)->andReturn('/path/to/invalid/extension/'); + $zip->shouldReceive('locateName')->with('/path/to/invalid/extension/Extension.php')->andReturnTrue(); + app()->instance(ZipArchive::class, $zip); + File::shouldReceive('exists')->with('/path/to/invalid/extension/extension.json')->andReturn(true); + + expect(fn() => $manager->extractExtension($zipPath)) + ->toThrow(SystemException::class, 'extension.json files are no longer supported, please convert to composer.json: /path/to/invalid/extension/extension.json'); +}); + +it('extractExtension throws exception if composer.json file is invalid', function() { + $manager = resolve(ExtensionManager::class); + $zipPath = '/path/to/invalid/extension.zip'; + $zip = mock(ZipArchive::class); + $zip->shouldReceive('open')->with($zipPath)->andReturnTrue(); + $zip->shouldReceive('getNameIndex')->with(0)->andReturn('/path/to/invalid/extension/'); + $zip->shouldReceive('locateName')->with('/path/to/invalid/extension/Extension.php')->andReturnTrue(); + $zip->shouldReceive('getFromName')->andReturn(json_encode([])); + app()->instance(ZipArchive::class, $zip); + File::shouldReceive('exists')->with('/path/to/invalid/extension/extension.json')->andReturn(false); + File::shouldReceive('exists')->with('/path/to/invalid/extension/composer.json')->andReturn(true); + + expect(fn() => $manager->extractExtension($zipPath)) + ->toThrow(SystemException::class, lang('igniter::system.extensions.error_config_no_found')); +}); + +it('installs extension successfully', function() { + $manager = resolve(ExtensionManager::class); + $updateManager = mock(UpdateManager::class); + $updateManager->shouldReceive('migrateExtension')->with('igniter.user'); + app()->instance(UpdateManager::class, $updateManager); + + expect($manager->installExtension('igniter.user'))->toBeTrue(); +}); + +it('installExtension returns false if extension class is not applied', function() { + $manager = resolve(ExtensionManager::class); + + expect($manager->installExtension('test.extension'))->toBeFalse(); +}); + +it('uninstalls extension and purges data', function() { + $manager = resolve(ExtensionManager::class); + $updateManager = mock(UpdateManager::class); + $updateManager->shouldReceive('purgeExtension')->with('test.extension'); + app()->instance(UpdateManager::class, $updateManager); + + expect($manager->uninstallExtension('test.extension', true))->toBeTrue(); +}); + +it('uninstalls extension without purging data', function() { + $manager = resolve(ExtensionManager::class); + + expect($manager->uninstallExtension('test.extension'))->toBeTrue(); +}); + +it('deletes extension and purges data', function() { + $manager = resolve(ExtensionManager::class); + Extension::create(['name' => 'test.extension', 'status' => 1]); + $updateManager = mock(UpdateManager::class); + $updateManager->shouldReceive('purgeExtension')->with('test.extension'); + app()->instance(UpdateManager::class, $updateManager); + $composerManager = mock(ComposerManager::class); + $composerManager->shouldReceive('getPackageName')->with('test.extension')->andReturnNull()->once(); + app()->instance(ComposerManager::class, $composerManager); + File::shouldReceive('isDirectory')->andReturnTrue(); + File::shouldReceive('deleteDirectory')->andReturnTrue(); + File::shouldReceive('directories')->andReturn([]); + + $manager->deleteExtension('test.extension'); +}); + +it('deletes extension without purging data', function() { + $manager = resolve(ExtensionManager::class); + $composerManager = mock(ComposerManager::class); + $composerManager->shouldReceive('getPackageName')->with('test.extension')->andReturnNull()->once(); + app()->instance(ComposerManager::class, $composerManager); + File::shouldReceive('isDirectory')->andReturnTrue(); + File::shouldReceive('deleteDirectory')->andReturnTrue(); + File::shouldReceive('directories')->andReturn([]); + + $manager->deleteExtension('test.extension', false); +}); + +it('uninstalls composer package if package name is found', function() { + $manager = resolve(ExtensionManager::class); + $composerManager = mock(ComposerManager::class); + $composerManager->shouldReceive('getPackageName')->with('test.extension')->andReturn('test/extension'); + $composerManager->shouldReceive('uninstall')->with(['test/extension' => false])->andReturnTrue()->once(); + app()->instance(ComposerManager::class, $composerManager); + File::shouldReceive('isDirectory')->andReturnTrue(); + File::shouldReceive('deleteDirectory')->andReturnTrue(); + File::shouldReceive('directories')->andReturn([]); + + $manager->deleteExtension('test.extension', false); +}); diff --git a/tests/src/System/Classes/FormRequestTest.php b/tests/src/System/Classes/FormRequestTest.php new file mode 100644 index 00000000..71e12334 --- /dev/null +++ b/tests/src/System/Classes/FormRequestTest.php @@ -0,0 +1,22 @@ + 'required', + ]; + } + }; + + $formRequest->setContainer(app()); + + expect(fn() => $formRequest->validateResolved())->toThrow(ValidationException::class); +}); diff --git a/tests/src/System/Classes/HubManagerTest.php b/tests/src/System/Classes/HubManagerTest.php new file mode 100644 index 00000000..e3803777 --- /dev/null +++ b/tests/src/System/Classes/HubManagerTest.php @@ -0,0 +1,191 @@ +hubManager = resolve(HubManager::class); +}); + +it('prepares request correctly', function() { + config([ + 'igniter-system.edgeUpdates' => true, + 'igniter-system.carteKey' => 'carte_key', + ]); + App::partialMock()->shouldReceive('runningInConsole')->andReturnFalse(); + $expectedResponse = ['data' => []]; + Http::fake(['https://api.tastyigniter.com/v2/items' => Http::response($expectedResponse)]); + + $this->hubManager->listItems(['filter' => 'value']); + + Http::assertSent(function(Request $request) { + $postData = $request->data(); + + return $request->hasHeader('Authorization', 'Bearer carte_key') + && $request->hasHeader('X-Igniter-Host') + && $request->hasHeader('X-Igniter-User-Ip') + && $request->hasHeader('X-Igniter-Platform', 'php:'.PHP_VERSION.';version:'.Igniter::version().';url:'.url()->current()) + && $postData['client'] === 'tastyigniter' + && $postData['server'] === base64_encode(serialize([ + 'php' => PHP_VERSION, + 'url' => url()->to('/'), + 'version' => Igniter::version(), + 'host' => gethostname() ?: 'unknown', + ])) + && $postData['edge'] === 1 + && $postData['filter'] === 'value' + && $postData['include'] === 'require'; + }); +}); + +it('lists items with default filter', function() { + $expectedResponse = [ + 'data' => [ + ['code' => 'item1'], + ['code' => 'item2'], + ], + ]; + Http::fake(['https://api.tastyigniter.com/v2/items' => Http::response($expectedResponse)]); + + expect($this->hubManager->listItems())->toBe($expectedResponse); +}); + +it('gets detail of an item', function() { + $expectedResponse = [ + 'code' => 'item1', + ]; + Http::fake(['https://api.tastyigniter.com/v2/item/detail' => Http::response($expectedResponse)]); + + expect($this->hubManager->getDetail('item', ['itemName']))->toBe($expectedResponse); +}); + +it('gets detail of an site', function() { + $expectedResponse = [ + 'code' => 'site1', + ]; + Http::fake(['https://api.tastyigniter.com/v2/site/detail' => Http::response($expectedResponse)]); + + expect($this->hubManager->getDetail('site', ['siteName']))->toBe($expectedResponse); +}); + +it('gets details of multiple items', function() { + $expectedResponse = [ + 'details' => 'info', + ]; + Http::fake(['https://api.tastyigniter.com/v2/type/details' => Http::response($expectedResponse)]); + + expect($this->hubManager->getDetails('type', ['item1', 'item2']))->toBe(['details' => 'info']); +}); + +it('applies items and returns collection', function() { + $expectedResponse = [ + 'data' => [ + [ + 'code' => 'item1', + 'type' => 'core', + 'package' => 'item1/package', + 'name' => 'Package1', + 'version' => '1.0.0', + 'author' => 'Sam', + ], + [ + 'code' => 'item2', + 'type' => 'extension', + 'package' => 'item2/package', + 'name' => 'Package2', + 'version' => '1.0.0', + 'author' => 'Sam', + ], + ], + ]; + Http::fake(['https://api.tastyigniter.com/v2/core/apply' => Http::response($expectedResponse)]); + Igniter::partialMock()->shouldReceive('version')->andReturn('1.0.0'); + + $result = $this->hubManager->applyItems($expectedResponse); + expect($result)->toBeCollection(); +}); + +it('sets carte with key and info', function() { + $this->hubManager->setCarte('key', ['info']); + + expect(setting()->getPref('carte_key'))->toBe('key') + ->and(setting()->getPref('carte_info'))->toBe(['info']); +}); + +it('throws exception if updates endpoint is not configured', function() { + config(['igniter-system.updatesEndpoint' => null]); + + expect(fn() => $this->hubManager->listItems())->toThrow(SystemException::class, 'Updates endpoint not configured'); +}); + +it('throws exception if response is not ok', function() { + Http::fake(['https://api.tastyigniter.com/v2/items' => Http::response([ + 'message' => 'Error message', + 'errors' => [ + 'item' => ['Error message'], + ], + ], 500)]); + + expect(fn() => $this->hubManager->listItems())->toThrow(SystemException::class, 'Error message'); +}); + +it('lists languages with filter', function() { + $expectedResponse = ['language1', 'language2']; + Http::fake(['https://api.tastyigniter.com/v2/languages' => Http::response($expectedResponse)]); + + $result = $this->hubManager->listLanguages(['filter' => 'value']); + expect($result)->toBe($expectedResponse); +}); + +it('gets language by locale', function() { + $expectedResponse = ['language' => 'info']; + Http::fake(['https://api.tastyigniter.com/v2/language/locale' => Http::response($expectedResponse)]); + + $result = $this->hubManager->getLanguage('locale'); + expect($result)->toBe($expectedResponse); +}); + +it('applies language pack', function() { + $expectedResponse = ['result' => 'success']; + Http::fake(['https://api.tastyigniter.com/v2/language/apply' => Http::response($expectedResponse)]); + + $result = $this->hubManager->applyLanguagePack('locale', ['item1', 'item2']); + expect($result)->toBe($expectedResponse); +}); + +it('downloads language pack with eTag', function() { + $expectedResponse = ['download' => 'data']; + Http::fake(['https://api.tastyigniter.com/v2/language/download' => Http::response($expectedResponse, 200, [ + 'TI-ETag' => 'etag', + ])]); + + $result = $this->hubManager->downloadLanguagePack('etag', ['param' => 'value']); + expect($result)->toBe($expectedResponse); +}); + +it('downloads language pack throws exception if ETag mismatch', function() { + Http::fake([ + 'https://api.tastyigniter.com/v2/language/download' => Http::response([ + 'data' => [], + ], 200, [ + 'TI-ETag' => 'different-etag', + ]), + ]); + + expect(fn() => $this->hubManager->downloadLanguagePack('etag', ['param' => 'value'])) + ->toThrow(SystemException::class, 'ETag mismatch, please try again.'); +}); + +it('publishes translations', function() { + $expectedResponse = ['publish' => 'success']; + Http::fake(['https://api.tastyigniter.com/v2/language/upload' => Http::response($expectedResponse)]); + + $result = $this->hubManager->publishTranslations('locale', ['pack1', 'pack2']); + expect($result)->toBe($expectedResponse); +}); diff --git a/tests/src/System/Classes/LanguageManagerTest.php b/tests/src/System/Classes/LanguageManagerTest.php new file mode 100644 index 00000000..a09d3e7d --- /dev/null +++ b/tests/src/System/Classes/LanguageManagerTest.php @@ -0,0 +1,166 @@ +namespaces(); + + expect($result)->toBeArray() + ->and($result)->toHaveKeys(['igniter', 'igniter.api']); +}); + +it('lists enabled languages', function() { + Language::factory()->create(['code' => 'fa', 'status' => 1]); + + $manager = resolve(LanguageManager::class); + $result = $manager->listLanguages(); + + expect($result->isNotEmpty())->toBeTrue(); +}); + +it('returns empty paths if language directory does not exist', function() { + File::shouldReceive('exists')->with(base_path('language'))->andReturn(false); + + $manager = resolve(LanguageManager::class); + + expect($manager->paths())->toBe([]); +}); + +it('returns paths of language directories', function() { + File::shouldReceive('exists')->with(base_path('language'))->andReturn(true); + File::shouldReceive('directories')->with(base_path('language'))->andReturn(['/path/to/lang/en', '/path/to/lang/fr']); + + $manager = resolve(LanguageManager::class); + $result = $manager->paths(); + + expect($result)->toBe(['en' => '/path/to/lang/en', 'fr' => '/path/to/lang/fr']) + ->and($result)->toBe($manager->paths()); +}); + +it('lists locale packages for a given locale', function() { + File::shouldReceive('glob')->andReturn(['/path/to/lang/en/file.php']); + + $manager = resolve(LanguageManager::class); + $result = $manager->listLocalePackages('en'); + + expect($result)->toBeArray() + ->and($result[0])->toBeInstanceOf(\stdClass::class) + ->and($result[0]->code)->toBe('igniter') + ->and($result[0]->name)->toBe('Application') + ->and($result[0]->files)->toBe(['/path/to/lang/en/file.php']); +}); + +it('publishes translations for a language', function() { + $language = Language::factory()->create(['code' => 'fa', 'status' => 1]); + $language->translations()->saveMany([ + Translation::create(['locale' => 'fa', 'group' => 'igniter', 'item' => 'default', 'text' => 'Default']), + ]); + $expectedResponse = ['publish' => 'success']; + Http::fake(['https://api.tastyigniter.com/v2/language/upload' => Http::response($expectedResponse)]); + + $manager = resolve(LanguageManager::class); + expect($manager->publishTranslations($language))->toBe($expectedResponse); +}); + +it('throws exception if language not found when requesting update list', function() { + $manager = resolve(LanguageManager::class); + + expect(fn() => $manager->requestUpdateList('invalid'))->toThrow(ApplicationException::class, 'Language not found'); +}); + +it('returns update list correctly', function() { + $expectedResponse = ['data' => ['result' => 'success']]; + Http::fake(['https://api.tastyigniter.com/v2/language/apply' => Http::response($expectedResponse)]); + $language = Language::factory()->create(['code' => 'en', 'status' => 1]); + + $manager = resolve(LanguageManager::class); + $result = $manager->requestUpdateList($language->code); + + expect($result['items'])->toBe($expectedResponse['data']); +}); + +it('applies language pack and returns data', function() { + $expectedResponse = ['data' => ['result' => 'success']]; + Http::fake(['https://api.tastyigniter.com/v2/language/apply' => Http::response($expectedResponse)]); + Extension::create(['name' => 'Igniter.Api', 'status' => 1]); + + $manager = resolve(LanguageManager::class); + $result = $manager->applyLanguagePack('en'); + + expect($result)->toBe($expectedResponse['data']); +}); + +it('returns languages matching search term', function() { + $expectedResponse = ['data' => [['name' => 'English']]]; + Http::fake(['https://api.tastyigniter.com/v2/languages' => Http::response($expectedResponse)]); + $manager = resolve(LanguageManager::class); + + $expectedResponse['data'][0]['require'] = []; + expect($manager->searchLanguages('term'))->toBe($expectedResponse); +}); + +it('returns language details by locale', function() { + $expectedResponse = ['data' => ['name' => 'English']]; + $manager = resolve(LanguageManager::class); + Http::fake(['https://api.tastyigniter.com/v2/language/en' => Http::response($expectedResponse)]); + + expect($manager->findLanguage('en'))->toBe($expectedResponse['data']); +}); + +it('installs language pack successfully', function() { + $expectedResponse = ['filename.php' => [ + 'text_key' => 'This is a text', + ]]; + Http::fake(['https://api.tastyigniter.com/v2/language/download' => Http::response($expectedResponse, 200, [ + 'TI-ETag' => 'etag', + ])]); + File::shouldReceive('makeDirectory')->once(); + File::shouldReceive('put')->once(); + $manager = resolve(LanguageManager::class); + + expect($manager->installLanguagePack('en', ['name' => 'test', 'hash' => 'etag']))->toBeTrue(); +}); + +it('lists translations for a given package', function() { + $manager = resolve(LanguageManager::class); + $language = Language::factory()->create(['code' => 'en', 'status' => 1]); + + $result = $manager->listTranslations($language, 'igniter.api'); + expect($result->strings->isNotEmpty())->toBeTrue(); +}); + +it('lists translations with filter applied', function() { + $manager = resolve(LanguageManager::class); + $language = Language::factory()->create(['code' => 'en', 'status' => 1]); + + $result = $manager->listTranslations($language, 'igniter.api', 'unchanged'); + expect($result->strings->isEmpty())->toBeTrue(); + + $result = $manager->listTranslations($language, 'igniter.api', 'changed', 'actions.text_index'); + expect($result->strings->isNotEmpty())->toBeTrue(); +}); + +it('lists translations with search term applied', function() { + $manager = resolve(LanguageManager::class); + $language = Language::factory()->create(['code' => 'en', 'status' => 1]); + + $result = $manager->listTranslations($language, 'igniter', null, 'invalid-term'); + expect($result->strings->isEmpty())->toBeTrue(); +}); + +it('returns empty translations if no packages found', function() { + $manager = resolve(LanguageManager::class); + $language = Language::factory()->create(['code' => 'en', 'status' => 1]); + + $result = $manager->listTranslations($language, 'invalid-code', null, 'value1'); + expect($result->strings->isEmpty())->toBeTrue(); +}); diff --git a/tests/src/System/Classes/MailManagerTest.php b/tests/src/System/Classes/MailManagerTest.php index dc1cf5a6..f5611a39 100644 --- a/tests/src/System/Classes/MailManagerTest.php +++ b/tests/src/System/Classes/MailManagerTest.php @@ -3,17 +3,127 @@ namespace Igniter\Tests\System\Classes; use Igniter\System\Classes\MailManager; +use Igniter\System\Models\MailLayout; +use Igniter\System\Models\MailTemplate; +use Illuminate\Support\Facades\Blade; +use Illuminate\Support\HtmlString; it('renders mail templates', function() { $manager = resolve(MailManager::class); $template = $manager->getTemplate('_mail.test_template'); expect((string)$manager->renderTextTemplate($template)) - ->toContain('PLAIN TEXT CONTENT'); + ->toContain('PLAIN TEXT CONTENT') + ->and((string)$manager->renderTemplate($template)) + ->toContain('HTML CONTENT') + ->and((string)$manager->renderView($template->subject)) + ->toContain('Test mail template subject'); +}); - expect((string)$manager->renderTemplate($template)) - ->toContain('HTML CONTENT'); +it('applies mailer config values correctly', function($driver) { + setting()->set('protocol', $driver); - expect((string)$manager->renderView($template->subject)) - ->toContain('Test mail template subject'); + $manager = resolve(MailManager::class); + $manager->applyMailerConfigValues(); + + expect(config('mail.default'))->toBe($driver); +})->with([ + ['smtp'], + ['mailgun'], + ['postmark'], + ['ses'], +]); + +it('fetches and caches template correctly', function() { + $manager = resolve(MailManager::class); + $template = MailTemplate::create([ + 'code' => 'test', + 'subject' => 'Test subject', + 'body' => 'Test body', + ]); + + $result = $manager->getTemplate('test'); + expect($result->getKey())->toBe($template->getKey()) + ->and($manager->getTemplate('test')->getKey())->toBe($template->getKey()); +}); + +it('renders template with layout', function() { + $manager = resolve(MailManager::class); + $template = MailTemplate::create([ + 'code' => 'test', + 'subject' => 'Test subject', + 'body' => 'Test body', + ]); + $template->layout = MailLayout::factory()->create([ + 'code' => 'test_layout', + 'layout' => '{{ $layout_css }} Test layout content {!! $body !!}', + 'layout_css' => 'layout css', + ]); + + $result = $manager->renderTemplate($template); + expect($result)->toBe("layout css Test layout content

Test body

\n"); +}); + +it('renders text template with layout', function() { + $manager = resolve(MailManager::class); + $template = MailTemplate::create([ + 'code' => 'test', + 'subject' => 'Test subject', + 'body' => 'plain body content', + 'plain_body' => '', // will use body content + ]); + $template->layout = MailLayout::factory()->create([ + 'code' => 'test_layout', + 'layout' => '{{ $layout_css }} Test layout content {!! $body !!}', + 'plain_layout' => 'plain layout content {!! $body !!}', + ]); + + $result = $manager->renderTextTemplate($template); + expect($result)->toBeInstanceOf(HtmlString::class) + ->and($result->toHtml())->toBe('plain layout content plain body content'); +}); + +it('renders missing partial correctly', function() { + $manager = resolve(MailManager::class); + $manager->startPartial('test_partial'); + + expect($manager->renderPartial())->toBe(''); + + // Clear output buffer + new HtmlString(trim(ob_get_clean())); +}); + +it('loads and returns registered layouts', function() { + $manager = resolve(MailManager::class); + + $result = $manager->listRegisteredLayouts(); + expect($result)->toBe(['default' => 'igniter.system::_mail.layouts.default']); +}); + +it('loads and returns registered templates', function() { + $manager = resolve(MailManager::class); + + $result = $manager->listRegisteredTemplates(); + expect($result)->not()->toBeEmpty(); +}); + +it('loads and returns registered variables', function() { + $manager = resolve(MailManager::class); + + $result = $manager->listRegisteredVariables(); + expect($result)->not()->toBeEmpty(); +}); + +it('registers custom blade directives when rendering view', function() { + $manager = resolve(MailManager::class); + Blade::shouldReceive('directive')->andReturnUsing(function($name, $callback) { + $callback(null); + + return in_array($name, ['partial', 'endpartial']); + }); + Blade::shouldReceive('render')->andReturn('rendered view'); + + $template = $manager->getTemplate('_mail.test_template'); + + expect((string)$manager->renderView($template->subject))->toBeString(); }); diff --git a/tests/src/System/Classes/PackageInfoTest.php b/tests/src/System/Classes/PackageInfoTest.php new file mode 100644 index 00000000..0f94f60d --- /dev/null +++ b/tests/src/System/Classes/PackageInfoTest.php @@ -0,0 +1,63 @@ + 'test-code', + 'package' => 'test-package', + 'type' => 'test-type', + 'name' => 'test-name', + 'version' => '1.0.0', + 'author' => 'test-author', + ]; + + $packageInfo = PackageInfo::fromArray($data); + + expect($packageInfo->code)->toBe('test-code') + ->and($packageInfo->package)->toBe('test-package') + ->and($packageInfo->type)->toBe('test-type') + ->and($packageInfo->name)->toBe('test-name') + ->and($packageInfo->version)->toBe('1.0.0') + ->and($packageInfo->author)->toBe('test-author') + ->and($packageInfo->description)->toBe('') + ->and($packageInfo->icon)->toBe([]) + ->and($packageInfo->installedVersion)->toBe('') + ->and($packageInfo->publishedAt)->toBe('') + ->and($packageInfo->tags)->toBe([]) + ->and($packageInfo->hash)->toBe('') + ->and($packageInfo->updatedAt)->toBe('') + ->and($packageInfo->homepage)->toBe('') + ->and($packageInfo->isCore())->toBeFalse(); +}); + +it('returns default value if icon key does not exist and no default provided', function() { + $packageInfo = new PackageInfo('test-code', 'test-package', 'test-type', 'test-name', '1.0.0', icon: ['icon-key' => 'icon-value']); + + expect($packageInfo->icon('non-existent-key', ''))->toBe(''); +}); + +it('returns empty string if no changelog tag exists and no description provided', function() { + $packageInfo = new PackageInfo('test-code', 'test-package', 'test-type', 'test-name', '1.0.0', tags: ['data' => []]); + + expect($packageInfo->changeLog())->toBe(''); +}); + +it('returns changelog if tag exists', function() { + $packageInfo = new PackageInfo('test-code', 'test-package', 'test-type', 'test-name', '1.0.0', tags: [ + 'data' => [ + ['name' => 'tag1', 'description' => 'tag **description**'], + ['name' => 'tag2', 'description' => ''], + ], + ]); + + expect((string)$packageInfo->changeLog())->toBe("

tag description

\n"); +}); + +it('returns formatted published date with custom format', function() { + $packageInfo = new PackageInfo('test-code', 'test-package', 'test-type', 'test-name', '1.0.0', publishedAt: '2023-01-01'); + + expect($packageInfo->publishedAt())->toBe('01 Jan 2023'); +}); diff --git a/tests/src/System/Classes/PackageManifestTest.php b/tests/src/System/Classes/PackageManifestTest.php new file mode 100644 index 00000000..4644a188 --- /dev/null +++ b/tests/src/System/Classes/PackageManifestTest.php @@ -0,0 +1,162 @@ +shouldReceive('getRequire')->andReturn($expected); + $manifest = new PackageManifest($filesystem, $this->app->basePath(), Igniter::getCachedAddonsPath()); + + expect($manifest->packages())->toBe($expected); +}); + +it('returns all extensions from manifest', function() { + $expected = [ + ['type' => 'tastyigniter-extension'], + ['type' => 'tastyigniter-theme'], + ]; + $filesystem = mock(Filesystem::class); + $filesystem->shouldReceive('getRequire')->andReturn($expected); + $manifest = new PackageManifest($filesystem, $this->app->basePath(), Igniter::getCachedAddonsPath()); + + expect($manifest->extensions())->toBe([['type' => 'tastyigniter-extension']]); +}); + +it('returns all themes from manifest', function() { + $expected = [ + ['type' => 'tastyigniter-extension'], + ['type' => 'tastyigniter-theme'], + ]; + $filesystem = mock(Filesystem::class); + $filesystem->shouldReceive('getRequire')->andReturn($expected); + $manifest = new PackageManifest($filesystem, $this->app->basePath(), Igniter::getCachedAddonsPath()); + + expect($manifest->themes())->toBe([['type' => 'tastyigniter-theme']]); +}); + +it('returns correct package path for relative path', function() { + $manifest = resolve(PackageManifest::class); + $manifest->vendorPath = '/vendor'; + + $result = $manifest->getPackagePath('../path/to/package'); + expect($result)->toBe('/vendor/composer/../path/to/package'); +}); + +it('returns correct package path for absolute path', function() { + $manifest = resolve(PackageManifest::class); + + $result = $manifest->getPackagePath('/path/to/package'); + expect($result)->toBe('/path/to/package'); +}); + +it('returns version for given package code', function() { + $expected = [ + ['code' => 'package1', 'version' => '1.0.0'], + ['code' => 'package2', 'version' => '2.0.0'], + ]; + $filesystem = mock(Filesystem::class); + $filesystem->shouldReceive('getRequire')->andReturn($expected); + $manifest = new PackageManifest($filesystem, $this->app->basePath(), Igniter::getCachedAddonsPath()); + + expect($manifest->getVersion('package1'))->toBe('1.0.0'); +}); + +it('returns core version from installed packages', function() { + $expected = [ + 'packages' => [ + ['name' => 'tastyigniter/core', 'version' => '1.0.0'], + ], + ]; + $filesystem = mock(Filesystem::class); + $filesystem->shouldReceive('exists')->andReturnTrue(); + $filesystem->shouldReceive('get')->andReturn(json_encode($expected)); + $manifest = new PackageManifest($filesystem, $this->app->basePath(), Igniter::getCachedAddonsPath()); + $manifest->vendorPath = '/vendor'; + + expect($manifest->coreVersion())->toBe('1.0.0'); +}); + +it('builds manifest with extensions and themes', function() { + $filesystem = mock(Filesystem::class); + $manifest = new PackageManifest($filesystem, $this->app->basePath(), Igniter::getCachedAddonsPath()); + $filesystem->shouldReceive('exists')->andReturnTrue(); + $filesystem->shouldReceive('get')->andReturn(json_encode([ + 'packages' => [ + [ + 'name' => 'tastyigniter/ti-ext-sample', + 'extra' => ['tastyigniter-extension' => ['code' => 'sample']], + 'install-path' => '/path/to/sample', + ], + [ + 'name' => 'tastyigniter/ti-theme-sample', + 'extra' => ['tastyigniter-theme' => ['code' => 'sample']], + 'install-path' => '/path/to/sample', + ], + [ + 'name' => 'other/package', + 'extra' => [], + 'install-path' => '/path/to/other', + ], + ], + ])); + $filesystem->shouldReceive('replace')->once(); + + $manifest->build(); +}); + +it('returns core addons from composer.json', function() { + $expected = [ + 'tastyigniter/ti-ext-sample' => ['code' => 'igniter.sample', 'version' => '1.0.0'], + 'tastyigniter/ti-theme-sample' => ['code' => 'igniter.sample', 'version' => '1.0.0'], + ]; + $filesystem = mock(Filesystem::class); + $filesystem->shouldReceive('get')->andReturn(json_encode([ + 'require' => [ + 'tastyigniter/ti-ext-sample' => '1.0.0', + 'tastyigniter/ti-theme-sample' => '1.0.0', + 'other/package' => '1.0.0', + ], + ])); + $manifest = new PackageManifest($filesystem, $this->app->basePath(), Igniter::getCachedAddonsPath()); + + $result = $manifest->coreAddons(); + + expect($result)->toBe($expected) + ->and($result)->toBe($manifest->coreAddons()); +}); + +it('returns empty array if no disabled addons file exists', function() { + $filesystem = mock(Filesystem::class); + $filesystem->shouldReceive('get')->andReturn(json_encode([])); + $manifest = new PackageManifest($filesystem, $this->app->basePath(), Igniter::getCachedAddonsPath()); + $manifest->manifestPath = '/path/to/manifest'; + + expect($manifest->disabledAddons())->toBe([]); +}); + +it('returns disabled addons from file', function() { + $filesystem = mock(Filesystem::class); + $filesystem->shouldReceive('get')->andReturn(json_encode(['addon1', 'addon2'])); + $manifest = new PackageManifest($filesystem, $this->app->basePath(), Igniter::getCachedAddonsPath()); + + expect($manifest->disabledAddons())->toBe(['addon1', 'addon2']); +}); + +it('writes disabled addons to file', function() { + $filesystem = mock(Filesystem::class); + $manifest = new PackageManifest($filesystem, $this->app->basePath(), Igniter::getCachedAddonsPath()); + $manifest->manifestPath = '/path/to/manifest'; + $filesystem->shouldReceive('replace')->with('/path/to/disabled-addons.json', json_encode(['addon1', 'addon2'])); + $filesystem->shouldReceive('get')->andReturn(json_encode(['addon1', 'addon2'])); + + $manifest->writeDisabled(['addon1', 'addon2']); + + $manifest->manifestPath = Igniter::getCachedAddonsPath(); + + expect($manifest->disabledAddons())->toBe(['addon1', 'addon2']); +}); diff --git a/tests/src/System/Classes/UpdateManagerTest.php b/tests/src/System/Classes/UpdateManagerTest.php index 7dfcb690..60a73c21 100644 --- a/tests/src/System/Classes/UpdateManagerTest.php +++ b/tests/src/System/Classes/UpdateManagerTest.php @@ -2,20 +2,401 @@ namespace Igniter\Tests\System\Classes; +use Facades\Igniter\System\Helpers\SystemHelper; +use Igniter\Flame\Composer\Manager as ComposerManager; +use Igniter\Flame\Database\Migrations\Migrator; +use Igniter\Flame\Exception\ApplicationException; +use Igniter\Flame\Exception\SystemException; +use Igniter\Flame\Support\Facades\Igniter; +use Igniter\Main\Classes\ThemeManager; +use Igniter\Main\Models\Theme; +use Igniter\System\Classes\ExtensionManager; use Igniter\System\Classes\UpdateManager; +use Igniter\System\Database\Seeds\DatabaseSeeder; +use Igniter\System\Models\Extension; +use Illuminate\Support\Facades\Http; +use Symfony\Component\Console\Output\OutputInterface; -it('requests latest updates', function() { - $result = resolve(UpdateManager::class)->requestUpdateList(); +it('logs messages correctly', function() { + $updateManager = resolve(UpdateManager::class); + $updateManager->log('Test message'); + expect($updateManager->getLogs())->toBe(['Test message']); +}); + +it('resets logs correctly', function() { + $updateManager = resolve(UpdateManager::class); + $updateManager->log('Test message'); + $updateManager->resetLogs(); + expect($updateManager->getLogs())->toBe([]); +}); + +it('logs error if migration table not found during down', function() { + $migrator = mock(Migrator::class); + app()->instance('migrator', $migrator); + $updateManager = new UpdateManager(); + $migrator->shouldReceive('repositoryExists')->andReturn(false); + + $updateManager->down(); + expect($updateManager->getLogs())->toContain('Migration table not found.'); +}); + +it('rolls back extensions and core migrations during down', function() { + $migrator = mock(Migrator::class); + app()->instance('migrator', $migrator); + $migrator->shouldReceive('repositoryExists')->andReturn(true); + $migrator->shouldReceive('resetAll')->times(2); + Igniter::shouldReceive('migrationPath')->andReturn([ + 'test.extension' => 'path/to/migrations', + ]); + $migrator->shouldReceive('setOutput'); + Igniter::shouldReceive('coreMigrationPath')->andReturn([ + 'igniter.system' => ['path/to/migrations'], + ]); + $updateManager = new UpdateManager(); + $outputMock = mock(OutputInterface::class); + $outputMock->shouldReceive('writeln'); + $updateManager->setLogsOutput($outputMock); + + $updateManager->down(); + $logs = $updateManager->getLogs(); + + expect($logs[0])->toContain('Purging extension test.extension') + ->and($logs[1])->toContain('Rolling back igniter.system') + ->and($logs[2])->toContain('Rolled back igniter.system'); +}); + +it('runs core and extension migrations during migrate', function() { + $updateManager = mockMigrate(); + + $updateManager->migrate(); + + expect($updateManager->getLogs())->toBeEmpty(); +}); + +it('logs error if unable to find migrations for extension during migrate', function() { + $updateManager = new UpdateManager(); + Igniter::shouldReceive('migrationPath')->andReturn([]); + + $updateManager->migrateExtension('nonexistent-extension'); + + expect($updateManager->getLogs())->toContain('Unable to find migrations for: nonexistent-extension'); +}); + +it('migrates extension correctly', function() { + $migrator = mock(Migrator::class); + app()->instance('migrator', $migrator); + $migrator->shouldReceive('setOutput'); + $updateManager = new UpdateManager(); + Igniter::shouldReceive('migrationPath')->andReturn(['test.extension' => ['/path/to/migrations']]); + $outputMock = mock(OutputInterface::class); + $outputMock->shouldReceive('writeln'); + $updateManager->setLogsOutput($outputMock); + $migrator->shouldReceive('runGroup')->once(); + + $updateManager->migrateExtension('test.extension'); + + expect($updateManager->getLogs())->toContain('Migrating extension test.extension'); +}); + +it('logs error if migration table not found during purge extension', function() { + $updateManager = new UpdateManager(); + Igniter::shouldReceive('migrationPath')->andReturn([]); + + $updateManager->purgeExtension('nonexistent.extension'); + + expect($updateManager->getLogs())->toContain('Unable to find migrations for: nonexistent.extension'); +}); + +it('logs error if unable to find migrations for extension during rollback', function() { + $updateManager = new UpdateManager(); + Igniter::shouldReceive('migrationPath')->andReturn([]); + + $updateManager->rollbackExtension('nonexistent-extension'); + + expect($updateManager->getLogs())->toContain('Unable to find migrations for: nonexistent-extension'); +}); + +it('rolls back extension migrations correctly', function() { + $migrator = mock(Migrator::class); + app()->instance('migrator', $migrator); + $migrator->shouldReceive('setOutput'); + $updateManager = new UpdateManager(); + $migrator->shouldReceive('rollbackAll')->once(); + Igniter::shouldReceive('migrationPath')->andReturn(['test.extension' => ['/path/to/migrations']]); + $outputMock = mock(OutputInterface::class); + $outputMock->shouldReceive('writeln'); + $updateManager->setLogsOutput($outputMock); + + $updateManager->rollbackExtension('test.extension'); + + expect($updateManager->getLogs())->toContain('Rolling back extension test.extension'); +}); + +it('returns true if last check is due', function() { + mockRequestUpdate(); + $updateManager = resolve(UpdateManager::class); + $result = $updateManager->isLastCheckDue(); + expect($result)->toBeTrue(); +}); + +it('returns recommended items with installed status', function() { + mockInstalledItems(); + $updateManager = new UpdateManager(); + + $result = $updateManager->listItems('extension'); + + expect($result['data'][0]['code'])->toBe('extension1') + ->and($result['data'][0]['installed'])->toBeTrue() + ->and($result['data'][1]['code'])->toBe('extension2') + ->and($result['data'][1]['installed'])->toBeFalse(); +}); + +it('returns searched items with installed status', function() { + mockInstalledItems(); + $updateManager = new UpdateManager(); + + $result = $updateManager->searchItems('extension', 'searchQuery'); + + expect($result['data'][0]['code'])->toBe('extension1') + ->and($result['data'][0]['installed'])->toBeTrue() + ->and($result['data'][1]['code'])->toBe('extension2') + ->and($result['data'][1]['installed'])->toBeFalse(); +}); - expect($result)->toBeArray(); +it('applies site detail correctly', function() { + $expectedResponse = [ + 'data' => [ + 'name' => 'Test Site', + 'url' => 'https://test-site.com', + ], + ]; + Http::fake(['https://api.tastyigniter.com/v2/site/detail' => Http::response($expectedResponse)]); + $updateManager = resolve(UpdateManager::class); + app()->setBasePath(__DIR__.'/../Fixtures'); + + $result = $updateManager->applySiteDetail('test-key'); + + expect($result)->toBeArray() + ->and(setting()->getPref('carte_key'))->toBe('test-key') + ->and($updateManager->getSiteDetail())->toBe($result); }); -it('runs core database migrations', function() {})->skip(); +it('returns extensions installed items correctly', function() { + Extension::create(['name' => 'extension1', 'version' => '1.0.0']); + $expectedResponse = [ + 'data' => [ + ['code' => 'extension1', 'icon' => null], + ['code' => 'extension2', 'icon' => null], + ], + ]; + Http::fake(['https://api.tastyigniter.com/v2/items' => Http::response($expectedResponse)]); + $updateManager = resolve(UpdateManager::class); + + $result = $updateManager->getInstalledItems('extensions'); + + expect($result)->toBeArray() + ->and($result)->toBe($updateManager->getInstalledItems('extensions')); +}); + +it('applies items correctly', function() { + config(['igniter-system.disableCoreUpdates' => true]); + $expectedResponse = [ + 'data' => [ + [ + 'code' => 'core-package', + 'type' => 'core', + 'package' => 'item1/package', + 'name' => 'Package1', + 'version' => '1.0.0', + 'author' => 'Sam', + ], + [ + 'code' => 'package1', + 'type' => 'extension', + 'package' => 'item2/package', + 'name' => 'Package2', + 'version' => '1.0.0', + 'author' => 'Sam', + ], + ], + ]; + Http::fake(['https://api.tastyigniter.com/v2/core/apply' => Http::response($expectedResponse)]); + $updateManager = new UpdateManager(); + + $result = $updateManager->requestApplyItems(['package1', 'core-package']); + + expect($result->count())->toBe(1) + ->and($result->first()->code)->toBe('package1'); +}); -it('runs extension database migrations', function() {})->skip(); +it('marks update as ignored', function() { + $updateManager = new UpdateManager(); + + $updateManager->markedAsIgnored('package1'); + + expect($updateManager->getIgnoredUpdates())->toBe(['package1' => true]); +}); + +it('returns null if pre-install checks fail on assertIniSet', function() { + $updateManager = resolve(UpdateManager::class); + SystemHelper::shouldReceive('assertIniSet')->andReturn(false); + expect($updateManager->preInstall())->toBeNull(); +}); + +it('throws exception if pre-install checks fail on assertIniMaxExecutionTime', function() { + $updateManager = resolve(UpdateManager::class); + SystemHelper::shouldReceive('assertIniSet')->andReturn(true); + SystemHelper::shouldReceive('assertIniMaxExecutionTime')->andReturn(true); + SystemHelper::shouldReceive('assertIniMemoryLimit')->andReturn(false); + expect(fn() => $updateManager->preInstall())->toThrow(ApplicationException::class); +}); + +it('throws exception if pre-install checks fail on assertIniMemoryLimit', function() { + $updateManager = resolve(UpdateManager::class); + SystemHelper::shouldReceive('assertIniSet')->andReturn(true); + SystemHelper::shouldReceive('assertIniMaxExecutionTime')->andReturn(false); + SystemHelper::shouldReceive('assertIniMemoryLimit')->andReturn(true); + expect(fn() => $updateManager->preInstall())->toThrow(ApplicationException::class); +}); + +it('installs packages correctly', function() { + $composerManager = mock(ComposerManager::class); + app()->instance(ComposerManager::class, $composerManager); + $composerManager->shouldReceive('install')->once(); + + $updateManager = resolve(UpdateManager::class); + + $updateManager->install([ + [ + 'code' => 'test.extension', + 'package' => 'test/extension', + 'type' => 'extension', + 'name' => 'Test Package', + 'version' => '2.0.0', + 'author' => 'Sam', + 'description' => 'Test package description', + 'icon' => 'fa-icon', + 'installedVersion' => '1.0.0', + 'publishedAt' => '2021-01-01 00:00:00', + 'tags' => [], + 'hash' => 'hash', + 'updatedAt' => '2021-01-01 00:00:00', + ], + ]); + + expect($updateManager->getLogs()[0])->toContain('Test Package (1.0.0 => 2.0.0)'); +}); + +it('completes installation correctly', function() { + mockRequestUpdate(); + $extensionManager = mock(ExtensionManager::class); + $themeManager = mock(ThemeManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + app()->instance(ThemeManager::class, $themeManager); + $extensionManager->shouldReceive('installExtension')->with('test.extension', '2.0.0')->once(); + $themeManager->shouldReceive('installTheme')->with('test.theme', '2.0.0')->once(); + $requirements = [ + [ + 'code' => 'test.core', + 'package' => 'test/core', + 'type' => 'core', + 'name' => 'Test Package', + 'version' => '2.0.0', + 'author' => 'Sam', + 'description' => 'Test package description', + 'icon' => 'fa-icon', + 'installedVersion' => '1.0.0', + 'publishedAt' => '2021-01-01 00:00:00', + 'tags' => [], + 'hash' => 'hash', + 'updatedAt' => '2021-01-01 00:00:00', + ], + [ + 'code' => 'test.extension', + 'package' => 'test/extension', + 'type' => 'extension', + 'name' => 'Test Package', + 'version' => '2.0.0', + 'author' => 'Sam', + 'description' => 'Test package description', + 'icon' => 'fa-icon', + 'installedVersion' => '1.0.0', + 'publishedAt' => '2021-01-01 00:00:00', + 'tags' => [], + 'hash' => 'hash', + 'updatedAt' => '2021-01-01 00:00:00', + ], + [ + 'code' => 'test.theme', + 'package' => 'test/theme', + 'type' => 'theme', + 'name' => 'Test Package', + 'version' => '2.0.0', + 'author' => 'Sam', + 'description' => 'Test package description', + 'icon' => 'fa-icon', + 'installedVersion' => '1.0.0', + 'publishedAt' => '2021-01-01 00:00:00', + 'tags' => [], + 'hash' => 'hash', + 'updatedAt' => '2021-01-01 00:00:00', + ], + ]; + + $updateManager = mockMigrate(); + expect(fn() => $updateManager->completeInstall($requirements))->toThrow(SystemException::class); +}); -it('runs core database seeders', function() {})->skip(); +function mockRequestUpdate() +{ + $expectedResponse = [ + 'data' => [ + [ + 'code' => 'item1', + 'type' => 'core', + 'package' => 'item1/package', + 'name' => 'Package1', + 'version' => '1.0.0', + 'author' => 'Sam', + ], + [ + 'code' => 'item2', + 'type' => 'extension', + 'package' => 'item2/package', + 'name' => 'Package2', + 'version' => '1.0.0', + 'author' => 'Sam', + ], + ], + ]; + Http::fake(['https://api.tastyigniter.com/v2/core/apply' => Http::response($expectedResponse)]); +} -it('purges extension database migrations', function() {})->skip(); +function mockInstalledItems(): void +{ + Extension::create(['name' => 'extension1', 'version' => '1.0.0']); + Theme::create(['code' => 'theme1', 'name' => 'Theme', 'version' => '1.0.0', 'data' => []]); + $expectedResponse = [ + 'data' => [ + ['code' => 'extension1', 'icon' => null], + ['code' => 'extension2', 'icon' => null], + ], + ]; + Http::fake(['https://api.tastyigniter.com/v2/items' => Http::response($expectedResponse)]); +} -it('rollbacks extension database migrations', function() {})->skip(); +function mockMigrate(): UpdateManager +{ + $migrator = mock(Migrator::class); + app()->instance('migrator', $migrator); + $migrator->shouldReceive('setOutput'); + $migrator->shouldReceive('runGroup')->twice(); + $databaseSeeder = mock(DatabaseSeeder::class)->makePartial(); + $databaseSeeder->shouldReceive('run'); + app()->instance(DatabaseSeeder::class, $databaseSeeder); + $updateManager = new UpdateManager(); + $outputMock = mock(OutputInterface::class); + $outputMock->shouldReceive('writeln'); + $updateManager->setLogsOutput($outputMock); + return $updateManager; +} diff --git a/tests/src/System/Console/Commands/ExtensionInstallTest.php b/tests/src/System/Console/Commands/ExtensionInstallTest.php new file mode 100644 index 00000000..141c8944 --- /dev/null +++ b/tests/src/System/Console/Commands/ExtensionInstallTest.php @@ -0,0 +1,74 @@ + true]); + $expectedResponse = [ + 'data' => [ + [ + 'code' => 'package1', + 'type' => 'extension', + 'package' => 'item2/package', + 'name' => 'Package2', + 'version' => '1.0.0', + 'author' => 'Sam', + ], + ], + ]; + Http::fake(['https://api.tastyigniter.com/v2/core/apply' => Http::response($expectedResponse)]); + $composerManager = mock(ComposerManager::class); + app()->instance(ComposerManager::class, $composerManager); + $composerManager->shouldReceive('install')->once(); + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('loadExtensions')->once(); + $extensionManager->shouldReceive('installExtension')->once(); + + $this->artisan('igniter:extension-install IgniterLab.Demo') + ->expectsOutput('Installing IgniterLab.Demo extension') + ->assertExitCode(0); +}); + +it('outputs error if extension not found', function() { + config(['igniter-system.disableCoreUpdates' => true]); + $expectedResponse = [ + 'data' => [], + ]; + Http::fake(['https://api.tastyigniter.com/v2/core/apply' => Http::response($expectedResponse)]); + + $this->artisan('igniter:extension-install IgniterLab.Demo') + ->expectsOutput('Extension IgniterLab.Demo not found') + ->assertExitCode(0); +}); + +it('handles composer exception during installation', function() { + config(['igniter-system.disableCoreUpdates' => true]); + $expectedResponse = [ + 'data' => [ + [ + 'code' => 'package1', + 'type' => 'extension', + 'package' => 'item2/package', + 'name' => 'Package2', + 'version' => '1.0.0', + 'author' => 'Sam', + ], + ], + ]; + Http::fake(['https://api.tastyigniter.com/v2/core/apply' => Http::response($expectedResponse)]); + $composerManager = mock(ComposerManager::class); + app()->instance(ComposerManager::class, $composerManager); + $composerManager->shouldReceive('install')->andThrow(new ComposerException(new Exception('Composer error'), new BufferIO())); + + $this->artisan('igniter:extension-install IgniterLab.Demo') + ->expectsOutput("Error updating composer requirements: Composer error\nOutput: ") + ->assertExitCode(0); +}); diff --git a/tests/src/System/Console/Commands/ExtensionRefreshTest.php b/tests/src/System/Console/Commands/ExtensionRefreshTest.php new file mode 100644 index 00000000..b782c7ad --- /dev/null +++ b/tests/src/System/Console/Commands/ExtensionRefreshTest.php @@ -0,0 +1,44 @@ +instance(UpdateManager::class, $updateManager); + $updateManager->shouldReceive('setLogsOutput')->once(); + $updateManager->shouldReceive('rollbackExtension')->once(); + + $this->artisan('igniter:extension-refresh igniter.user --step=1') + ->expectsOutput('Rolling back extension igniter.user...') + ->assertExitCode(0); +}); + +it('purges and migrates extension without step option', function() { + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + $updateManager->shouldReceive('setLogsOutput')->once(); + $updateManager->shouldReceive('purgeExtension')->once(); + $updateManager->shouldReceive('migrateExtension')->once(); + + $this->artisan('igniter:extension-refresh igniter.user') + ->expectsOutput('Purging extension igniter.user...') + ->assertExitCode(0); +}); + +it('throws exception if extension not found', function() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Extension "igniter.demo" not found.'); + + $this->artisan('igniter:extension-refresh Igniter.Demo') + ->assertExitCode(1); +}); + +it('does not proceed if confirmation is denied', function() { + $this->app['env'] = 'production'; + + $this->artisan('igniter:extension-refresh igniter.user') + ->expectsConfirmation('Are you sure you want to run this command?', 'no') + ->assertExitCode(0); +}); diff --git a/tests/src/System/Console/Commands/ExtensionRemoveTest.php b/tests/src/System/Console/Commands/ExtensionRemoveTest.php new file mode 100644 index 00000000..04463ba9 --- /dev/null +++ b/tests/src/System/Console/Commands/ExtensionRemoveTest.php @@ -0,0 +1,57 @@ +shouldReceive('getIdentifier')->with('igniterlab.demo')->andReturn('IgniterLab.Demo'); + $extensionManager->shouldReceive('hasExtension')->with('IgniterLab.Demo')->andReturn(true); + $extensionManager->shouldReceive('deleteExtension')->with('IgniterLab.Demo')->once(); + app()->instance(ExtensionManager::class, $extensionManager); + + $this->artisan('igniter:extension-remove igniterlab.demo') + ->expectsOutput('Removing extension: IgniterLab.Demo') + ->expectsOutput('Deleted extension: IgniterLab.Demo') + ->assertExitCode(0); +}); + +it('outputs error if extension not found', function() { + $extensionManager = mock(ExtensionManager::class); + $extensionManager->shouldReceive('getIdentifier')->with('igniterlab.demo')->andReturn('IgniterLab.Demo'); + $extensionManager->shouldReceive('hasExtension')->with('IgniterLab.Demo')->andReturn(false); + app()->instance(ExtensionManager::class, $extensionManager); + + $this->artisan('igniter:extension-remove igniterlab.demo') + ->expectsOutput('Unable to find a registered extension called "IgniterLab.Demo"') + ->assertExitCode(0); +}); + +it('does not proceed if confirmation is denied', function() { + $this->app['env'] = 'production'; + $extensionManager = mock(ExtensionManager::class); + $extensionManager->shouldReceive('getIdentifier')->with('igniterlab.demo')->andReturn('IgniterLab.Demo'); + $extensionManager->shouldReceive('hasExtension')->with('IgniterLab.Demo')->andReturn(true); + app()->instance(ExtensionManager::class, $extensionManager); + + $this->artisan('igniter:extension-remove igniterlab.demo --no-interaction') + ->expectsConfirmation('Are you sure you want to run this command?', 'no') + ->assertExitCode(0); +}); + +it('handles composer exception during removal', function() { + $extensionManager = mock(ExtensionManager::class); + $extensionManager->shouldReceive('getIdentifier')->with('igniterlab.demo')->andReturn('IgniterLab.Demo'); + $extensionManager->shouldReceive('hasExtension')->with('IgniterLab.Demo')->andReturn(true); + $extensionManager->shouldReceive('deleteExtension')->with('IgniterLab.Demo') + ->andThrow(new ComposerException(new Exception('Composer error'), new BufferIO())); + app()->instance(ExtensionManager::class, $extensionManager); + + $this->artisan('igniter:extension-remove igniterlab.demo') + ->expectsOutput("Error updating composer requirements: Composer error\nOutput: ") + ->assertExitCode(0); +}); diff --git a/tests/src/System/Console/Commands/IgniterDownTest.php b/tests/src/System/Console/Commands/IgniterDownTest.php new file mode 100644 index 00000000..0829e47c --- /dev/null +++ b/tests/src/System/Console/Commands/IgniterDownTest.php @@ -0,0 +1,23 @@ +instance(UpdateManager::class, $updateManager); + $updateManager->shouldReceive('setLogsOutput')->once(); + $updateManager->shouldReceive('down')->once(); + + $this->artisan('igniter:down') + ->assertExitCode(0); +}); + +it('does not destroy database tables when not confirmed', function() { + $this->app['env'] = 'production'; + + $this->artisan('igniter:down') + ->expectsConfirmation('Are you sure you want to run this command?', 'no') + ->assertExitCode(0); +}); diff --git a/tests/src/System/Console/Commands/IgniterInstallTest.php b/tests/src/System/Console/Commands/IgniterInstallTest.php new file mode 100644 index 00000000..0067229a --- /dev/null +++ b/tests/src/System/Console/Commands/IgniterInstallTest.php @@ -0,0 +1,125 @@ +instance(ComposerManager::class, $composerManager); + $composerManager->shouldReceive('assertSchema')->once(); + File::shouldReceive('exists')->withArgs(fn($path) => ends_with($path, '/.env'))->andReturn(false, true, true); + File::shouldReceive('exists')->withArgs(fn($path) => ends_with($path, '/example.env'))->andReturnTrue(); + File::shouldReceive('move')->withArgs(fn($from, $to) => ends_with($from, '/.env') && ends_with($to, '/backup.env')); + File::shouldReceive('delete')->withArgs(fn($path) => ends_with($path, '/.env')); + File::shouldReceive('copy')->withArgs(fn($from, $to) => ends_with($from, '/example.env') && ends_with($to, '/.env')); + SystemHelper::shouldReceive('replaceInEnv'); + config([ + 'app.key' => null, + 'app.url' => 'http://localhost', + 'database.connections.mysql.host' => 'localhost', + 'database.connections.mysql.port' => '3306', + 'database.connections.mysql.database' => 'igniter', + 'database.connections.mysql.username' => 'root', + 'database.connections.mysql.password' => '', + 'database.connections.mysql.prefix' => '', + ]); + DB::partialMock()->shouldReceive('purge')->once(); + Igniter::shouldReceive('hasDatabase')->andReturnFalse(); + $installCommand = mock(IgniterInstall::class)->makePartial(); + $installCommand->setOutput($output = mock(OutputStyle::class)); + $installCommand->shouldReceive('option')->andReturnFalse(); + $installCommand->shouldReceive('confirm')->andReturnTrue(); + $installCommand->shouldReceive('alert')->with('INSTALLATION STARTED')->once(); + $installCommand->shouldReceive('line')->byDefault(); + $installCommand->shouldReceive('ask')->with('MySQL Host', 'localhost')->andReturn('localhost'); + $installCommand->shouldReceive('ask')->with('MySQL Port', '3306')->andReturn('3306'); + $installCommand->shouldReceive('ask')->with('MySQL Database', 'igniter')->andReturn('igniter'); + $installCommand->shouldReceive('ask')->with('MySQL Username', 'root')->andReturn('root'); + $installCommand->shouldReceive('ask')->with('MySQL Password', '')->andReturn(''); + $installCommand->shouldReceive('ask')->with('MySQL Table Prefix', '')->andReturn(''); + $installCommand->shouldReceive('ask')->with('Site Name', 'TastyIgniter')->andReturn('TastyIgniter'); + $installCommand->shouldReceive('ask')->with('Site URL', 'http://localhost')->andReturn('http://localhost'); + $installCommand->shouldReceive('confirm')->withSomeOfArgs('Install demo data?')->andReturn(true); + $installCommand->shouldReceive('call')->with('igniter:up', ['--force' => true]); + $installCommand->shouldReceive('ask')->with('Admin Name', 'Chef Admin')->andReturn('Chef Admin'); + $output->shouldReceive('ask')->withArgs(function($name, $value, $callback) { + return $name === 'Admin Email' && $callback($value); + })->andReturn('admin@domain.tld'); + $output->shouldReceive('ask')->withArgs(function($name, $value, $callback) { + return $name === 'Admin Password' && $callback($value); + })->andReturn('123456'); + $output->shouldReceive('ask')->withArgs(function($name, $value, $callback) { + return $name === 'Admin Username' && $callback($value); + })->andReturn('admin'); + $installCommand->shouldReceive('line')->with('Admin user admin created!'); + $installCommand->shouldReceive('call')->with('storage:link')->once(); + $installCommand->shouldReceive('call')->with('igniter:theme-vendor-publish'); + $installCommand->shouldReceive('alert')->with('INSTALLATION COMPLETE')->once(); + + return $installCommand; +} + +it('installs TastyIgniter successfully', function() { + Event::fake(); + $installCommand = setupInstallation(); + Process::shouldReceive('run')->andReturn(0); + Igniter::shouldReceive('adminUri')->andReturn('admin'); + SystemHelper::shouldReceive('runningOnMac')->andReturnTrue(); + + $installCommand->handle(); + + expect(User::where('email', 'admin@domain.tld')->exists())->toBeTrue(); +}); + +it('skips setup if already installed and not forced', function() { + Event::fake(); + $composerManager = mock(ComposerManager::class); + app()->instance(ComposerManager::class, $composerManager); + $composerManager->shouldReceive('assertSchema')->once(); + Igniter::shouldReceive('hasDatabase')->andReturnTrue(); + $installCommand = mock(IgniterInstall::class)->makePartial(); + $installCommand->setOutput($output = mock(OutputStyle::class)); + $installCommand->shouldReceive('option')->andReturnFalse(); + $installCommand->shouldReceive('confirm')->with('Application appears to be installed already. Continue anyway?', false)->andReturnFalse(); + $installCommand->shouldReceive('alert')->with('INSTALLATION STARTED')->never(); + + $installCommand->handle(); +}); + +it('opens browser window when installing in windows', function() { + Event::fake(); + $installCommand = setupInstallation(); + Process::shouldReceive('run')->andReturn(0); + Igniter::shouldReceive('adminUri')->andReturn('admin'); + SystemHelper::shouldReceive('runningOnMac')->andReturnFalse(); + SystemHelper::shouldReceive('runningOnWindows')->andReturnTrue(); + + $installCommand->handle(); + + expect(User::where('email', 'admin@domain.tld')->exists())->toBeTrue(); +}); + +it('opens browser window when installing in linux', function() { + Event::fake(); + $installCommand = setupInstallation(); + Process::shouldReceive('run')->andReturn(0); + Igniter::shouldReceive('adminUri')->andReturn('admin'); + SystemHelper::shouldReceive('runningOnMac')->andReturnFalse(); + SystemHelper::shouldReceive('runningOnWindows')->andReturnFalse(); + + $installCommand->handle(); + + expect(User::where('email', 'admin@domain.tld')->exists())->toBeTrue(); +}); diff --git a/tests/src/System/Console/Commands/IgniterPackageDiscoverTest.php b/tests/src/System/Console/Commands/IgniterPackageDiscoverTest.php new file mode 100644 index 00000000..47d4c8c9 --- /dev/null +++ b/tests/src/System/Console/Commands/IgniterPackageDiscoverTest.php @@ -0,0 +1,40 @@ +instance(PackageManifest::class, $packageManifest); + $filesystem = mock(Filesystem::class); + $packageManifest->files = $filesystem; + $filesystem->shouldReceive('exists')->andReturn(true); + $filesystem->shouldReceive('delete')->andReturnNull(); + $packageManifest->shouldReceive('build')->andReturnSelf(); + $packageManifest->shouldReceive('packages')->andReturn([ + ['code' => 'igniter.demo', 'name' => 'Demo'], + ['code' => 'igniter.blog', 'name' => 'Blog'], + ]); + + $this->artisan('igniter:package-discover') + ->assertExitCode(0); +}); + +it('skips deletion if manifest file does not exist', function() { + $packageManifest = mock(PackageManifest::class); + app()->instance(PackageManifest::class, $packageManifest); + $filesystem = mock(Filesystem::class); + $packageManifest->files = $filesystem; + $filesystem->shouldReceive('exists')->andReturn(false); + $filesystem->shouldReceive('delete')->never(); + $packageManifest->shouldReceive('build')->andReturnSelf(); + $packageManifest->shouldReceive('packages')->andReturn([ + ['code' => 'igniter.demo', 'name' => 'Demo'], + ['code' => 'igniter.blog', 'name' => 'Blog'], + ]); + + $this->artisan('igniter:package-discover') + ->assertExitCode(0); +}); diff --git a/tests/src/System/Console/Commands/IgniterPasswdTest.php b/tests/src/System/Console/Commands/IgniterPasswdTest.php new file mode 100644 index 00000000..90dcf215 --- /dev/null +++ b/tests/src/System/Console/Commands/IgniterPasswdTest.php @@ -0,0 +1,46 @@ +create(); + + $this->artisan('igniter:passwd', ['email' => $user->email, 'password' => 'newpassword']) + ->expectsOutput('Password successfully changed.') + ->assertExitCode(0); + + expect(User::find($user->getKey())->password)->not->toBe($user->password); +}); + +it('throws exception if user does not exist', function() { + $this->expectException(FlashException::class); + $this->expectExceptionMessage('The specified user does not exist.'); + + $this->artisan('igniter:passwd', ['email' => 'user@example.com', 'password' => 'newpassword']) + ->assertExitCode(1); +}); + +it('prompts for email if not provided', function() { + $user = User::factory()->create(); + + $this->artisan('igniter:passwd', ['password' => 'newpassword']) + ->expectsQuestion('Admin email to reset', $user->email) + ->expectsOutput('Password successfully changed.') + ->assertExitCode(0); + + expect(User::find($user->getKey())->password)->not->toBe($user->password); +}); + +it('generates password if not provided', function() { + $user = User::factory()->create(); + + $this->artisan('igniter:passwd', ['email' => $user->email]) + ->expectsQuestion('Enter new password (leave blank for generated password)', null) + ->expectsOutput('Password successfully changed.') + ->assertExitCode(0); + + expect(User::find($user->getKey())->password)->not->toBe($user->password); +}); diff --git a/tests/src/System/Console/Commands/IgniterUpTest.php b/tests/src/System/Console/Commands/IgniterUpTest.php new file mode 100644 index 00000000..d7b041e3 --- /dev/null +++ b/tests/src/System/Console/Commands/IgniterUpTest.php @@ -0,0 +1,79 @@ +makePartial()->shouldAllowMockingProtectedMethods(); + $command->shouldReceive('confirmToProceed')->andReturn(true); + $input = mock(InputInterface::class)->makePartial(); + $output = mock(OutputStyle::class); + $command->setInput($input); + $command->setOutput($output); + $command->setLaravel(app()); + $input->shouldReceive('getOption')->with('force')->andReturn(true); + + $migrator = mock('migrator'); + $migrator->shouldReceive('getRepository->prepareMigrationTable')->once(); + app()->instance('migrator', $migrator); + + $updateManager = mock(UpdateManager::class); + $updateManager->shouldReceive('setLogsOutput')->with($output)->andReturnSelf(); + $updateManager->shouldReceive('migrate')->once(); + app()->instance(UpdateManager::class, $updateManager); + + $command->shouldReceive('call')->with('migrate', ['--force' => true])->once(); + $command->shouldReceive('renameConflictingFoundationTables')->once(); + + $command->handle(); +}); + +it('does not build database tables when not confirmed', function() { + $command = mock(IgniterUp::class)->makePartial()->shouldAllowMockingProtectedMethods(); + $command->shouldReceive('confirmToProceed')->andReturn(false); + + $command->shouldNotReceive('call'); + $command->shouldNotReceive('renameConflictingFoundationTables'); + + $command->handle(); +}); + +it('renames conflicting foundation tables', function() { + $command = mock(IgniterUp::class)->makePartial()->shouldAllowMockingProtectedMethods(); + $output = mock(OutputStyle::class); + $command->setOutput($output); + $reflection = new \ReflectionClass($command); + $property = $reflection->getProperty('components'); + $property->setAccessible(true); + $property->setValue($command, $output); + + Schema::shouldReceive('hasColumn')->with('users', 'staff_id')->andReturn(true); + Schema::shouldReceive('rename')->with('users', 'admin_users')->once(); + Schema::shouldReceive('hasTable')->andReturnUsing(function($table) { + return in_array($table, ['cache', 'failed_jobs', 'jobs', 'job_batches', 'sessions']); + }); + Schema::shouldReceive('rename')->with(Mockery::any(), Mockery::any())->times(5); + + $output->shouldReceive('info')->times(6); + + $command->renameConflictingFoundationTables(); +}); + +it('skips renaming if no conflicting foundation tables', function() { + $command = mock(IgniterUp::class)->makePartial()->shouldAllowMockingProtectedMethods(); + $output = mock(OutputStyle::class); + $command->setOutput($output); + + Schema::shouldReceive('hasColumn')->with('users', 'staff_id')->andReturn(false); + + $output->shouldNotReceive('info'); + Schema::shouldNotReceive('rename'); + + $command->renameConflictingFoundationTables(); +}); diff --git a/tests/src/System/Console/Commands/IgniterUpdateTest.php b/tests/src/System/Console/Commands/IgniterUpdateTest.php new file mode 100644 index 00000000..196386a2 --- /dev/null +++ b/tests/src/System/Console/Commands/IgniterUpdateTest.php @@ -0,0 +1,222 @@ +instance(ComposerManager::class, $composerManager); + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + + $composerManager->shouldReceive('assertSchema')->once(); + $updateManager->shouldReceive('setLogsOutput')->once(); + $updateManager->shouldReceive('requestUpdateList')->andReturn(['items' => null]); + + $this->artisan('igniter:update') + ->expectsOutput('Checking for updates...') + ->expectsOutput('No new updates found') + ->assertExitCode(0); +}); + +it('checks for updates and finds some', function() { + $command = mock(IgniterUpdate::class)->makePartial()->shouldAllowMockingProtectedMethods(); + $command->shouldReceive('option')->with('force')->andReturn(false); + $command->shouldReceive('option')->with('check')->andReturn(false); + $command->shouldReceive('confirmToProceed')->andReturn(true); + $output = mock(OutputStyle::class); + $command->setOutput($output); + + $composerManager = mock(ComposerManager::class); + app()->instance(ComposerManager::class, $composerManager); + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + + $composerManager->shouldReceive('assertSchema')->once(); + $updateManager->shouldReceive('setLogsOutput')->once(); + $updateManager->shouldReceive('requestUpdateList')->andReturn([ + 'items' => collect([ + (object)['code' => 'tastyigniter', 'name' => 'TastyIgniter', 'type' => 'core'], + (object)['code' => 'igniter.demo', 'name' => 'Demo', 'type' => 'extension'], + (object)['code' => 'igniter.theme', 'name' => 'Theme', 'type' => 'theme'], + ]), + 'count' => 3, + ]); + + $output->shouldReceive('writeln')->with('Checking for updates...')->once(); + $output->shouldReceive('writeln')->with('3 updates found')->once(); + $command->shouldReceive('option')->with('addons')->andReturnNull(); + $command->shouldReceive('option')->with('core')->andReturnNull(); + $output->shouldReceive('writeln')->with('Updating TastyIgniter...')->once(); + $output->shouldReceive('writeln')->with('Updating extensions/themes...')->once(); + $updateManager->shouldReceive('install')->twice(); + $command->shouldReceive('call')->with('igniter:up')->once(); + + $command->handle(); +}); + +it('checks for updates and runs check only and broadcasts notification', function() { + $composerManager = mock(ComposerManager::class); + app()->instance(ComposerManager::class, $composerManager); + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + + $composerManager->shouldReceive('assertSchema')->once(); + $updateManager->shouldReceive('setLogsOutput')->once(); + $updateManager->shouldReceive('requestUpdateList')->andReturn([ + 'items' => collect([ + (object)['code' => 'tastyigniter', 'name' => 'TastyIgniter', 'type' => 'core'], + (object)['code' => 'igniter.demo', 'name' => 'Demo', 'type' => 'extension'], + (object)['code' => 'igniter.theme', 'name' => 'Theme', 'type' => 'theme'], + ]), + 'count' => 3, + ]); + + $this->artisan('igniter:update --check') + ->expectsOutput('Checking for updates...') + ->assertExitCode(0); +}); + +it('updates core only', function() { + $command = mock(IgniterUpdate::class)->makePartial()->shouldAllowMockingProtectedMethods(); + $command->shouldReceive('option')->with('force')->andReturn(false); + $command->shouldReceive('option')->with('check')->andReturn(false); + $command->shouldReceive('confirmToProceed')->andReturn(true); + $output = mock(OutputStyle::class); + $command->setOutput($output); + + $composerManager = mock(ComposerManager::class); + app()->instance(ComposerManager::class, $composerManager); + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + + $composerManager->shouldReceive('assertSchema')->once(); + $updateManager->shouldReceive('setLogsOutput')->once(); + $updateManager->shouldReceive('requestUpdateList')->andReturn([ + 'items' => collect([ + (object)['code' => 'tastyigniter', 'name' => 'TastyIgniter', 'type' => 'core'], + (object)['code' => 'igniter.demo', 'name' => 'Demo', 'type' => 'extension'], + (object)['code' => 'igniter.theme', 'name' => 'Theme', 'type' => 'theme'], + ]), + 'count' => 3, + ]); + + $output->shouldReceive('writeln')->with('Checking for updates...')->once(); + $output->shouldReceive('writeln')->with('3 updates found')->once(); + $command->shouldReceive('option')->with('addons')->andReturnNull(); + $command->shouldReceive('option')->with('core')->andReturnTrue(); + $output->shouldReceive('writeln')->with('Updating TastyIgniter...')->once(); + $output->shouldReceive('writeln')->with('Updating extensions/themes...')->never(); + $updateManager->shouldReceive('install')->once(); + $command->shouldReceive('call')->with('igniter:up')->once(); + + $command->handle(); +}); + +it('updates specified addons only', function() { + $command = mock(IgniterUpdate::class)->makePartial()->shouldAllowMockingProtectedMethods(); + $command->shouldReceive('option')->with('force')->andReturn(false); + $command->shouldReceive('option')->with('check')->andReturn(false); + $command->shouldReceive('confirmToProceed')->andReturn(true); + $output = mock(OutputStyle::class); + $command->setOutput($output); + + $composerManager = mock(ComposerManager::class); + app()->instance(ComposerManager::class, $composerManager); + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + + $composerManager->shouldReceive('assertSchema')->once(); + $updateManager->shouldReceive('setLogsOutput')->once(); + $updateManager->shouldReceive('requestUpdateList')->andReturn([ + 'items' => collect([ + (object)['code' => 'tastyigniter', 'name' => 'TastyIgniter', 'type' => 'core'], + (object)['code' => 'igniter.demo', 'name' => 'Demo', 'type' => 'extension'], + (object)['code' => 'igniter.theme', 'name' => 'Theme', 'type' => 'theme'], + ]), + 'count' => 3, + ]); + + $output->shouldReceive('writeln')->with('Checking for updates...')->once(); + $output->shouldReceive('writeln')->with('3 updates found')->once(); + $command->shouldReceive('option')->with('addons')->andReturn(['igniter.demo']); + $command->shouldReceive('option')->with('core')->andReturnNull(); + $output->shouldReceive('writeln')->with('Updating TastyIgniter...')->never(); + $output->shouldReceive('writeln')->with('Updating extensions/themes...')->once(); + $updateManager->shouldReceive('install')->once(); + $command->shouldReceive('call')->with('igniter:up')->once(); + + $command->handle(); +}); + +it('errors when installing update fails', function() { + $command = mock(IgniterUpdate::class)->makePartial()->shouldAllowMockingProtectedMethods(); + $command->shouldReceive('option')->with('force')->andReturn(false); + $command->shouldReceive('option')->with('check')->andReturn(false); + $command->shouldReceive('confirmToProceed')->andReturn(true); + $output = mock(OutputStyle::class); + $command->setOutput($output); + + $composerManager = mock(ComposerManager::class); + app()->instance(ComposerManager::class, $composerManager); + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + + $composerManager->shouldReceive('assertSchema')->once(); + $updateManager->shouldReceive('setLogsOutput')->once(); + $updateManager->shouldReceive('requestUpdateList')->andReturn([ + 'items' => collect([ + (object)['code' => 'igniter.demo', 'name' => 'Demo', 'type' => 'extension'], + (object)['code' => 'igniter.theme', 'name' => 'Theme', 'type' => 'theme'], + ]), + 'count' => 2, + ]); + + $output->shouldReceive('writeln')->with('Checking for updates...')->once(); + $output->shouldReceive('writeln')->with('2 updates found')->once(); + $command->shouldReceive('option')->with('addons')->andReturnNull(); + $command->shouldReceive('option')->with('core')->andReturnNull(); + $output->shouldReceive('writeln')->with('Updating extensions/themes...'); + $updateManager->shouldReceive('install')->andThrow(new ComposerException(new Exception('Update failed'), new BufferIO())); + $output->shouldReceive('writeln') + ->withArgs(fn($message) => str_contains($message, 'Error updating composer requirements: Update failed'))->once(); + + $command->handle(); +}); + +it('bails when confirm to proceed is false', function() { + $command = mock(IgniterUpdate::class)->makePartial()->shouldAllowMockingProtectedMethods(); + $command->shouldReceive('option')->with('force')->andReturn(false); + $command->shouldReceive('option')->with('check')->andReturn(false); + $command->shouldReceive('confirmToProceed')->andReturn(false); + $output = mock(OutputStyle::class); + $command->setOutput($output); + + $composerManager = mock(ComposerManager::class); + app()->instance(ComposerManager::class, $composerManager); + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + + $composerManager->shouldReceive('assertSchema')->once(); + $updateManager->shouldReceive('setLogsOutput')->once(); + $updateManager->shouldReceive('requestUpdateList')->andReturn([ + 'items' => collect([ + (object)['code' => 'igniter.demo', 'name' => 'Demo', 'type' => 'extension'], + (object)['code' => 'igniter.theme', 'name' => 'Theme', 'type' => 'theme'], + ]), + 'count' => 2, + ]); + + $output->shouldReceive('writeln')->with('Checking for updates...')->once(); + $output->shouldReceive('writeln')->with('2 updates found')->once(); + $updateManager->shouldReceive('install')->never(); + + $command->handle(); +}); diff --git a/tests/src/System/Console/Commands/IgniterUtilTest.php b/tests/src/System/Console/Commands/IgniterUtilTest.php new file mode 100644 index 00000000..b17350aa --- /dev/null +++ b/tests/src/System/Console/Commands/IgniterUtilTest.php @@ -0,0 +1,110 @@ +artisan('igniter:util unknown') + ->expectsOutput('Utility command "unknown" does not exist!') + ->assertExitCode(0); +}); + +it('executes set version command successfully', function() { + Igniter::shouldReceive('hasDatabase')->andReturnTrue(); + Igniter::shouldReceive('version')->andReturn('2.0.0'); + $packageManifest = mock(PackageManifest::class); + app()->instance(PackageManifest::class, $packageManifest); + $packageManifest->shouldReceive('build')->andReturnSelf(); + $packageManifest->shouldReceive('packages')->andReturn([ + ['code' => 'igniter.demo', 'name' => 'Demo', 'type' => 'tastyigniter-extension', 'version' => '2.0.0'], + ['code' => 'igniter.blog', 'name' => 'Orange', 'type' => 'tastyigniter-theme', 'version' => '2.0.0'], + ]); + + $this->artisan('igniter:util set version --extensions') + ->expectsOutput('Setting TastyIgniter version number...') + ->expectsOutput('*** TastyIgniter latest version: 2.0.0') + ->expectsOutput('*** igniter.demo latest version: 2.0.0') + ->expectsOutput('*** igniter.blog latest version: 2.0.0') + ->assertExitCode(0); +}); + +it('skips set version command if no database', function() { + Igniter::shouldReceive('hasDatabase')->andReturnFalse(); + + $this->artisan('igniter:util set version') + ->expectsOutput('Setting TastyIgniter version number...') + ->expectsOutput('Skipping - No database detected.') + ->assertExitCode(0); +}); + +it('compiles scss successfully', function() { + $themeManager = mock(ThemeManager::class); + app()->instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('getActiveTheme')->andReturn(new Theme('demo')); + Assets::shouldReceive('buildBundles')->andReturn([ + 'Bundled assets compiled successfully', + ]); + + $this->artisan('igniter:util compile scss') + ->expectsOutput('Compiling registered asset bundles...') + ->expectsOutput('Bundled assets compiled successfully') + ->assertExitCode(0); +}); + +it('skips compiling scss if no active theme', function() { + $themeManager = mock(ThemeManager::class); + app()->instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('getActiveTheme')->andReturnNull(); + + $this->artisan('igniter:util compile scss') + ->expectsOutput('Compiling registered asset bundles...') + ->expectsOutput('Nothing to compile!') + ->assertExitCode(0); +}); + +it('sets carte successfully', function() { + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + $updateManager->shouldReceive('applySiteDetail')->with('carteKey')->once(); + + $this->artisan('igniter:util set carte --carteKey=carteKey') + ->expectsOutput('Setting Carte Key...') + ->assertExitCode(0); +}); + +it('skips setting carte if no key defined', function() { + $this->artisan('igniter:util set carte') + ->expectsOutput('Setting Carte Key...') + ->expectsOutput('No carteKey defined, use --key= to set a Carte') + ->assertExitCode(0); +}); + +it('sets theme successfully', function() { + $theme = ThemeModel::create([ + 'name' => 'Orange', + 'code' => 'igniter-orange', + 'version' => '1.0.0', + 'data' => [], + 'status' => 1, + ]); + + $this->artisan('igniter:util set theme --theme='.$theme->code) + ->expectsOutput('Theme ['.$theme->name.'] set as default') + ->assertExitCode(0); + + expect(ThemeModel::where('code', $theme->code)->first()->is_default)->toBeTrue(); + ThemeModel::clearDefaultModel(); +}); + +it('skips setting theme if no theme defined', function() { + $this->artisan('igniter:util set theme') + ->expectsOutput('No theme defined, use --theme= to set a theme') + ->assertExitCode(0); +}); diff --git a/tests/src/System/Console/Commands/LanguageInstallTest.php b/tests/src/System/Console/Commands/LanguageInstallTest.php new file mode 100644 index 00000000..171b03b2 --- /dev/null +++ b/tests/src/System/Console/Commands/LanguageInstallTest.php @@ -0,0 +1,60 @@ +instance(LanguageManager::class, $languageManager); + $languageManager->shouldReceive('applyLanguagePack')->andReturn([ + [ + 'name' => 'Igniter.Api', + 'code' => 'fr', + 'type' => 'module', + 'version' => '1.0.0+1', + 'hash' => 'hash', + ], + ]); + Extension::create(['name' => 'Igniter.Api', 'status' => 1, 'version' => '1.0.0']); + $languageManager->shouldReceive('installLanguagePack')->once()->with('fr', [ + 'name' => 'fr', + 'type' => 'module', + 'ver' => '1.0.0', + 'build' => '1', + 'hash' => 'hash', + ]); + + $this->artisan('igniter:language-install fr') + ->expectsOutput('1 translated strings found') + ->expectsOutput('Installing fr translated strings for Igniter.Api') + ->assertExitCode(0); +}); + +it('skips installation if no new translated strings found', function() { + $languageManager = mock(LanguageManager::class); + app()->instance(LanguageManager::class, $languageManager); + $languageManager->shouldReceive('applyLanguagePack')->andReturn([]); + + $this->artisan('igniter:language-install fr') + ->expectsOutput('No new translated strings found') + ->assertExitCode(0); +}); + +it('checks for updates only', function() { + $languageManager = mock(LanguageManager::class); + app()->instance(LanguageManager::class, $languageManager); + $languageManager->shouldReceive('applyLanguagePack')->andReturn([ + [ + 'name' => 'Igniter.Api', + 'code' => 'fr', + 'type' => 'module', + 'version' => '1.0.0+1', + 'hash' => 'hash', + ], + ]); + + $this->artisan('igniter:language-install fr --check') + ->assertExitCode(0); +}); diff --git a/tests/src/System/Console/Commands/ThemeInstallTest.php b/tests/src/System/Console/Commands/ThemeInstallTest.php new file mode 100644 index 00000000..b22890fc --- /dev/null +++ b/tests/src/System/Console/Commands/ThemeInstallTest.php @@ -0,0 +1,62 @@ +instance(UpdateManager::class, $updateManager); + $updateManager->shouldReceive('setLogsOutput')->once(); + $updateManager->shouldReceive('requestApplyItems')->with([[ + 'name' => 'demo', + 'type' => 'theme', + ]])->andReturn(collect([ + (object)['code' => 'demo', 'version' => '1.0.0'], + ])); + $updateManager->shouldReceive('install')->once(); + $themeManager = mock(ThemeManager::class); + app()->instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('loadThemes')->once(); + $themeManager->shouldReceive('installTheme')->with('demo', '1.0.0')->once(); + + $this->artisan('igniter:theme-install demo') + ->expectsOutput('Installing demo theme') + ->assertExitCode(0); +}); + +it('handles theme not found', function() { + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + $updateManager->shouldReceive('setLogsOutput')->once(); + $updateManager->shouldReceive('requestApplyItems')->with([[ + 'name' => 'demo', + 'type' => 'theme', + ]])->andReturn(collect()); + + $this->artisan('igniter:theme-install demo') + ->expectsOutput('Theme demo not found') + ->assertExitCode(0); +}); + +it('handles composer exception during installation', function() { + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + $updateManager->shouldReceive('setLogsOutput')->once(); + $updateManager->shouldReceive('requestApplyItems')->with([[ + 'name' => 'demo', + 'type' => 'theme', + ]])->andReturn(collect([ + (object)['code' => 'demo', 'version' => '1.0.0'], + ])); + $updateManager->shouldReceive('install')->andThrow(new ComposerException(new Exception('Composer error'), new BufferIO())); + + $this->artisan('igniter:theme-install demo') + ->expectsOutput('Installing demo theme') + ->expectsOutput("Error updating composer requirements: Composer error\nOutput: ") + ->assertExitCode(0); +}); diff --git a/tests/src/System/Console/Commands/ThemePublishTest.php b/tests/src/System/Console/Commands/ThemePublishTest.php new file mode 100644 index 00000000..c9f5e821 --- /dev/null +++ b/tests/src/System/Console/Commands/ThemePublishTest.php @@ -0,0 +1,69 @@ +getActiveTheme(); + $activeTheme->path = theme_path().'/demo'; + Igniter::shouldReceive('hasDatabase')->andReturnTrue(); + Igniter::shouldReceive('publishableThemeFiles')->andReturn([ + 'assets' => '/assets', + ]); + + $this->artisan('igniter:theme-publish') + ->expectsOutput('Publishing theme assets...') + ->expectsOutput('Publishing complete.') + ->assertExitCode(0); +}); + +it('skips publishing if no publishable files', function() { + $activeTheme = resolve(ThemeManager::class)->getActiveTheme(); + $activeTheme->path = theme_path().'/demo'; + Igniter::shouldReceive('hasDatabase')->andReturnTrue(); + Igniter::shouldReceive('publishableThemeFiles')->andReturn([]); + + $this->artisan('igniter:theme-publish') + ->expectsOutput('Publishing theme assets...') + ->expectsOutput('No publishable custom files for theme [tests-theme].') + ->assertExitCode(0); +}); + +it('throws exception if no active theme', function() { + config(['igniter-system.defaultTheme' => 'demo']); + + Igniter::shouldReceive('hasDatabase')->andReturnTrue(); + Igniter::shouldReceive('publishableThemeFiles')->andReturn([]); + + $this->expectException(SystemException::class); + $this->expectExceptionMessage(lang('igniter::admin.alert_error_nothing')); + + $this->artisan('igniter:theme-publish'); +}); + +it('throws exception if theme is locked', function() { + $activeTheme = resolve(ThemeManager::class)->getActiveTheme(); + $activeTheme->locked = true; + Igniter::shouldReceive('hasDatabase')->andReturnTrue(); + Igniter::shouldReceive('publishableThemeFiles')->andReturn([]); + + $this->expectException(SystemException::class); + $this->expectExceptionMessage(lang('igniter::system.themes.alert_theme_locked')); + + $this->artisan('igniter:theme-publish'); +}); + +it('throws exception if theme path is invalid', function() { + $activeTheme = resolve(ThemeManager::class)->getActiveTheme(); + $activeTheme->path = '/invalid/path'; + Igniter::shouldReceive('hasDatabase')->andReturnTrue(); + Igniter::shouldReceive('publishableThemeFiles')->andReturn([]); + + $this->expectException(SystemException::class); + $this->expectExceptionMessage(lang('igniter::system.themes.alert_no_publish_custom')); + + $this->artisan('igniter:theme-publish'); +}); diff --git a/tests/src/System/Console/Commands/ThemeRemoveTest.php b/tests/src/System/Console/Commands/ThemeRemoveTest.php new file mode 100644 index 00000000..8657bdf3 --- /dev/null +++ b/tests/src/System/Console/Commands/ThemeRemoveTest.php @@ -0,0 +1,51 @@ +instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('hasTheme')->with('tests-theme')->andReturnTrue(); + $themeManager->shouldReceive('deleteTheme')->with('tests-theme')->once(); + + $this->artisan('igniter:theme-remove tests-theme') + ->expectsConfirmation('Are you sure you want to run this command?', 'yes') + ->expectsOutput('Removing theme: tests-theme') + ->expectsOutput('Deleted theme: tests-theme') + ->assertExitCode(0); +}); + +it('handles theme not found', function() { + $themeManager = mock(ThemeManager::class); + app()->instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('hasTheme')->with('tests-theme')->andReturnFalse(); + + $this->artisan('igniter:theme-remove tests-theme') + ->expectsOutput('Unable to find a registered theme called "tests-theme"') + ->assertExitCode(0); +}); + +it('skips removal if not confirmed', function() { + $themeManager = mock(ThemeManager::class); + app()->instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('hasTheme')->with('tests-theme')->andReturnTrue(); + $themeManager->shouldReceive('deleteTheme')->never(); + + $this->artisan('igniter:theme-remove tests-theme') + ->expectsConfirmation('Are you sure you want to run this command?', 'no') + ->assertExitCode(0); +}); + +it('handles exception during removal', function() { + $themeManager = mock(ThemeManager::class); + app()->instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('hasTheme')->with('tests-theme')->andReturnTrue(); + $themeManager->shouldReceive('deleteTheme')->with('tests-theme')->andThrow(new \Exception('An error occurred')); + + $this->artisan('igniter:theme-remove tests-theme') + ->expectsConfirmation('Are you sure you want to run this command?', 'yes') + ->expectsOutput('An error occurred') + ->assertExitCode(0); +}); diff --git a/tests/src/System/Console/Commands/ThemeVendorPublishTest.php b/tests/src/System/Console/Commands/ThemeVendorPublishTest.php new file mode 100644 index 00000000..0c0a789d --- /dev/null +++ b/tests/src/System/Console/Commands/ThemeVendorPublishTest.php @@ -0,0 +1,51 @@ +instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('loadThemes'); + $themeManager->shouldReceive('listThemes')->andReturn([$theme = mock(Theme::class)]); + $theme->shouldReceive('getPathsToPublish')->andReturn([ + 'path/from/assets' => '/path/to/assets', + ]); + + $this->artisan('igniter:theme-vendor-publish') + ->expectsOutput('Publishing theme assets...') + ->expectsOutput('Publishing complete.') + ->assertExitCode(0); +}); + +it('publishes assets for a specific theme successfully', function() { + $themeManager = mock(ThemeManager::class); + app()->instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('loadThemes'); + $themeManager->shouldReceive('findTheme')->andReturn($theme = mock(Theme::class)); + $theme->shouldReceive('getPathsToPublish')->andReturn([ + 'path/from/assets' => '/path/to/assets', + ]); + + $this->artisan('igniter:theme-vendor-publish', ['--theme' => 'demo']) + ->expectsOutput('Publishing theme assets...') + ->expectsOutput('Publishing complete.') + ->assertExitCode(0); +}); + +it('handles no publishable resources for a theme', function() { + $themeManager = mock(ThemeManager::class); + app()->instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('loadThemes'); + $themeManager->shouldReceive('listThemes')->andReturn([$theme = mock(Theme::class)]); + $theme->shouldReceive('getPathsToPublish')->andReturn([]); + $theme->shouldReceive('getName')->andReturn('tests-theme'); + + $this->artisan('igniter:theme-vendor-publish') + ->expectsOutput('Publishing theme assets...') + ->expectsOutput('No publishable resources for theme [tests-theme].') + ->expectsOutput('Publishing complete.') + ->assertExitCode(0); +}); diff --git a/tests/src/System/DashboardWidgets/CacheTest.php b/tests/src/System/DashboardWidgets/CacheTest.php new file mode 100644 index 00000000..1e48c748 --- /dev/null +++ b/tests/src/System/DashboardWidgets/CacheTest.php @@ -0,0 +1,35 @@ +cacheWidget = new Cache(resolve(Dashboard::class), []); +}); + +it('renders cache widget successfully', function() { + expect($this->cacheWidget->render())->toBeString(); +}); + +it('defines widget properties correctly', function() { + $properties = $this->cacheWidget->defineProperties(); + + expect($properties)->toBe([ + 'title' => [ + 'label' => 'igniter::admin.dashboard.label_widget_title', + 'default' => 'igniter::admin.dashboard.text_cache_usage', + 'type' => 'text', + ], + ]); +}); + +it('clears cache successfully', function() { + CacheHelper::shouldReceive('clear')->once(); + + $result = $this->cacheWidget->onClearCache(); + + expect($result)->toHaveKey('#'.$this->cacheWidget->getId()); +}); diff --git a/tests/src/System/DashboardWidgets/NewsTest.php b/tests/src/System/DashboardWidgets/NewsTest.php new file mode 100644 index 00000000..e34743ce --- /dev/null +++ b/tests/src/System/DashboardWidgets/NewsTest.php @@ -0,0 +1,47 @@ +newsWidget = new News(resolve(Dashboard::class), []); +}); + +it('renders news widget successfully', function() { + expect($this->newsWidget->render())->toBeString(); +}); + +it('defines widget properties correctly', function() { + $properties = $this->newsWidget->defineProperties(); + + expect($properties)->toBe([ + 'title' => [ + 'label' => 'igniter::admin.dashboard.label_widget_title', + 'default' => 'igniter::admin.dashboard.text_news', + ], + 'newsCount' => [ + 'label' => 'igniter::admin.dashboard.text_news_count', + 'default' => 6, + 'type' => 'select', + 'options' => range(1, 10), + 'validationRule' => 'required|integer', + ], + ]); +}); + +it('loads feed items successfully', function() { + $this->newsWidget->render(); + + expect($this->newsWidget->vars['newsFeed'])->toBeArray() + ->and(count($this->newsWidget->vars['newsFeed']))->toBeLessThanOrEqual(6); +}); + +it('handles invalid RSS feed URL', function() { + $this->newsWidget->newsRss = 'https://invalid-url.com/feed'; + + $this->newsWidget->render(); + + expect($this->newsWidget->vars['newsFeed'])->toBe([]); +}); diff --git a/tests/src/System/EventSubscribers/ConsoleSubscriberTest.php b/tests/src/System/EventSubscribers/ConsoleSubscriberTest.php new file mode 100644 index 00000000..3b9980a7 --- /dev/null +++ b/tests/src/System/EventSubscribers/ConsoleSubscriberTest.php @@ -0,0 +1,82 @@ +subscribe(new Dispatcher)) + ->toHaveKey('console.schedule', 'defineSchedule') + ->toHaveKey(CommandStarting::class, 'handleCommandStarting') + ->toHaveKey(CommandFinished::class, 'handleCommandFinished'); +}); + +it('defines schedule correctly', function() { + Igniter::shouldReceive('prunableModels')->andReturn([]); + $schedule = mock(Schedule::class); + $schedule->shouldReceive('command')->with('igniter:update', ['--check' => true])->andReturnSelf(); + $schedule->shouldReceive('name')->with('System Updates Checker')->once()->andReturnSelf(); + $schedule->shouldReceive('everyThreeHours')->andReturnSelf(); + $schedule->shouldReceive('evenInMaintenanceMode')->andReturnSelf(); + $schedule->shouldReceive('command')->withSomeOfArgs('model:prune')->once()->andReturnSelf(); + $schedule->shouldReceive('name')->with('Prunable Models Checker')->andReturnSelf(); + $schedule->shouldReceive('daily')->andReturnSelf(); + + $subscriber = new ConsoleSubscriber(); + $subscriber->defineSchedule($schedule); +}); + +it('handles command starting event', function() { + $event = mock(CommandStarting::class); + $subscriber = new ConsoleSubscriber(); + + expect($subscriber->handleCommandStarting($event))->toBeNull(); +}); + +it('handles command finished event for package:discover', function() { + $event = mock(CommandFinished::class); + $event->command = 'package:discover'; + // Discover packages + $packageManifest = mock(PackageManifest::class); + app()->instance(PackageManifest::class, $packageManifest); + $filesystem = mock(Filesystem::class); + $packageManifest->files = $filesystem; + $filesystem->shouldReceive('exists')->andReturn(true); + $filesystem->shouldReceive('delete')->andReturnNull(); + $packageManifest->shouldReceive('build')->once()->andReturnSelf(); + $packageManifest->shouldReceive('packages')->andReturn([ + ['code' => 'igniter.demo', 'name' => 'Demo'], + ['code' => 'igniter.blog', 'name' => 'Blog'], + ]); + + $subscriber = new ConsoleSubscriber(); + $subscriber->handleCommandFinished($event); +}); + +it('handles command finished event for clear-compiled', function() { + $event = mock(CommandFinished::class); + $event->command = 'clear-compiled'; + CacheHelper::shouldReceive('clearCompiled')->once(); + + $subscriber = new ConsoleSubscriber(); + $subscriber->handleCommandFinished($event); +}); + +it('handles command finished event for other commands', function() { + $event = mock(CommandFinished::class); + $event->command = 'other-command'; + + $subscriber = new ConsoleSubscriber(); + expect($subscriber->handleCommandFinished($event))->toBeNull(); + // No assertions needed as the default case does nothing +}); diff --git a/tests/src/System/Fixtures/.env b/tests/src/System/Fixtures/.env new file mode 100644 index 00000000..e69de29b diff --git a/tests/src/System/Fixtures/TestBladeComponent.php b/tests/src/System/Fixtures/TestBladeComponent.php new file mode 100644 index 00000000..a9092e20 --- /dev/null +++ b/tests/src/System/Fixtures/TestBladeComponent.php @@ -0,0 +1,25 @@ + 'test::blade-component', + 'name' => 'Test Blade Component', + 'description' => 'Test blade component description', + ]; + } + + public function render() + { + return '
Test Component
'; + } +} diff --git a/tests/src/System/Fixtures/TestComponent.php b/tests/src/System/Fixtures/TestComponent.php new file mode 100644 index 00000000..30b3fd96 --- /dev/null +++ b/tests/src/System/Fixtures/TestComponent.php @@ -0,0 +1,49 @@ + 'testComponent', + 'name' => 'Test Component', + 'description' => 'Test component description', + ]; + } + + public function onAjaxHandler() + { + return ['result' => 'handler-result']; + } + + public function onAjaxHandlerWithStringResponse() + { + return 'handler-result'; + } + + public function onAjaxHandlerWithObjectResponse() + { + return response()->json(['json' => 'handler-result']); + } + + public function onAjaxHandlerWithRedirect() + { + return redirect()->to('http://localhost'); + } + + public function onAjaxHandlerWithFlash() + { + flash()->success('Flash message'); + } + + public function onAjaxHandlerWithValidationError() + { + Validator::make([], [ + 'name' => 'required', + ])->validate(); + } +} diff --git a/tests/src/System/Fixtures/TestComponentWithLifecycle.php b/tests/src/System/Fixtures/TestComponentWithLifecycle.php new file mode 100644 index 00000000..9952b4a2 --- /dev/null +++ b/tests/src/System/Fixtures/TestComponentWithLifecycle.php @@ -0,0 +1,20 @@ + 'testComponentWithLifecycle', + 'name' => 'Test Component With Lifecycle', + 'description' => 'Test component description with lifecycle methods', + ]; + } + + public function onRun() + { + return redirect()->to('http://localhost'); + } +} diff --git a/tests/src/System/Fixtures/TestExtensionSettingsModel.php b/tests/src/System/Fixtures/TestExtensionSettingsModel.php new file mode 100644 index 00000000..92a45789 --- /dev/null +++ b/tests/src/System/Fixtures/TestExtensionSettingsModel.php @@ -0,0 +1,14 @@ + [ + 'fields' => [ + 'field' => [ + 'label' => 'Field', + 'type' => 'text', + ], + ], + 'rules' => [ + 'field' => 'required', + ], + ], + ]; +} diff --git a/tests/src/System/Fixtures/TestLivewireComponent.php b/tests/src/System/Fixtures/TestLivewireComponent.php new file mode 100644 index 00000000..0b3fbd41 --- /dev/null +++ b/tests/src/System/Fixtures/TestLivewireComponent.php @@ -0,0 +1,20 @@ + 'test::livewire-component', + 'name' => 'Test Livewire Component', + 'description' => 'Test livewire component description', + ]; + } +} diff --git a/tests/src/System/Fixtures/composer.json b/tests/src/System/Fixtures/composer.json new file mode 100644 index 00000000..667b98dd --- /dev/null +++ b/tests/src/System/Fixtures/composer.json @@ -0,0 +1,22 @@ +{ + "description": "Test extension for TastyIgniter", + "require": { + "tastyigniter/core": "^v4.0@beta || ^v4.0@dev" + }, + "autoload": { + "psr-4": { + "Igniter\\TestExtension\\": "src/" + } + }, + "extra": { + "tastyigniter-extension": { + "code": "igniter.testextension", + "name": "Test Extension", + "icon": { + "class": "fa fa-cloud", + "color": "#fff", + "backgroundColor": "#02586F" + } + } + } +} diff --git a/tests/src/System/Helpers/CacheHelperTest.php b/tests/src/System/Helpers/CacheHelperTest.php new file mode 100644 index 00000000..57e271bd --- /dev/null +++ b/tests/src/System/Helpers/CacheHelperTest.php @@ -0,0 +1,76 @@ +once(); + File::shouldReceive('glob')->andReturn([]); + File::shouldReceive('isDirectory')->andReturn(true); + File::shouldReceive('directories')->andReturn([]); + File::shouldReceive('delete')->andReturn(true); + File::shouldReceive('deleteDirectory')->andReturn(true); + + $cacheHelper->clear(); +}); + +it('clears view cache successfully', function() { + $cacheHelper = new CacheHelper(); + $path = config('view.compiled'); + File::shouldReceive('glob')->with("{$path}/*")->andReturn(['view1.php', 'view2.php']); + File::shouldReceive('delete')->with('view1.php')->once(); + File::shouldReceive('delete')->with('view2.php')->once(); + + $cacheHelper->clearView(); +}); + +it('clears combiner cache successfully', function() { + $cacheHelper = new CacheHelper(); + File::shouldReceive('isDirectory')->with(storage_path().'/igniter/combiner')->andReturn(true); + File::shouldReceive('directories')->with(storage_path().'/igniter/combiner')->andReturn(['dir1', 'dir2']); + File::shouldReceive('deleteDirectory')->with('dir1')->once(); + File::shouldReceive('deleteDirectory')->with('dir2')->once(); + + $cacheHelper->clearCombiner(); +}); + +it('clears cache directory successfully', function() { + $cacheHelper = new CacheHelper(); + $path = config('igniter-pagic.parsedTemplateCachePath', storage_path('/igniter/cache')); + File::shouldReceive('isDirectory')->with($path)->andReturn(true); + File::shouldReceive('directories')->with($path)->andReturn(['dir1', 'dir2']); + File::shouldReceive('deleteDirectory')->with('dir1')->once(); + File::shouldReceive('deleteDirectory')->with('dir2')->once(); + + $cacheHelper->clearCache(); +}); + +it('does not clear when directory does not exists', function() { + config(['igniter-pagic.parsedTemplateCachePath' => storage_path('/non_existent')]); + $cacheHelper = new CacheHelper(); + File::shouldReceive('isDirectory')->with(storage_path().'/non_existent')->once()->andReturn(false); + + $cacheHelper->clearCache(); +}); + +it('clears compiled files successfully', function() { + $cacheHelper = new CacheHelper(); + File::shouldReceive('delete')->with(Igniter::getCachedAddonsPath())->once(); + File::shouldReceive('delete')->with(App::getCachedPackagesPath())->once(); + File::shouldReceive('delete')->with(App::getCachedServicesPath())->once(); + + $cacheHelper->clearCompiled(); +}); + +it('does not clear non-existent directory', function() { + $cacheHelper = new CacheHelper(); + File::shouldReceive('isDirectory')->with(storage_path().'/non_existent')->once()->andReturn(false); + + $cacheHelper->clearDirectory('/non_existent'); +}); diff --git a/tests/src/System/Helpers/MailHelperTest.php b/tests/src/System/Helpers/MailHelperTest.php new file mode 100644 index 00000000..4e8226be --- /dev/null +++ b/tests/src/System/Helpers/MailHelperTest.php @@ -0,0 +1,40 @@ +once()->andReturn(true); + $result = (new MailHelper)->sendTemplate('view', ['key' => 'value']); + + expect($result)->toBeTrue(); +}); + +it('queues template email successfully', function() { + Mail::shouldReceive('queue')->once()->andReturn(true); + $result = (new MailHelper)->queueTemplate('view', ['key' => 'value']); + + expect($result)->toBeTrue(); +}); + +it('sends template email with callback', function() { + Mail::shouldReceive('send')->once()->andReturn(true); + $callback = function($message) { + $message->subject('Test Subject'); + }; + $result = (new MailHelper)->sendTemplate('view', ['key' => 'value'], $callback); + + expect($result)->toBeTrue(); +}); + +it('queues template email with callback', function() { + Mail::shouldReceive('queue')->once()->andReturn(true); + $callback = function($message) { + $message->subject('Test Subject'); + }; + $result = (new MailHelper)->queueTemplate('view', ['key' => 'value'], $callback); + + expect($result)->toBeTrue(); +}); diff --git a/tests/src/System/Helpers/SystemHelperTest.php b/tests/src/System/Helpers/SystemHelperTest.php new file mode 100644 index 00000000..96d1152d --- /dev/null +++ b/tests/src/System/Helpers/SystemHelperTest.php @@ -0,0 +1,110 @@ +phpVersion(); + + expect($version)->toBe(PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION); +}); + +it('returns correct extension version', function() { + $version = (new SystemHelper)->extensionVersion('json'); + + expect($version)->toBe(phpversion('json')); +}); + +it('normalizes version correctly', function() { + $version = (new SystemHelper)->normalizeVersion('7.4.3-1+ubuntu18.04.1+deb.sury.org+1'); + + expect($version)->toBe('7.4.3'); +}); + +it('asserts ini_set works', function() { + $result = (new SystemHelper)->assertIniSet(); + + expect($result)->toBeTrue(); +}); + +it('asserts ini max execution time is less than 120', function() { + $oldValue = ini_get('max_execution_time'); + ini_set('max_execution_time', 100); + + $result = (new SystemHelper)->assertIniMaxExecutionTime(120); + + expect($result)->toBeTrue(); + ini_set('max_execution_time', $oldValue); +}); + +it('asserts ini memory limit is less than 256MB', function() { + expect((new SystemHelper)->assertIniMemoryLimit(250))->toBeBool(); +}); + +it('retrieves PHP ini value as bool', function() { + $oldValue = ini_get('display_errors'); + ini_set('display_errors', 1); + + $result = (new SystemHelper)->phpIniValueAsBool('display_errors'); + + expect($result)->toBeTrue(); + ini_set('display_errors', $oldValue); +}); + +it('retrieves PHP ini value in bytes', function() { + $result = (new SystemHelper)->phpIniValueInBytes('memory_limit'); + + expect($result)->not()->toBeNull(); +}); + +it('normalizes PHP size in bytes', function() { + expect((new SystemHelper)->phpSizeInBytes('2G'))->toBe(2 * 1024 * 1024 * 1024) + ->and((new SystemHelper)->phpSizeInBytes('2M'))->toBe(2 * 1024 * 1024) + ->and((new SystemHelper)->phpSizeInBytes('2K'))->toBe(2 * 1024); +}); + +it('replaces value in env file', function() { + File::shouldReceive('put')->once(); + File::shouldReceive('get')->andReturn('APP_ENV=local'); + + (new SystemHelper)->replaceInEnv('APP_ENV', 'APP_ENV=production'); +}); + +it('throws exception for unsupported extension config file', function() { + File::shouldReceive('exists')->with('/path/extension.json')->andReturn(true); + + expect(fn() => (new SystemHelper)->extensionConfigFromFile('/path'))->toThrow(SystemException::class); +}); + +it('validates extension config correctly', function() { + $config = [ + 'code' => 'test.extension', + 'name' => 'Test Extension', + 'description' => 'This is a test extension', + ]; + + $result = (new SystemHelper)->extensionValidateConfig($config); + + expect($result)->toBe($config); +}); + +it('detects running on Windows', function() { + $result = (new SystemHelper)->runningOnWindows(); + + expect($result)->toBe(PHP_OS_FAMILY === 'Windows'); +}); + +it('detects running on Mac', function() { + $result = (new SystemHelper)->runningOnMac(); + + expect($result)->toBe(PHP_OS_FAMILY === 'Darwin'); +}); + +it('detects running on Linux', function() { + $result = (new SystemHelper)->runningOnLinux(); + + expect($result)->toBe(PHP_OS_FAMILY === 'Linux'); +}); diff --git a/tests/src/System/Http/Controllers/AssetControllerTest.php b/tests/src/System/Http/Controllers/AssetControllerTest.php new file mode 100644 index 00000000..a4640177 --- /dev/null +++ b/tests/src/System/Http/Controllers/AssetControllerTest.php @@ -0,0 +1,20 @@ +with('combined')->once()->andReturn(new Response('combined-contents')); + + $response = $this->get('/_assets/combined-cache-key'); + expect($response->getContent())->toBe('combined-contents'); +}); + +it('returns combined contents of admin assets', function() { + Assets::shouldReceive('combineGetContents')->with('combined')->once()->andReturn(new Response('combined-contents')); + + $response = $this->get('/admin/_assets/combined-cache-key'); + expect($response->getContent())->toBe('combined-contents'); +}); diff --git a/tests/src/System/Http/Controllers/CountriesTest.php b/tests/src/System/Http/Controllers/CountriesTest.php new file mode 100644 index 00000000..0ffd6e09 --- /dev/null +++ b/tests/src/System/Http/Controllers/CountriesTest.php @@ -0,0 +1,108 @@ +get(route('igniter.system.countries')) + ->assertOk(); +}); + +it('loads create country page', function() { + actingAsSuperUser() + ->get(route('igniter.system.countries')) + ->assertOk(); +}); + +it('loads edit country page', function() { + $country = Country::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.system.countries', ['slug' => 'edit/'.$country->getKey()])) + ->assertOk(); +}); + +it('loads country preview page', function() { + $country = Country::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.system.countries', ['slug' => 'edit/'.$country->getKey()])) + ->assertOk(); +}); + +it('sets a default country', function() { + $country = Country::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.system.countries'), [ + 'default' => $country->getKey(), + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSetDefault', + ]); + + Country::clearDefaultModel(); + expect(Country::getDefaultKey())->toBe($country->getKey()); +}); + +it('creates country', function() { + actingAsSuperUser() + ->post(route('igniter.system.countries', ['slug' => 'create']), [ + 'country_name' => 'Test United States', + 'priority' => 1, + 'iso_code_2' => 'US', + 'iso_code_3' => 'USA', + 'status' => 1, + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(Country::where('country_name', 'Test United States')->exists())->toBeTrue(); +}); + +it('updates country', function() { + $country = Country::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.system.countries', ['slug' => 'edit/'.$country->getKey()]), [ + 'country_name' => 'United States', + 'priority' => 1, + 'iso_code_2' => 'US', + 'iso_code_3' => 'USA', + 'status' => 1, + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(Country::find($country->getKey())->country_name)->toBe('United States'); +}); + +it('deletes country', function() { + $country = Country::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.system.countries', ['slug' => 'edit/'.$country->getKey()]), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]); + + expect(Country::find($country->getKey()))->toBeNull(); +}); + +it('deletes multiple countries', function() { + $countries = Country::factory()->count(3)->create(); + + actingAsSuperUser() + ->post(route('igniter.system.countries'), [ + 'checked' => $countries->pluck('country_id')->all(), + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]); + + expect(Country::whereIn('country_id', $countries->pluck('country_id')->all())->exists())->toBeFalse(); +}); diff --git a/tests/src/System/Http/Controllers/CurrenciesTest.php b/tests/src/System/Http/Controllers/CurrenciesTest.php new file mode 100644 index 00000000..000f87de --- /dev/null +++ b/tests/src/System/Http/Controllers/CurrenciesTest.php @@ -0,0 +1,98 @@ +get(route('igniter.system.currencies')) + ->assertOk(); +}); + +it('loads create currency page', function() { + actingAsSuperUser() + ->get(route('igniter.system.currencies', ['slug' => 'create'])) + ->assertOk(); +}); + +it('loads edit currency page', function() { + $currency = Currency::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.system.currencies', ['slug' => 'edit/'.$currency->getKey()])) + ->assertOk(); +}); + +it('loads currency preview page', function() { + $currency = Currency::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.system.currencies', ['slug' => 'preview/'.$currency->getKey()])) + ->assertOk(); +}); + +it('sets a default currency', function() { + $currency = Currency::factory()->create(['currency_status' => 1]); + + actingAsSuperUser() + ->post(route('igniter.system.currencies'), [ + 'default' => $currency->getKey(), + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSetDefault', + ]); + + Currency::clearDefaultModel(); + expect(Currency::getDefaultKey())->toBe($currency->getKey()); +}); + +it('creates currency', function() { + actingAsSuperUser() + ->post(route('igniter.system.currencies', ['slug' => 'create']), [ + 'Currency' => [ + 'currency_name' => 'Test United States', + 'currency_code' => 'USD', + 'currency_symbol' => '$', + 'country_id' => 1, + 'currency_status' => 1, + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(Currency::where('currency_name', 'Test United States')->exists())->toBeTrue(); +}); + +it('updates currency', function() { + $currency = Currency::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.system.currencies', ['slug' => 'edit/'.$currency->getKey()]), [ + 'Currency' => [ + 'currency_name' => 'Test United States', + 'currency_code' => 'USD', + 'currency_symbol' => '$', + 'country_id' => 1, + 'currency_status' => 1, + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(Currency::where('currency_name', 'Test United States')->exists())->toBeTrue(); +}); + +it('deletes currency', function() { + $currency = Currency::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.system.currencies', ['slug' => 'edit/'.$currency->getKey()]), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]); + + expect(Currency::find($currency->getKey()))->toBeNull(); +}); diff --git a/tests/src/System/Http/Controllers/ExtensionsTest.php b/tests/src/System/Http/Controllers/ExtensionsTest.php new file mode 100644 index 00000000..d206654b --- /dev/null +++ b/tests/src/System/Http/Controllers/ExtensionsTest.php @@ -0,0 +1,391 @@ +get(route('igniter.system.extensions')) + ->assertOk(); +}); + +it('loads extension settings page', function() { + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('getExtensions')->andReturn([ + 'igniter.tests' => getExtension(), + ]); + + actingAsSuperUser() + ->get(route('igniter.system.extensions', ['slug' => 'edit/igniter/tests/settings'])) + ->assertOk(); +}); + +it('loads extension delete page', function() { + Extension::create([ + 'name' => 'igniter.tests', + 'status' => 1, + ]); + $extensionManager = mock(ExtensionManager::class)->makePartial(); + app()->instance(ExtensionManager::class, $extensionManager); + $extension = mock(BaseExtension::class); + $extensionManager->shouldReceive('findExtension')->andReturn($extension); + $extension->disabled = true; + $extension->shouldReceive('extensionMeta')->andReturn([ + 'code' => 'Igniter.Tests', + 'name' => 'Igniter Tests', + 'description' => 'Test extension', + ]); + + actingAsSuperUser() + ->get(route('igniter.system.extensions', ['slug' => 'delete/igniter.tests'])) + ->assertOk(); +}); + +it('fails to load extension delete page when extension is not found', function() { + Extension::create([ + 'name' => 'igniter.tests', + 'status' => 1, + ]); + $extensionManager = mock(ExtensionManager::class)->makePartial(); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('findExtension')->andReturnNull(); + $extensionManager->shouldReceive('deleteExtension')->once(); + + actingAsSuperUser() + ->get(route('igniter.system.extensions', ['slug' => 'delete/igniter.tests'])) + ->assertRedirect(); +}); + +it('fails to load extension delete page when extension is not disabled', function() { + Extension::create([ + 'name' => 'igniter.tests', + 'status' => 1, + ]); + $extensionManager = mock(ExtensionManager::class)->makePartial(); + app()->instance(ExtensionManager::class, $extensionManager); + $extension = mock(BaseExtension::class); + $extensionManager->shouldReceive('findExtension')->andReturn($extension); + $extension->disabled = false; + + actingAsSuperUser() + ->get(route('igniter.system.extensions', ['slug' => 'delete/igniter.tests'])) + ->assertRedirect(); +}); + +it('loads extension readme', function() { + $extension = Extension::create([ + 'name' => 'Igniter.Tests', + 'status' => 1, + ]); + + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [ + 'recordId' => $extension->getKey(), + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onLoadReadme', + ]) + ->assertOk(); +}); + +it('fails to load extension readme when request is invalid', function() { + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [ + 'recordId' => null, + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onLoadReadme', + ]) + ->assertStatus(406); +}); + +it('installs extension', function() { + $extensionManager = mock(ExtensionManager::class)->makePartial(); + app()->instance(ExtensionManager::class, $extensionManager); + $extension = mock(BaseExtension::class); + $extensionManager->shouldReceive('findExtension')->andReturn($extension); + $extensionManager->shouldReceive('installExtension')->once()->andReturnTrue(); + $extension->shouldReceive('extensionMeta')->andReturn([ + 'code' => 'Igniter.Tests', + 'name' => 'Igniter Tests', + 'description' => 'Test extension', + ]); + + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [ + 'code' => 'igniter.tests', + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onInstall', + ]) + ->assertOk(); +}); + +it('fails to install extension when request is invalid', function() { + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [ + 'code' => null, + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onInstall', + ]) + ->assertStatus(406); +}); + +it('flashes error when installation fails', function() { + $extensionManager = mock(ExtensionManager::class)->makePartial(); + app()->instance(ExtensionManager::class, $extensionManager); + $extension = mock(BaseExtension::class); + $extensionManager->shouldReceive('findExtension')->andReturn($extension); + $extensionManager->shouldReceive('installExtension')->once()->andReturnFalse(); + + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [ + 'code' => 'igniter.tests', + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onInstall', + ]); + + expect(flash()->messages()->first())->message->toBe(lang('igniter::admin.alert_error_try_again')); +}); + +it('uninstalls extension', function() { + Extension::create([ + 'name' => 'Igniter.Tests', + 'status' => 1, + ]); + $extensionManager = mock(ExtensionManager::class)->makePartial(); + app()->instance(ExtensionManager::class, $extensionManager); + $extension = mock(BaseExtension::class); + $extensionManager->shouldReceive('findExtension')->andReturn($extension); + $extensionManager->shouldReceive('isRequired')->andReturnFalse(); + $extensionManager->shouldReceive('uninstallExtension')->once()->andReturnTrue(); + $extension->shouldReceive('extensionMeta')->andReturn([ + 'code' => 'Igniter.Tests', + 'name' => 'Igniter Tests', + 'description' => 'Test extension', + ]); + + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [ + 'code' => 'igniter.tests', + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onUninstall', + ]) + ->assertOk(); +}); + +it('fails to uninstall extension when request is invalid', function() { + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [ + 'code' => null, + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onUninstall', + ]) + ->assertStatus(406); +}); + +it('flashes error when uninstallation fails', function() { + Extension::create([ + 'name' => 'Igniter.Tests', + 'status' => 1, + ]); + $extensionManager = mock(ExtensionManager::class)->makePartial(); + app()->instance(ExtensionManager::class, $extensionManager); + $extension = mock(BaseExtension::class); + $extensionManager->shouldReceive('findExtension')->andReturn($extension); + $extensionManager->shouldReceive('uninstallExtension')->once()->andReturnFalse(); + + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [ + 'code' => 'igniter.tests', + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onUninstall', + ]); + + expect(flash()->messages()->first())->message->toBe(lang('igniter::admin.alert_error_try_again')); +}); + +it('updates extension settings', function() { + Extension::create([ + 'name' => 'Igniter.Tests', + 'status' => 1, + ]); + TestExtensionSettingsModel::clearInternalCache(); + TestExtensionSettingsModel::set('name', 'value'); + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('getExtensions')->andReturn([ + 'igniter.tests' => getExtension(), + ]); + + actingAsSuperUser() + ->post(route('igniter.system.extensions', ['slug' => 'edit/igniter/tests/settings']), [ + 'TestExtensionSettingsModel' => [ + 'name' => 'value', + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]) + ->assertOk(); +}); + +it('flashes error when updates extension settings fails validation', function() { + Extension::create([ + 'name' => 'Igniter.Tests', + 'status' => 1, + ]); + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('getExtensions')->andReturn([ + 'igniter.tests' => getExtensionWithSettingsRules(), + ]); + + actingAsSuperUser() + ->post(route('igniter.system.extensions', ['slug' => 'edit/igniter/tests/settings']), [ + 'TestExtensionSettingsWithRulesModel' => [ + 'name' => 'value', + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]) + ->assertOk(); +}); + +it('updates extension settings and redirects to settings page', function() { + Extension::create([ + 'name' => 'Igniter.Tests', + 'status' => 1, + ]); + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('getExtensions')->andReturn([ + 'igniter.tests' => getExtension(), + ]); + + actingAsSuperUser() + ->post(route('igniter.system.extensions', ['slug' => 'edit/igniter/tests/settings']), [ + 'close' => '1', + 'TestExtensionSettingsModel' => [ + 'name' => 'value', + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]) + ->assertOk(); +}); + +it('deletes extension', function() { + Extension::create([ + 'name' => 'Igniter.Tests', + 'status' => 1, + ]); + $extensionManager = mock(ExtensionManager::class)->makePartial(); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('findExtension')->andReturn(getExtension()); + $extensionManager->shouldReceive('deleteExtension')->once()->andReturnTrue(); + + actingAsSuperUser() + ->post(route('igniter.system.extensions', ['slug' => 'delete/igniter.tests']), [ + 'recordId' => 1, + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]) + ->assertOk(); +}); + +it('fails to delete extension when request is invalid', function() { + $extensionManager = mock(ExtensionManager::class)->makePartial(); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('findExtension')->andReturnNull(); + + actingAsSuperUser() + ->post(route('igniter.system.extensions', ['slug' => 'delete/igniter.tests']), [ + 'recordId' => null, + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]) + ->assertStatus(406); +}); + +function getExtension() +{ + return new class(app()) extends BaseExtension + { + public function extensionMeta(): array + { + return [ + 'code' => 'Igniter.Tests', + 'name' => 'Igniter Tests', + 'description' => 'Test extension', + ]; + } + + public function register() {} + + public function registerSettings(): array + { + return [ + 'settings' => [ + 'label' => 'Test Extension Settings', + 'icon' => 'fa fa-cog', + 'description' => 'Manage test extension settings.', + 'model' => TestExtensionSettingsModel::class, + 'request' => TestRequest::class, + 'permissions' => ['Igniter.Tests.*'], + ], + ]; + } + }; +} + +function getExtensionWithSettingsRules() +{ + return new class(app()) extends BaseExtension + { + public function extensionMeta(): array + { + return [ + 'code' => 'Igniter.Tests', + 'name' => 'Igniter Tests', + 'description' => 'Test extension', + ]; + } + + public function register() {} + + public function registerSettings(): array + { + return [ + 'settings' => [ + 'label' => 'Test Extension Settings', + 'icon' => 'fa fa-cog', + 'description' => 'Manage test extension settings.', + 'model' => TestExtensionSettingsWithRulesModel::class, + 'permissions' => ['Igniter.Tests.*'], + ], + ]; + } + }; +} diff --git a/tests/src/System/Http/Controllers/LanguagesTest.php b/tests/src/System/Http/Controllers/LanguagesTest.php new file mode 100644 index 00000000..03006190 --- /dev/null +++ b/tests/src/System/Http/Controllers/LanguagesTest.php @@ -0,0 +1,266 @@ +get(route('igniter.system.languages')) + ->assertOk(); +}); + +it('searches languages successfully', function() { + $language = Language::factory()->create(); + $languageManager = mock(LanguageManager::class); + app()->instance(LanguageManager::class, $languageManager); + $languageManager->shouldReceive('searchLanguages')->with($language->name)->andReturn([ + [ + 'name' => $language->name, + 'code' => $language->code, + 'icon' => [ + 'url' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABKklEQVR42mNk', + 'class' => 'flag-icon flag-icon-gb', + 'style' => 'width: 16px; height: 11px;', + ], + 'description' => 'description', + ], + ]); + + actingAsSuperUser() + ->get(route('igniter.system.languages', ['slug' => 'search']) + .'?'.http_build_query(['filter' => ['search' => $language->name]])) + ->assertOk() + ->assertSee($language->name); +}); + +it('returns empty array when search filter is invalid', function() { + actingAsSuperUser() + ->get(route('igniter.system.languages', ['slug' => 'search']) + .'?'.http_build_query(['filter' => []])) + ->assertOk() + ->assertJson([]); +}); + +it('loads languages create page', function() { + actingAsSuperUser() + ->get(route('igniter.system.languages', ['slug' => 'create'])) + ->assertOk(); +}); + +it('loads languages edit page', function() { + $language = Language::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.system.languages', ['slug' => 'edit/'.$language->getKey()])) + ->assertOk(); +}); + +it('sets default language successfully', function() { + $language = Language::factory()->create(['status' => 1]); + + actingAsSuperUser() + ->post(route('igniter.system.languages'), [ + 'default' => $language->code, + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSetDefault', + ]) + ->assertOk(); + + Language::clearDefaultModel(); + expect(Language::getDefault()->getKey())->toBe($language->getKey()); +}); + +it('filters language translations successfully', function() { + $language = Language::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.system.languages', ['slug' => 'edit/'.$language->getKey()]), [ + 'Language' => [ + '_group' => 'igniter.user', + '_search' => 'Text to search', + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSubmitFilter', + ]) + ->assertOk(); +}); + +it('checks for language pack updates', function() { + $language = Language::factory()->create(); + $languageManager = mock(LanguageManager::class); + app()->instance(LanguageManager::class, $languageManager); + $languageManager->shouldReceive('applyLanguagePack')->once()->with($language->code, [])->andReturn([ + [ + 'code' => $language->code, + 'name' => $language->name, + 'locale' => $language->code, + 'version' => '1.0.0', + 'description' => 'description', + 'icon' => [ + 'url' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABKklEQVR42mNk', + 'class' => 'flag-icon flag-icon-gb', + 'style' => 'width: 16px; height: 11px;', + ], + ], + ]); + + actingAsSuperUser() + ->post(route('igniter.system.languages', ['slug' => 'edit/'.$language->getKey()]), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onCheckUpdates', + ]) + ->assertOk(); +}); + +it('publishes translated strings', function() { + $language = Language::factory()->create(); + $languageManager = mock(LanguageManager::class); + app()->instance(LanguageManager::class, $languageManager); + $languageManager->shouldReceive('publishTranslations')->once(); + + actingAsSuperUser() + ->post(route('igniter.system.languages', ['slug' => 'edit/'.$language->getKey()]), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onPublishTranslations', + ]) + ->assertOk(); +}); + +it('applies marketplace locale items', function() { + $languageManager = mock(LanguageManager::class); + app()->instance(LanguageManager::class, $languageManager); + $languageManager->shouldReceive('findLanguage')->once()->with('fr_FR')->andReturn([ + 'code' => 'fr', + 'locale' => 'fr_FR', + 'name' => 'French', + 'version' => '1.0.0', + ]); + $languageManager->shouldReceive('applyLanguagePack')->once()->with('fr_FR')->andReturn($applyResponse = [ + 'code' => 'fr', + 'locale' => 'fr_FR', + 'name' => 'French', + 'version' => '1.0.0', + 'description' => 'description', + 'icon' => [ + 'url' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABKklEQVR42mNk', + 'class' => 'flag-icon flag-icon-gb', + 'style' => 'width: 16px; height: 11px;', + ], + ]); + + actingAsSuperUser() + ->post(route('igniter.system.languages'), [ + 'items' => [ + [ + 'name' => $applyResponse['locale'], + 'type' => 'extension', + 'ver' => '1.0.0', + 'action' => 'update', + ], + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onApplyItems', + ]) + ->assertOk() + ->assertJson([ + 'steps' => [ + 'update-'.$applyResponse['code'] => [ + 'meta' => $applyResponse, + 'process' => 'update-'.$applyResponse['code'], + 'progress' => sprintf(lang('igniter::system.languages.alert_update_progress'), + $applyResponse['locale'], $applyResponse['name'], + ), + ], + ], + ]); +}); + +it('flashes error when missing items to apply in request', function() { + actingAsSuperUser() + ->post(route('igniter.system.languages'), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onApplyItems', + ]) + ->assertStatus(406); +}); + +it('applies update for marketplace locale translated strings', function() { + $language = Language::factory()->create(); + $languageManager = mock(LanguageManager::class); + app()->instance(LanguageManager::class, $languageManager); + $languageManager->shouldReceive('applyLanguagePack')->once()->with($language->code, [])->andReturn([ + $applyResponse = [ + 'code' => $language->code, + 'locale' => 'fr_FR', + 'name' => 'French', + 'version' => '1.0.0', + 'description' => 'description', + 'icon' => [ + 'url' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABKklEQVR42mNk', + 'class' => 'flag-icon flag-icon-gb', + 'style' => 'width: 16px; height: 11px;', + ], + ], + ]); + + actingAsSuperUser() + ->post(route('igniter.system.languages', ['slug' => 'edit/'.$language->getKey()]), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onApplyUpdate', + ]) + ->assertOk() + ->assertJson([ + 'steps' => [ + 'update-'.$language->code => [ + 'meta' => $applyResponse, + 'process' => 'update-'.$language->code, + 'progress' => sprintf(lang('igniter::system.languages.alert_update_progress'), + $applyResponse['locale'], $applyResponse['name'], + ), + ], + ], + ]); +}); + +it('processes update for marketplace locale translated strings', function() { + $language = Language::factory()->create(); + $languageManager = mock(LanguageManager::class); + app()->instance(LanguageManager::class, $languageManager); + $languageManager->shouldReceive('installLanguagePack')->once()->with($language->code, [ + 'name' => $language->code, + 'type' => 'extension', + 'ver' => '1.0.0', + 'build' => '383', + 'hash' => 'download-file-hash', + ]); + + actingAsSuperUser() + ->post(route('igniter.system.languages', ['slug' => 'edit/'.$language->getKey()]), [ + 'process' => 'update-'.$language->code, + 'meta' => [ + 'code' => $language->code, + 'name' => $language->code, + 'author' => 'Author', + 'type' => 'extension', + 'version' => '1.0.0+383', + 'hash' => 'download-file-hash', + 'description' => 'description', + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onProcessItems', + ]) + ->assertOk() + ->assertJson([ + 'success' => true, + 'message' => sprintf(lang('igniter::system.languages.alert_update_complete'), $language->code, $language->code), + ]); +}); + + + diff --git a/tests/src/System/Http/Controllers/MailLayoutsTest.php b/tests/src/System/Http/Controllers/MailLayoutsTest.php new file mode 100644 index 00000000..ebde85fc --- /dev/null +++ b/tests/src/System/Http/Controllers/MailLayoutsTest.php @@ -0,0 +1,92 @@ +get(route('igniter.system.mail_layouts')) + ->assertOk(); +}); + +it('loads mail layouts create page', function() { + actingAsSuperUser() + ->get(route('igniter.system.mail_layouts', ['slug' => 'create'])) + ->assertOk(); +}); + +it('loads mail layouts edit page', function() { + $mailLayout = MailLayout::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.system.mail_layouts', ['slug' => 'edit/'.$mailLayout->getKey()])) + ->assertOk(); +}); + +it('loads mail layouts preview page', function() { + $mailLayout = MailLayout::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.system.mail_layouts', ['slug' => 'edit/'.$mailLayout->getKey()])) + ->assertOk(); +}); + +it('creates mail layout', function() { + actingAsSuperUser() + ->post(route('igniter.system.mail_layouts', ['slug' => 'create']), [ + 'MailLayout' => [ + 'name' => 'Test Layout', + 'code' => 'test_layout', + 'layout' => 'Test Layout Content', + 'language_id' => 1, + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]) + ->assertOk(); + + $this->assertDatabaseHas('mail_layouts', [ + 'name' => 'Test Layout', + 'code' => 'test_layout', + 'layout' => 'Test Layout Content', + ]); +}); + +it('updates mail layout', function() { + $mailLayout = MailLayout::factory()->create(['code' => 'default']); + + actingAsSuperUser() + ->post(route('igniter.system.mail_layouts', ['slug' => 'edit/'.$mailLayout->getKey()]), [ + 'MailLayout' => [ + 'name' => 'Updated Layout', + 'code' => 'updated_layout', + 'layout' => 'Updated Body', + 'language_id' => 1, + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]) + ->assertOk(); + + $this->assertDatabaseHas('mail_layouts', [ + 'name' => 'Updated Layout', + 'code' => $mailLayout->code, + 'layout' => 'Updated Body', + ]); +}); + +it('deletes mail layout', function() { + $mailLayout = MailLayout::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.system.mail_layouts', ['slug' => 'edit/'.$mailLayout->getKey()]), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]) + ->assertOk(); + + expect(MailLayout::find($mailLayout->getKey()))->toBeNull(); +}); diff --git a/tests/src/System/Http/Controllers/MailPartialsTest.php b/tests/src/System/Http/Controllers/MailPartialsTest.php new file mode 100644 index 00000000..8e2a9ac9 --- /dev/null +++ b/tests/src/System/Http/Controllers/MailPartialsTest.php @@ -0,0 +1,90 @@ +get(route('igniter.system.mail_partials')) + ->assertOk(); +}); + +it('loads mail partials create page', function() { + actingAsSuperUser() + ->get(route('igniter.system.mail_partials', ['slug' => 'create'])) + ->assertOk(); +}); + +it('loads mail partials edit page', function() { + $mailPartial = MailPartial::create(); + + actingAsSuperUser() + ->get(route('igniter.system.mail_partials', ['slug' => 'edit/'.$mailPartial->getKey()])) + ->assertOk(); +}); + +it('loads mail partials preview page', function() { + $mailPartial = MailPartial::create(); + + actingAsSuperUser() + ->get(route('igniter.system.mail_partials', ['slug' => 'edit/'.$mailPartial->getKey()])) + ->assertOk(); +}); + +it('creates mail partial', function() { + actingAsSuperUser() + ->post(route('igniter.system.mail_partials', ['slug' => 'create']), [ + 'MailPartial' => [ + 'name' => 'Test Partial', + 'code' => 'test_partial', + 'html' => 'Test Partial Content', + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]) + ->assertOk(); + + $this->assertDatabaseHas('mail_partials', [ + 'name' => 'Test Partial', + 'code' => 'test_partial', + 'html' => 'Test Partial Content', + ]); +}); + +it('updates mail partial', function() { + $mailPartial = MailPartial::create(['code' => 'test_partial']); + + actingAsSuperUser() + ->post(route('igniter.system.mail_partials', ['slug' => 'edit/'.$mailPartial->getKey()]), [ + 'MailPartial' => [ + 'name' => 'Test Partial', + 'code' => 'test_partial', + 'html' => 'Test Partial Content', + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]) + ->assertOk(); + + $this->assertDatabaseHas('mail_partials', [ + 'name' => 'Test Partial', + 'code' => 'test_partial', + 'html' => 'Test Partial Content', + ]); +}); + +it('deletes mail partial', function() { + $mailPartial = MailPartial::create(); + + actingAsSuperUser() + ->post(route('igniter.system.mail_partials', ['slug' => 'edit/'.$mailPartial->getKey()]), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]) + ->assertOk(); + + $this->assertDatabaseMissing('mail_partials', ['partial_id' => $mailPartial->getKey()]); +}); diff --git a/tests/src/System/Http/Controllers/MailTemplatesTest.php b/tests/src/System/Http/Controllers/MailTemplatesTest.php new file mode 100644 index 00000000..91d402b9 --- /dev/null +++ b/tests/src/System/Http/Controllers/MailTemplatesTest.php @@ -0,0 +1,118 @@ +get(route('igniter.system.mail_templates')) + ->assertOk(); +}); + +it('loads mail templates create page', function() { + actingAsSuperUser() + ->get(route('igniter.system.mail_templates', ['slug' => 'create'])) + ->assertOk(); +}); + +it('loads mail templates edit page', function() { + $mailTemplate = MailTemplate::create(); + + actingAsSuperUser() + ->get(route('igniter.system.mail_templates', ['slug' => 'edit/'.$mailTemplate->getKey()])) + ->assertOk(); +}); + +it('loads mail templates preview page', function() { + $mailTemplate = MailTemplate::create(); + + actingAsSuperUser() + ->get(route('igniter.system.mail_templates', ['slug' => 'edit/'.$mailTemplate->getKey()])) + ->assertOk(); +}); + +it('creates mail template', function() { + actingAsSuperUser() + ->post(route('igniter.system.mail_templates', ['slug' => 'create']), [ + 'MailTemplate' => [ + 'code' => '_mail.test_template', + 'label' => 'Test Template Subject', + 'subject' => 'Test Template Subject', + 'plain_body' => 'Test Template Text Body', + 'body' => 'Test Template HTML Body', + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]) + ->assertOk(); + + $this->assertDatabaseHas('mail_templates', [ + 'code' => '_mail.test_template', + 'subject' => 'Test Template Subject', + 'plain_body' => 'Test Template Text Body', + 'body' => 'Test Template HTML Body', + ]); +}); + +it('updates mail template', function() { + $mailTemplate = MailTemplate::create(['code' => '_mail.test_template']); + + actingAsSuperUser() + ->post(route('igniter.system.mail_templates', ['slug' => 'edit/'.$mailTemplate->getKey()]), [ + 'MailTemplate' => [ + 'code' => '_mail.test_template', + 'label' => 'Test Template Subject', + 'subject' => 'Test Template Subject', + 'plain_body' => 'Test Template Text Body', + 'body' => 'Test Template HTML Body', + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]) + ->assertOk(); + + $this->assertDatabaseHas('mail_templates', [ + 'code' => '_mail.test_template', + 'subject' => 'Test Template Subject', + 'plain_body' => 'Test Template Text Body', + 'body' => 'Test Template HTML Body', + ]); +}); + +it('deletes mail template', function() { + $mailTemplate = MailTemplate::create(['code' => '_mail.test_template']); + + actingAsSuperUser() + ->post(route('igniter.system.mail_templates', ['slug' => 'edit/'.$mailTemplate->getKey()]), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]) + ->assertOk(); + + $this->assertDatabaseMissing('mail_templates', [ + 'template_id' => $mailTemplate->getKey(), + ]); +}); + +it('tests mail template', function() { + $mailTemplate = MailTemplate::create(['code' => '_mail.test_template']); + + actingAsSuperUser() + ->post(route('igniter.system.mail_templates', ['slug' => 'edit/'.$mailTemplate->getKey()]), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onTestTemplate', + ]) + ->assertOk(); +}); + +it('flashes error when request is invalid', function() { + actingAsSuperUser() + ->post(route('igniter.system.mail_templates', ['slug' => 'edit/']), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onTestTemplate', + ]) + ->assertStatus(406); +}); diff --git a/tests/src/System/Http/Controllers/RequestLogsTest.php b/tests/src/System/Http/Controllers/RequestLogsTest.php new file mode 100644 index 00000000..cdaa2730 --- /dev/null +++ b/tests/src/System/Http/Controllers/RequestLogsTest.php @@ -0,0 +1,25 @@ +get(route('igniter.system.request_logs')) + ->assertOk(); +}); + +it('empties request logs', function() { + RequestLog::createLog(404); + + actingAsSuperUser() + ->post(route('igniter.system.request_logs'), [ + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onEmptyLog', + ]) + ->assertOk(); + + $this->assertDatabaseCount('request_logs', 0); +}); diff --git a/tests/src/System/Http/Controllers/SettingsTest.php b/tests/src/System/Http/Controllers/SettingsTest.php new file mode 100644 index 00000000..155203f6 --- /dev/null +++ b/tests/src/System/Http/Controllers/SettingsTest.php @@ -0,0 +1,130 @@ +get(route('igniter.system.settings')) + ->assertOk(); +}); + +it('loads general settings page', function() { + actingAsSuperUser() + ->get(route('igniter.system.settings', ['slug' => 'edit/general'])) + ->assertOk(); +}); + +it('flashes error when accessing restricted settings page', function() { + $role = UserRole::factory()->create(['permissions' => ['Site.Settings' => 1]]); + $user = User::factory()->for($role, 'role')->create(); + + $this->actingAs($user, 'igniter-admin') + ->get(route('igniter.system.settings', ['slug' => 'edit/statuses'])) + ->assertSee(lang('igniter::admin.alert_user_restricted')); +}); + +it('updates general settings', function() { + actingAsSuperUser() + ->post(route('igniter.system.settings', ['slug' => 'edit/general']), [ + 'Setting' => [ + 'site_name' => 'New Site Name', + 'site_email' => 'site@example.com', + 'site_logo' => 'path/to/logo.png', + 'distance_unit' => 'km', + 'default_geocoder' => 'nominatim', + 'timezone' => 'Europe/London', + 'detect_language' => '0', + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]) + ->assertOk(); +}); + +it('flashes error when updating restricted settings', function() { + $role = UserRole::factory()->create(['permissions' => ['Site.Settings' => 1]]); + $user = User::factory()->for($role, 'role')->create(); + + $this->actingAs($user, 'igniter-admin') + ->post(route('igniter.system.settings', ['slug' => 'edit/statuses']), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]) + ->assertSee(lang('igniter::admin.alert_user_restricted')); +}); + +it('flashes error when updates settings fails validation', function() { + actingAsSuperUser() + ->post(route('igniter.system.settings', ['slug' => 'edit/general']), [ + 'Setting' => [ + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]) + ->assertStatus(406); +}); + +it('updates extension settings and redirects to settings page', function() { + actingAsSuperUser() + ->post(route('igniter.system.settings', ['slug' => 'edit/general']), [ + 'close' => '1', + 'Setting' => [ + 'site_name' => 'New Site Name', + 'site_email' => 'site@example.com', + 'site_logo' => 'path/to/logo.png', + 'distance_unit' => 'km', + 'default_geocoder' => 'nominatim', + 'timezone' => 'Europe/London', + 'detect_language' => '0', + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]) + ->assertOk(); +}); + +it('sends test email', function() { + Mail::shouldReceive('raw')->once(); + + actingAsSuperUser() + ->post(route('igniter.system.settings', ['slug' => 'edit/mail']), [ + 'Setting' => [ + 'sender_name' => 'Test Sender', + 'sender_email' => 'sender@example.com', + 'protocol' => 'log', + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onTestMail', + ]) + ->assertOk(); + + expect(flash()->messages()->first())->message->toBe(sprintf(lang('igniter::system.settings.alert_email_sent'), AdminAuth::getStaffEmail())); +}); + +it('flashes error when sending test email fails', function() { + Mail::shouldReceive('raw')->andThrow(new \Exception('Test exception')); + + actingAsSuperUser() + ->post(route('igniter.system.settings', ['slug' => 'edit/mail']), [ + 'Setting' => [ + 'sender_name' => 'Test Sender', + 'sender_email' => 'sender@example.com', + 'protocol' => 'log', + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onTestMail', + ]) + ->assertOk(); + + expect(flash()->messages()->first())->message->toBe('Test exception'); +}); diff --git a/tests/src/System/Http/Controllers/SystemLogsTest.php b/tests/src/System/Http/Controllers/SystemLogsTest.php new file mode 100644 index 00000000..c13532e2 --- /dev/null +++ b/tests/src/System/Http/Controllers/SystemLogsTest.php @@ -0,0 +1,25 @@ +get(route('igniter.system.system_logs')) + ->assertOk(); +}); + +it('empties system logs', function() { + File::partialMock()->shouldReceive('exists')->andReturn(false, true); + File::partialMock()->shouldReceive('isWritable')->andReturnTrue(); + File::partialMock()->shouldReceive('put')->andReturnSelf(); + + actingAsSuperUser() + ->post(route('igniter.system.system_logs'), [ + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onEmptyLog', + ]) + ->assertOk(); +}); diff --git a/tests/src/System/Http/Controllers/UpdatesTest.php b/tests/src/System/Http/Controllers/UpdatesTest.php new file mode 100644 index 00000000..173e8397 --- /dev/null +++ b/tests/src/System/Http/Controllers/UpdatesTest.php @@ -0,0 +1,21 @@ +get(route('igniter.system.updates')) + ->assertOk(); +}); + +it('flashes error when requesting update list fails', function() { + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + $updateManager->shouldReceive('requestUpdateList')->andThrow(new \Exception('Error requesting update list')); + + actingAsSuperUser() + ->post(route('igniter.system.updates')) + ->assertOk(); +}); diff --git a/tests/src/System/Http/Middleware/CheckRequirementsTest.php b/tests/src/System/Http/Middleware/CheckRequirementsTest.php new file mode 100644 index 00000000..db740a1e --- /dev/null +++ b/tests/src/System/Http/Middleware/CheckRequirementsTest.php @@ -0,0 +1,40 @@ +andReturn(false); + View::shouldReceive('make')->with('igniter.system::no_database')->andReturn('no_database_view'); + ResponseFacade::shouldReceive('make')->with('no_database_view')->andReturn(new Response('no_database_view')); + + $middleware = new CheckRequirements(); + $request = new Request(); + $next = function($req) { + return new Response('next_response'); + }; + + $result = $middleware->handle($request, $next); + + expect($result->getContent())->toBe('no_database_view'); +}); + +it('calls next middleware when database exists', function() { + Igniter::shouldReceive('hasDatabase')->andReturn(true); + + $middleware = new CheckRequirements(); + $request = new Request(); + $next = function($req) { + return new Response('next_response'); + }; + + $result = $middleware->handle($request, $next); + + expect($result->getContent())->toBe('next_response'); +}); diff --git a/tests/src/System/Http/Middleware/PoweredByTest.php b/tests/src/System/Http/Middleware/PoweredByTest.php index 57da7b6d..aa7d90da 100644 --- a/tests/src/System/Http/Middleware/PoweredByTest.php +++ b/tests/src/System/Http/Middleware/PoweredByTest.php @@ -2,4 +2,48 @@ namespace Igniter\Tests\System\Http\Middleware; -it('adds powered by text to response header')->skip(); +use Igniter\System\Http\Middleware\PoweredBy; +use Illuminate\Http\Request; +use Illuminate\Http\Response; + +it('adds X-Powered-By header when config is enabled', function() { + config()->set('igniter-system.sendPoweredByHeader', true); + $middleware = new PoweredBy(); + $request = new Request(); + $response = new Response(); + $next = function($req) use ($response) { + return $response; + }; + + $result = $middleware->handle($request, $next); + + expect($result->headers->get('X-Powered-By'))->toBe('TastyIgniter'); +}); + +it('does not add X-Powered-By header when config is disabled', function() { + config()->set('igniter-system.sendPoweredByHeader', false); + $middleware = new PoweredBy(); + $request = new Request(); + $response = new Response(); + $next = function($req) use ($response) { + return $response; + }; + + $result = $middleware->handle($request, $next); + + expect($result->headers->has('X-Powered-By'))->toBeFalse(); +}); + +it('does not add X-Powered-By header for non-Response instance', function() { + config()->set('igniter-system.sendPoweredByHeader', true); + $middleware = new PoweredBy(); + $request = new \Illuminate\Http\Request(); + $response = new \Symfony\Component\HttpFoundation\Response(); + $next = function($req) use ($response) { + return $response; + }; + + $result = $middleware->handle($request, $next); + + expect($result->headers->has('X-Powered-By'))->toBeFalse(); +}); diff --git a/tests/src/System/Http/Requests/AdvancedSettingsRequestTest.php b/tests/src/System/Http/Requests/AdvancedSettingsRequestTest.php new file mode 100644 index 00000000..fc92a974 --- /dev/null +++ b/tests/src/System/Http/Requests/AdvancedSettingsRequestTest.php @@ -0,0 +1,27 @@ +attributes(); + + expect($attributes)->toHaveKey('enable_request_log', lang('igniter::system.settings.label_enable_request_log')) + ->and($attributes)->toHaveKey('maintenance_mode', lang('igniter::system.settings.label_maintenance_mode')) + ->and($attributes)->toHaveKey('maintenance_message', lang('igniter::system.settings.label_maintenance_message')) + ->and($attributes)->toHaveKey('activity_log_timeout', lang('igniter::system.settings.label_activity_log_timeout')); +}); + +it('returns correct validation rules', function() { + $request = new AdvancedSettingsRequest; + + $rules = $request->rules(); + + expect($rules['enable_request_log'])->toContain('required', 'boolean') + ->and($rules['maintenance_mode'])->toContain('required', 'boolean') + ->and($rules['maintenance_message'])->toContain('required_if:maintenance_mode,1', 'string') + ->and($rules['activity_log_timeout'])->toContain('required', 'integer', 'max:999'); +}); diff --git a/tests/src/System/Http/Requests/CountryRequestTest.php b/tests/src/System/Http/Requests/CountryRequestTest.php new file mode 100644 index 00000000..2f68e05c --- /dev/null +++ b/tests/src/System/Http/Requests/CountryRequestTest.php @@ -0,0 +1,27 @@ +attributes(); + + expect($attributes)->toHaveKey('country_name', lang('igniter::admin.label_name')) + ->and($attributes)->toHaveKey('priority', lang('igniter::system.countries.label_priority')) + ->and($attributes)->toHaveKey('iso_code_2', lang('igniter::system.countries.label_iso_code2')) + ->and($attributes)->toHaveKey('iso_code_3', lang('igniter::system.countries.label_iso_code3')) + ->and($attributes)->toHaveKey('format', lang('igniter::system.countries.label_format')) + ->and($attributes)->toHaveKey('status', lang('igniter::admin.label_status')); +}); + +it('returns correct validation rules', function() { + $rules = (new CountryRequest)->rules(); + + expect($rules['country_name'])->toContain('required', 'string', 'between:2,255') + ->and($rules['priority'])->toContain('required', 'integer') + ->and($rules['iso_code_2'])->toContain('required', 'string', 'size:2') + ->and($rules['iso_code_3'])->toContain('required', 'string', 'size:3') + ->and($rules['format'])->toContain('min:2', 'string') + ->and($rules['status'])->toContain('required', 'boolean'); +}); diff --git a/tests/src/System/Http/Requests/CountryTest.php b/tests/src/System/Http/Requests/CountryTest.php deleted file mode 100644 index 7a78b1e3..00000000 --- a/tests/src/System/Http/Requests/CountryTest.php +++ /dev/null @@ -1,24 +0,0 @@ -rules(); - - expect('required')->toBeIn(array_get($rules, 'country_name')) - ->and('required')->toBeIn(array_get($rules, 'priority')) - ->and('required')->toBeIn(array_get($rules, 'iso_code_2')) - ->and('required')->toBeIn(array_get($rules, 'iso_code_3')) - ->and('required')->toBeIn(array_get($rules, 'status')); -}); - -it('has max characters rule for inputs', function() { - $rules = (new CountryRequest)->rules(); - - expect('between:2,255')->toBeIn(array_get($rules, 'country_name')) - ->and('size:2')->toBeIn(array_get($rules, 'iso_code_2')) - ->and('size:3')->toBeIn(array_get($rules, 'iso_code_3')) - ->and('min:2')->toBeIn(array_get($rules, 'format')); -}); diff --git a/tests/src/System/Http/Requests/CurrencyRequestTest.php b/tests/src/System/Http/Requests/CurrencyRequestTest.php new file mode 100644 index 00000000..38c97465 --- /dev/null +++ b/tests/src/System/Http/Requests/CurrencyRequestTest.php @@ -0,0 +1,35 @@ +attributes(); + + expect($attributes)->toHaveKey('currency_name', lang('igniter::system.currencies.label_title')) + ->and($attributes)->toHaveKey('currency_code', lang('igniter::system.currencies.label_code')) + ->and($attributes)->toHaveKey('currency_symbol', lang('igniter::system.currencies.label_symbol')) + ->and($attributes)->toHaveKey('country_id', lang('igniter::system.currencies.label_country')) + ->and($attributes)->toHaveKey('symbol_position', lang('igniter::system.currencies.label_symbol_position')) + ->and($attributes)->toHaveKey('currency_rate', lang('igniter::system.currencies.label_rate')) + ->and($attributes)->toHaveKey('thousand_sign', lang('igniter::system.currencies.label_thousand_sign')) + ->and($attributes)->toHaveKey('decimal_sign', lang('igniter::system.currencies.label_decimal_sign')) + ->and($attributes)->toHaveKey('decimal_position', lang('igniter::system.currencies.label_decimal_position')) + ->and($attributes)->toHaveKey('currency_status', lang('igniter::admin.label_status')); +}); + +it('returns correct validation rules', function() { + $rules = (new CurrencyRequest)->rules(); + + expect($rules['currency_name'])->toContain('required', 'string', 'between:2,32') + ->and($rules['currency_code'])->toContain('required', 'string', 'size:3') + ->and($rules['currency_symbol'])->toContain('required', 'string') + ->and($rules['country_id'])->toContain('required', 'integer') + ->and($rules['symbol_position'])->toContain('string', 'size:1') + ->and($rules['currency_rate'])->toContain('numeric') + ->and($rules['thousand_sign'])->toContain('string', 'size:1') + ->and($rules['decimal_sign'])->toContain('string', 'size:1') + ->and($rules['decimal_position'])->toContain('integer', 'max:10') + ->and($rules['currency_status'])->toContain('required', 'boolean'); +}); diff --git a/tests/src/System/Http/Requests/CurrencyTest.php b/tests/src/System/Http/Requests/CurrencyTest.php deleted file mode 100644 index 79710d27..00000000 --- a/tests/src/System/Http/Requests/CurrencyTest.php +++ /dev/null @@ -1,25 +0,0 @@ -rules(); - - expect('required')->toBeIn(array_get($rules, 'currency_name')) - ->and('required')->toBeIn(array_get($rules, 'currency_code')) - ->and('required')->toBeIn(array_get($rules, 'country_id')) - ->and('required')->toBeIn(array_get($rules, 'currency_status')); -}); - -it('has max characters rule for inputs', function() { - $rules = (new CurrencyRequest)->rules(); - - expect('between:2,32')->toBeIn(array_get($rules, 'currency_name')) - ->and('size:3')->toBeIn(array_get($rules, 'currency_code')) - ->and('size:1')->toBeIn(array_get($rules, 'symbol_position')) - ->and('size:1')->toBeIn(array_get($rules, 'thousand_sign')) - ->and('size:1')->toBeIn(array_get($rules, 'decimal_sign')) - ->and('max:10')->toBeIn(array_get($rules, 'decimal_position')); -}); diff --git a/tests/src/System/Http/Requests/GeneralSettingsRequestTest.php b/tests/src/System/Http/Requests/GeneralSettingsRequestTest.php new file mode 100644 index 00000000..f90cd9b8 --- /dev/null +++ b/tests/src/System/Http/Requests/GeneralSettingsRequestTest.php @@ -0,0 +1,31 @@ +attributes(); + + expect($attributes)->toHaveKey('site_name', lang('igniter::system.settings.label_site_name')) + ->and($attributes)->toHaveKey('site_email', lang('igniter::system.settings.label_site_email')) + ->and($attributes)->toHaveKey('site_logo', lang('igniter::system.settings.label_site_logo')) + ->and($attributes)->toHaveKey('maps_api_key', lang('igniter::system.settings.label_maps_api_key')) + ->and($attributes)->toHaveKey('distance_unit', lang('igniter::system.settings.label_distance_unit')) + ->and($attributes)->toHaveKey('timezone', lang('igniter::system.settings.label_timezone')) + ->and($attributes)->toHaveKey('detect_language', lang('igniter::system.settings.label_detect_language')) + ->and($attributes)->toHaveKey('country_id', lang('igniter::system.settings.label_country')); +}); + +it('returns correct validation rules', function() { + $rules = (new GeneralSettingsRequest)->rules(); + + expect($rules['site_name'])->toContain('required', 'string', 'min:2', 'max:255') + ->and($rules['site_email'])->toContain('required', 'email:filter', 'max:96') + ->and($rules['site_logo'])->toContain('required', 'string') + ->and($rules['distance_unit'])->toContain('required', 'in:mi,km') + ->and($rules['default_geocoder'])->toContain('required', 'in:nominatim,google,chain') + ->and($rules['maps_api_key'])->toContain('required_if:default_geocoder,google', 'alpha_dash') + ->and($rules['timezone'])->toContain('required', 'timezone') + ->and($rules['detect_language'])->toContain('required', 'boolean'); +}); diff --git a/tests/src/System/Http/Requests/LanguageRequestTest.php b/tests/src/System/Http/Requests/LanguageRequestTest.php new file mode 100644 index 00000000..af28b9ec --- /dev/null +++ b/tests/src/System/Http/Requests/LanguageRequestTest.php @@ -0,0 +1,26 @@ +attributes(); + + expect($attributes)->toHaveKey('name', lang('igniter::admin.label_name')) + ->and($attributes)->toHaveKey('code', lang('igniter::system.languages.label_code')) + ->and($attributes)->toHaveKey('status', lang('igniter::admin.label_status')) + ->and($attributes)->toHaveKey('translations.*.source', lang('igniter::system.column_source')) + ->and($attributes)->toHaveKey('translations.*.translation', lang('igniter::system.column_translation')); +}); + +it('returns correct validation rules', function() { + $rules = (new LanguageRequest)->rules(); + + expect($rules['name'])->toContain('required', 'string', 'between:2,32') + ->and($rules['code'])->toContain('required', 'regex:/^[a-zA-Z_]+$/') + ->and($rules['code'][2]->__toString())->toBe('unique:languages,NULL,NULL,language_id') + ->and($rules['status'])->toContain('required', 'boolean') + ->and($rules['translations.*.source'])->toContain('string', 'max:2500') + ->and($rules['translations.*.translation'])->toContain('nullable', 'string', 'max:2500'); +}); diff --git a/tests/src/System/Http/Requests/LanguageTest.php b/tests/src/System/Http/Requests/LanguageTest.php deleted file mode 100644 index 78423374..00000000 --- a/tests/src/System/Http/Requests/LanguageTest.php +++ /dev/null @@ -1,23 +0,0 @@ -rules(); - - expect('required')->toBeIn(array_get($rules, 'name')) - ->and('required')->toBeIn(array_get($rules, 'code')) - ->and('required')->toBeIn(array_get($rules, 'status')); -}); - -it('has unique rule for code input', function() { - expect('unique:languages')->toBeIn(array_get((new LanguageRequest)->rules(), 'code')); -})->skip(); - -it('has max characters rule for code input', function() { - expect('between:2,32')->toBeIn(array_get((new LanguageRequest)->rules(), 'name')) - ->and('max:2500')->toBeIn(array_get((new LanguageRequest)->rules(), 'translations.*.source')) - ->and('max:2500')->toBeIn(array_get((new LanguageRequest)->rules(), 'translations.*.translation')); -}); diff --git a/tests/src/System/Http/Requests/MailLayoutRequestTest.php b/tests/src/System/Http/Requests/MailLayoutRequestTest.php new file mode 100644 index 00000000..4397ad91 --- /dev/null +++ b/tests/src/System/Http/Requests/MailLayoutRequestTest.php @@ -0,0 +1,27 @@ +attributes(); + + expect($attributes)->toHaveKey('name', lang('igniter::admin.label_name')) + ->and($attributes)->toHaveKey('code', lang('igniter::system.mail_templates.label_code')) + ->and($attributes)->toHaveKey('layout_css', lang('igniter::system.mail_templates.label_layout_css')) + ->and($attributes)->toHaveKey('plain_layout', lang('igniter::system.mail_templates.label_plain')) + ->and($attributes)->toHaveKey('layout', lang('igniter::system.mail_templates.label_body')); +}); + +it('returns correct validation rules', function() { + $rules = (new MailLayoutRequest)->rules(); + + expect($rules['name'])->toContain('required', 'string', 'between:2,32') + ->and($rules['code'])->toContain('sometimes', 'required', 'regex:/^[a-z-_\.\:]+$/i') + ->and($rules['code'][3]->__toString())->toBe('unique:mail_layouts,NULL,NULL,layout_id') + ->and($rules['language_id'])->toContain('integer') + ->and($rules['layout'])->toContain('string') + ->and($rules['layout_css'])->toContain('nullable', 'string') + ->and($rules['plain_layout'])->toContain('nullable', 'string'); +}); diff --git a/tests/src/System/Http/Requests/MailLayoutTest.php b/tests/src/System/Http/Requests/MailLayoutTest.php deleted file mode 100644 index 807e1326..00000000 --- a/tests/src/System/Http/Requests/MailLayoutTest.php +++ /dev/null @@ -1,50 +0,0 @@ -rules(); - - expect('required')->toBeIn(array_get($rules, 'name')) - ->and('required')->toBeIn(array_get($rules, 'code')); -}); - -it('has regex rule for code input', function() { - expect('regex:/^[a-z-_\.\:]+$/i')->toBeIn(array_get((new MailLayoutRequest)->rules(), 'code')); -}); - -it('has sometimes rule for code input', function() { - expect('sometimes')->toBeIn(array_get((new MailLayoutRequest)->rules(), 'code')); -}); - -it('has characters length between 2 and 32 characters rule for code input', function() { - expect('between:2,32')->toBeIn(array_get((new MailLayoutRequest)->rules(), 'name')); -}); - -it('has unique rule for code input', function() { - expect((string)(Rule::unique('mail_layouts')->ignore(null, 'layout_id'))) - ->toBeIn( - collect(array_get((new MailLayoutRequest)->rules(), 'code'))->map(function($rule) { - return (string)$rule; - })->toArray() - ); -}); - -it('has string rule for inputs: layout_css, name, plain_layout and layout', function() { - $rules = (new MailLayoutRequest)->rules(); - - expect('string')->toBeIn(array_get($rules, 'layout')) - ->and('string')->toBeIn(array_get($rules, 'layout_css')) - ->and('string')->toBeIn(array_get($rules, 'name')) - ->and('string')->toBeIn(array_get($rules, 'plain_layout')); -}); - -it('has nullable rule for input rules: layout_css and plain_layout', function() { - $rules = (new MailLayoutRequest)->rules(); - - expect('nullable')->toBeIn(array_get($rules, 'layout_css')) - ->and('nullable')->toBeIn(array_get($rules, 'plain_layout')); -}); diff --git a/tests/src/System/Http/Requests/MailPartialRequestTest.php b/tests/src/System/Http/Requests/MailPartialRequestTest.php new file mode 100644 index 00000000..99df08f4 --- /dev/null +++ b/tests/src/System/Http/Requests/MailPartialRequestTest.php @@ -0,0 +1,23 @@ +attributes(); + + expect($attributes)->toHaveKey('name', lang('igniter::admin.label_name')) + ->and($attributes)->toHaveKey('code', lang('igniter::system.mail_templates.label_code')) + ->and($attributes)->toHaveKey('html', lang('igniter::system.mail_templates.label_html')); +}); + +it('returns correct validation rules', function() { + $rules = (new MailPartialRequest)->rules(); + + expect($rules['name'])->toContain('required', 'string') + ->and($rules['code'])->toContain('sometimes', 'required', 'regex:/^[a-z-_\.\:]+$/i') + ->and($rules['code'][3]->__toString())->toBe('unique:mail_partials,NULL,NULL,partial_id') + ->and($rules['html'])->toContain('required', 'string') + ->and($rules['text'])->toContain('nullable', 'string'); +}); diff --git a/tests/src/System/Http/Requests/MailPartialTest.php b/tests/src/System/Http/Requests/MailPartialTest.php deleted file mode 100644 index 5bd2fb97..00000000 --- a/tests/src/System/Http/Requests/MailPartialTest.php +++ /dev/null @@ -1,45 +0,0 @@ -rules(); - - expect('required')->toBeIn(array_get($rules, 'name')) - ->and('required')->toBeIn(array_get($rules, 'code')) - ->and('required')->toBeIn(array_get($rules, 'html')); -}); - -it('has regex rule for code input', function() { - expect('regex:/^[a-z-_\.\:]+$/i')->toBeIn(array_get((new MailPartialRequest)->rules(), 'code')); -}); - -it('has sometimes rule for code input', function() { - expect('sometimes')->toBeIn(array_get((new MailPartialRequest)->rules(), 'code')); -}); - -it('has unique rule for code input', function() { - expect((string)(Rule::unique('mail_partials')->ignore(null, 'partial_id'))) - ->toBeIn( - collect(array_get((new MailPartialRequest)->rules(), 'code'))->map(function($rule) { - return (string)$rule; - })->toArray() - ); -}); - -it('has string rule for input rules: html, name, and text', function() { - $rules = (new MailPartialRequest)->rules(); - - expect('string')->toBeIn(array_get($rules, 'text')) - ->and('string')->toBeIn(array_get($rules, 'name')) - ->and('string')->toBeIn(array_get($rules, 'html')); -}); - -it('has nullable rule for text input', function() { - $rules = (new MailPartialRequest)->rules(); - - expect('nullable')->toBeIn(array_get($rules, 'text')); -}); diff --git a/tests/src/System/Http/Requests/MailSettingsRequestTest.php b/tests/src/System/Http/Requests/MailSettingsRequestTest.php new file mode 100644 index 00000000..6ed272db --- /dev/null +++ b/tests/src/System/Http/Requests/MailSettingsRequestTest.php @@ -0,0 +1,44 @@ +attributes(); + + expect($attributes)->toHaveKey('sender_name', lang('igniter::system.settings.label_sender_name')) + ->and($attributes)->toHaveKey('sender_email', lang('igniter::system.settings.label_sender_email')) + ->and($attributes)->toHaveKey('protocol', lang('igniter::system.settings.label_protocol')) + ->and($attributes)->toHaveKey('mail_logo', lang('igniter::system.settings.label_mail_logo')) + ->and($attributes)->toHaveKey('smtp_host', lang('igniter::system.settings.label_smtp_host')) + ->and($attributes)->toHaveKey('smtp_port', lang('igniter::system.settings.label_smtp_port')) + ->and($attributes)->toHaveKey('smtp_encryption', lang('igniter::system.settings.label_smtp_encryption')) + ->and($attributes)->toHaveKey('smtp_user', lang('igniter::system.settings.label_smtp_user')) + ->and($attributes)->toHaveKey('smtp_pass', lang('igniter::system.settings.label_smtp_pass')) + ->and($attributes)->toHaveKey('mailgun_domain', lang('igniter::system.settings.label_mailgun_domain')) + ->and($attributes)->toHaveKey('mailgun_secret', lang('igniter::system.settings.label_mailgun_secret')) + ->and($attributes)->toHaveKey('postmark_token', lang('igniter::system.settings.label_postmark_token')) + ->and($attributes)->toHaveKey('ses_key', lang('igniter::system.settings.label_ses_key')) + ->and($attributes)->toHaveKey('ses_secret', lang('igniter::system.settings.label_ses_secret')) + ->and($attributes)->toHaveKey('ses_region', lang('igniter::system.settings.label_ses_region')); +}); + +it('returns correct validation rules', function() { + $rules = (new MailSettingsRequest)->rules(); + + expect($rules['sender_name'])->toContain('required', 'string') + ->and($rules['sender_email'])->toContain('required', 'email:filter') + ->and($rules['protocol'])->toContain('required', 'string') + ->and($rules['mail_logo'])->toContain('nullable', 'string') + ->and($rules['smtp_host'])->toContain('string') + ->and($rules['smtp_port'])->toContain('string') + ->and($rules['smtp_user'])->toContain('string') + ->and($rules['smtp_pass'])->toContain('string') + ->and($rules['mailgun_domain'])->toContain('required_if:protocol,mailgun', 'string') + ->and($rules['mailgun_secret'])->toContain('required_if:protocol,mailgun', 'string') + ->and($rules['postmark_token'])->toContain('required_if:protocol,postmark', 'string') + ->and($rules['ses_key'])->toContain('required_if:protocol,ses', 'string') + ->and($rules['ses_secret'])->toContain('required_if:protocol,ses', 'string') + ->and($rules['ses_region'])->toContain('required_if:protocol,ses', 'string'); +}); diff --git a/tests/src/System/Http/Requests/MailTemplateRequestTest.php b/tests/src/System/Http/Requests/MailTemplateRequestTest.php new file mode 100644 index 00000000..5c578a1c --- /dev/null +++ b/tests/src/System/Http/Requests/MailTemplateRequestTest.php @@ -0,0 +1,26 @@ +attributes(); + + expect($attributes)->toHaveKey('label', lang('igniter::admin.label_description')) + ->and($attributes)->toHaveKey('subject', lang('igniter::system.mail_templates.label_subject')) + ->and($attributes)->toHaveKey('code', lang('igniter::system.mail_templates.label_code')) + ->and($attributes)->toHaveKey('layout_id', lang('igniter::system.mail_templates.label_layout')); +}); + +it('returns correct validation rules', function() { + $rules = (new MailTemplateRequest)->rules(); + + expect($rules['label'])->toContain('required', 'string') + ->and($rules['subject'])->toContain('required', 'string') + ->and($rules['code'])->toContain('sometimes', 'required', 'min:2', 'max:255', 'regex:/^[a-z-_\.\:]+$/i') + ->and($rules['code'][4]->__toString())->toBe('unique:mail_templates,code,NULL,template_id') + ->and($rules['layout_id'])->toContain('nullable', 'integer') + ->and($rules['plain_body'])->toContain('nullable', 'string') + ->and($rules['body'])->toContain('string'); +}); diff --git a/tests/src/System/Http/Requests/MailTemplateTest.php b/tests/src/System/Http/Requests/MailTemplateTest.php deleted file mode 100644 index 6d19f07d..00000000 --- a/tests/src/System/Http/Requests/MailTemplateTest.php +++ /dev/null @@ -1,55 +0,0 @@ -rules(); - - expect('required')->toBeIn(array_get($rules, 'label')) - ->and('required')->toBeIn(array_get($rules, 'subject')) - ->and('required')->toBeIn(array_get($rules, 'code')); -}); - -it('has string rule for input rules: label, subject and plain_body', function() { - $rules = (new MailTemplateRequest)->rules(); - - expect('string')->toBeIn(array_get($rules, 'label')) - ->and('string')->toBeIn(array_get($rules, 'subject')) - ->and('string')->toBeIn(array_get($rules, 'plain_body')); -}); - -it('has nullable for plain_body input', function() { - expect('nullable')->toBeIn(array_get((new MailTemplateRequest)->rules(), 'plain_body')); -}); - -it('has layout_id for integer input', function() { - expect('integer')->toBeIn(array_get((new MailTemplateRequest)->rules(), 'layout_id')); -}); - -it('has regex rule for code input', function() { - expect('regex:/^[a-z-_\.\:]+$/i')->toBeIn(array_get((new MailTemplateRequest)->rules(), 'code')); -}); - -it('has max of 255 characters rule for code input', function() { - expect('max:255')->toBeIn(array_get((new MailTemplateRequest)->rules(), 'code')); -}); - -it('has min of 2 characters rule for code input', function() { - expect('max:255')->toBeIn(array_get((new MailTemplateRequest)->rules(), 'code')); -}); - -it('has sometimes rule for code input', function() { - expect('sometimes')->toBeIn(array_get((new MailTemplateRequest)->rules(), 'code')); -}); - -it('has unique rule for code input', function() { - expect((string)(Rule::unique('mail_templates', 'code')->ignore(null, 'template_id'))) - ->toBeIn( - collect(array_get((new MailTemplateRequest)->rules(), 'code'))->map(function($rule) { - return (string)$rule; - })->toArray() - ); -}); diff --git a/tests/src/System/Libraries/AssetsTest.php b/tests/src/System/Libraries/AssetsTest.php new file mode 100644 index 00000000..006d07d8 --- /dev/null +++ b/tests/src/System/Libraries/AssetsTest.php @@ -0,0 +1,136 @@ +andReturnTrue(); + Igniter::shouldReceive('adminUri')->andReturn('admin'); + Igniter::shouldReceive('hasDatabase')->andReturnTrue(); + $path = realpath(__DIR__.'/../../../../resources/views/admin/_meta/assets.json'); + + $assets = new Assets(); + $assets->addFromManifest($path); + + expect($assets->getCss())->toContain('rel="stylesheet" type="text/css"') + ->and($assets->getJs())->toContain('charset="utf-8" type="text/javascript"') + ->and($assets->getFavIcon())->toContain('rel="shortcut icon" type="image/x-icon"') + ->and($assets->getMetas())->toContain('name="Content-type" content="text/html; charset=utf-8" type="equiv"') + ->and($assets->getRss())->toBeNull(); +}); + +it('adds assets from theme manifest successfully', function() { + $theme = resolve(ThemeManager::class)->findTheme('igniter-orange'); + + $assets = new Assets(); + $assets->addAssetsFromThemeManifest($theme); + + expect($assets->getCss())->toContain('rel="stylesheet" type="text/css"') + ->and($assets->getJs())->toContain('charset="utf-8" type="text/javascript"'); +}); + +it('returns null when adding assets from non existence manifest', function() { + $assets = new Assets(); + $assets->addFromManifest('/non/existence/path'); + + expect($assets->getCss())->toBeNull() + ->and($assets->getJs())->toBeNull() + ->and($assets->getFavIcon())->toBeNull() + ->and($assets->getMetas())->toBeNull() + ->and($assets->getRss())->toBeNull() + ->and($assets->getJsVars())->toBe(''); +}); + +it('adds favicon successfully', function() { + $assets = new Assets(); + $assets->addFavIcon(['href' => 'favicon.ico']); + $assets->addFavIcon(['href' => public_path('favicon.ico')]); + + expect($assets->getFavIcon())->toContain('favicon.ico'); +}); + +it('adds rss successfully', function() { + $assets = new Assets(); + $assets->addTag('rss', 'https://example.com/rss.xml'); + + expect($assets->getRss())->toContain('https://example.com/rss.xml'); +}); + +it('adds and retrieves js variables successfully', function() { + $assets = new Assets(); + $assets->putJsVars([ + 'key' => 'value', + 'object' => (object)['key' => 'value'], + 'toJson' => new class + { + public function toJson() + { + return ['key' => 'value']; + } + }, + 'toString' => new class + { + public function __toString() + { + return 'value'; + } + }, + ]); + $assets->mergeJsVars('key', ['new-value']); + + $jsVars = $assets->getJsVars(); + + expect($jsVars)->toContain('key') + ->and($jsVars)->toContain('value'); +}); + +it('throws exception when invalid transforming js variable', function() { + $assets = new Assets(); + $assets->putJsVars([ + 'key' => new class + { + }, + ]); + + expect(fn() => $assets->getJsVars()) + ->toThrow(new \RuntimeException('Cannot transform this object to JavaScript.')); +}); + +it('removes duplicate assets from combined', function() { + File::put(public_path('/test.css'), 'body { color: red; }'); + + Assets::registerCallback(function(Assets $manager) { + $manager->addTags([ + 'css' => [ + [ + 'path' => base_path('css/style.css'), + 'rel' => 'stylesheet', + 'type' => 'text/css', + ], + [ + 'path' => 'css/style.css', + 'rel' => 'stylesheet', + 'type' => 'text/css', + ], + [ + 'path' => 'css/style.css', + 'rel' => 'stylesheet', + 'type' => 'text/css', + ], + [ + 'path' => public_path('/test.css'), + 'rel' => 'stylesheet', + 'type' => 'text/css', + ], + ], + ]); + }); + $assets = new Assets(); + $assets->registerSourcePath(__DIR__.'/../../../resources'); + + expect($assets->getCss())->toContain('rel="stylesheet" type="text/css"'); +}); diff --git a/tests/src/System/Libraries/CountryTest.php b/tests/src/System/Libraries/CountryTest.php index 6003ca9a..f0b3f192 100644 --- a/tests/src/System/Libraries/CountryTest.php +++ b/tests/src/System/Libraries/CountryTest.php @@ -3,63 +3,106 @@ use Igniter\System\Libraries\Country; use Igniter\System\Models\Country as CountryModel; -it('formats address correctly', function($address, $expectedWithLineBreaks, $expectedWithoutLineBreaks) { - $country = new Country; - - expect($country->addressFormat($address))->toBe($expectedWithLineBreaks) - ->and($country->addressFormat($address, false))->toBe($expectedWithoutLineBreaks); -})->with([ - [ - [ - 'address_1' => '123 Street', - 'address_2' => 'Apt 4B', - 'city' => 'City', - 'postcode' => '12345', - 'state' => 'State', - 'country' => 'Country', - ], - '123 Street
Apt 4B
City 12345
State
Country', - '123 Street, Apt 4B, City 12345, State, Country', - ], - [ - [ - 'address_1' => '123 Street', - 'city' => 'City', - 'postcode' => '12345', - 'state' => 'State', - 'country' => 'Country', - ], - '123 Street
City 12345
State
Country', - '123 Street, City 12345, State, Country', - ], -]); - -it('gets country name by id correctly', function() { +it('formats address correctly', function() { + $address = [ + 'address_1' => '123 Street', + 'address_2' => 'Apt 4B', + 'city' => 'City', + 'postcode' => '12345', + 'state' => 'State', + 'country' => 'Country', + ]; + $expectedWithLineBreaks = '123 Street
Apt 4B
City 12345
State
Country'; + $expectedWithoutLineBreaks = '123 Street, Apt 4B, City 12345, State, Country'; + + $countryLibrary = new Country; + + expect($countryLibrary->addressFormat($address))->toBe($expectedWithLineBreaks) + ->and($countryLibrary->addressFormat($address, false))->toBe($expectedWithoutLineBreaks); +}); + +it('formats address correctly using country id', function() { + $country = CountryModel::factory()->create(); + $address = [ + 'address_1' => '123 Street', + 'address_2' => 'Apt 4B', + 'city' => 'City', + 'postcode' => '12345', + 'state' => 'State', + 'country_id' => $country->getKey(), + ]; + + $countryLibrary = new Country; + + expect($countryLibrary->addressFormat($address)) + ->toBe('123 Street
Apt 4B
City 12345
State
'.$country->country_name); +}); + +it('formats address correctly using model', function() { + $address = \Igniter\User\Models\Address::factory()->create([ + 'address_1' => '123 Street', + 'address_2' => 'Apt 4B', + 'city' => 'City', + 'postcode' => '12345', + 'state' => 'State', + ]); + + $countryName = $address->country->country_name; + $countryLibrary = new Country; + + expect($countryLibrary->addressFormat($address)) + ->toBe('123 Street
Apt 4B
City 12345
State
'.$countryName); +}); + +it('formats address correctly using custom format', function() { + $address = [ + 'address_1' => '123 Street', + 'address_2' => 'Apt 4B', + 'city' => 'City', + 'postcode' => '12345', + 'state' => 'State', + 'country' => 'Country', + 'format' => '{address_1}, {address_2}, {city}, {postcode}, {state}, {country}', + ]; + + $countryLibrary = new Country; + + expect($countryLibrary->addressFormat($address, false))->toBe('123 Street, Apt 4B, City, 12345, State, Country'); +}); + +it('returns country name by id correctly', function() { $country = CountryModel::factory()->create([ 'country_name' => 'Test Country', ]); - expect((new Country)->getCountryNameById($country->getKey()))->toBe('Test Country'); + $countryLibrary = new Country; + expect($countryLibrary->getCountryNameById($country->getKey()))->toBe('Test Country') + ->and($countryLibrary->getCountryNameById(1000))->toBeNull(); }); -it('gets country code by id correctly', function() { +it('returns country code by id correctly', function() { $country = CountryModel::factory()->create([ 'iso_code_2' => 'TQ', 'iso_code_3' => 'TQT', ]); - expect((new Country)->getCountryCodeById($country->getKey(), Country::ISO_CODE_2))->toBe('TQ') - ->and((new Country)->getCountryCodeById($country->getKey(), Country::ISO_CODE_3))->toBe('TQT'); + $countryLibrary = new Country; + expect($countryLibrary->getCountryCodeById(1000))->toBeNull() + ->and($countryLibrary->getCountryCodeById($country->getKey(), Country::ISO_CODE_2))->toBe('TQ') + ->and($countryLibrary->getCountryCodeById($country->getKey(), Country::ISO_CODE_3))->toBe('TQT'); }); -it('gets country name by code correctly', function() { +it('returns country name by code correctly', function() { CountryModel::factory()->create([ 'country_name' => 'Test Country', 'iso_code_2' => 'TQ', 'iso_code_3' => 'TQT', ]); - expect((new Country)->getCountryNameByCode('TQ'))->toBe('Test Country'); + $countryLibrary = new Country; + + expect($countryLibrary->getCountryNameByCode('TQ'))->toBe('Test Country') + ->and($countryLibrary->getCountryNameByCode('TAAQ'))->toBeNull(); }); it('lists all countries correctly', function() { @@ -67,8 +110,10 @@ 'country_name' => 'Test Country', ]); - $countries = (new Country)->listAll('country_name')->all(); + $countryLibrary = new Country; + $countries = $countryLibrary->listAll('country_name')->all(); expect($countries)->toBeArray() - ->and($countries)->toContain('Test Country'); + ->and($countries)->toContain('Test Country') + ->and($countryLibrary->listAll())->toBeCollection(); }); diff --git a/tests/src/System/Mail/AnonymousTemplateMailableTest.php b/tests/src/System/Mail/AnonymousTemplateMailableTest.php new file mode 100644 index 00000000..a8a07df9 --- /dev/null +++ b/tests/src/System/Mail/AnonymousTemplateMailableTest.php @@ -0,0 +1,58 @@ +getTemplateCode())->toBe($templateCode); +}); + +it('adds data without models', function() { + $mailable = new AnonymousTemplateMailable(); + $data = ['key1' => 'value1', 'key2' => new class extends Model + { + }]; + $mailable->with($data); + + expect($mailable->viewData)->toHaveKey('key1') + ->and($mailable->viewData)->not->toHaveKey('key2'); +}); + +it('applies callable callback', function() { + $mailable = new AnonymousTemplateMailable(); + $callback = function($message) { + $message->subject('Test Subject'); + }; + $mailable->applyCallback($callback); + + expect($mailable->callbacks)->toContain($callback); +}); + +it('applies array callback', function() { + $mailable = new AnonymousTemplateMailable(); + $callback = ['test@example.com']; + $mailable->applyCallback($callback); + + expect($mailable->to[0])->toContain('test@example.com'); +}); + +it('applies string callback', function() { + $mailable = new AnonymousTemplateMailable(); + $callback = 'test@example.com'; + $mailable->applyCallback($callback); + + expect($mailable->to[0])->toContain('test@example.com'); +}); + +it('does not apply null callback', function() { + $mailable = new AnonymousTemplateMailable(); + $callback = null; + $mailable->applyCallback($callback); + + expect($mailable->to)->toBeEmpty(); +}); diff --git a/tests/src/System/Mail/TemplateMailableTest.php b/tests/src/System/Mail/TemplateMailableTest.php new file mode 100644 index 00000000..165d48ae --- /dev/null +++ b/tests/src/System/Mail/TemplateMailableTest.php @@ -0,0 +1,76 @@ +getTemplateCode())->toBe('test_template'); +}); + +it('builds subject from mail template', function() { + MailTemplate::create([ + 'code' => '_mail.test_template', + 'subject' => 'Test Subject', + ]); + $mailable = new class extends TemplateMailable + { + protected string $templateCode = '_mail.test_template'; + }; + + $mailer = mock(Mailer::class); + $mailer->shouldReceive('send')->withArgs(function($view, $data, $messageCallback) { + $message = mock(Message::class); + $message->shouldReceive('subject')->once(); + $messageCallback($message); + return true; + })->once(); + $mailable->send($mailer); + + $mailable->hasSubject('Test Subject'); +}); + +it('builds view with rendered templates', function() { + MailTemplate::create([ + 'code' => '_mail.test_template', + 'subject' => 'Test Subject', + ]); + $mailable = new class extends TemplateMailable + { + protected string $templateCode = '_mail.test_template'; + }; + $mailManager = mock(MailManager::class); + app()->instance(MailManager::class, $mailManager); + $mailManager->shouldReceive('renderTemplate')->andReturn('Rendered HTML'); + $mailManager->shouldReceive('renderTextTemplate')->andReturn('Rendered Text'); + + $mailer = mock(Mailer::class); + $mailer->shouldReceive('send')->withArgs(function($view, $data, $messageCallback) { + expect((string)$view['html'])->toBe('Rendered HTML') + ->and((string)$view['text'])->toBe('Rendered Text'); + + return true; + }); + + $mailable->send($mailer); +}); + +it('returns variables correctly', function() { + $mailable = new class extends TemplateMailable + { + protected string $templateCode = 'test_template'; + public $var1 = 'value1'; + public $var2 = 'value2'; + }; + + expect($mailable->getVariables())->toContain('var1', 'var2'); +}); diff --git a/tests/src/System/Models/Concerns/DefaultableTest.php b/tests/src/System/Models/Concerns/DefaultableTest.php new file mode 100644 index 00000000..ea05639d --- /dev/null +++ b/tests/src/System/Models/Concerns/DefaultableTest.php @@ -0,0 +1,112 @@ +createQuietly(['is_default' => true]); + $model2 = Country::factory()->create(['is_default' => true]); + + expect(Country::find($model1->getKey())->isDefault())->toBeFalse() + ->and(Country::find($model2->getKey())->isDefault())->toBeTrue(); +}); + +it('set as default model on update', function() { + $model1 = Country::factory()->createQuietly(['is_default' => true]); + $model2 = Country::factory()->createQuietly(['is_default' => false]); + + $model2->update(['is_default' => true]); + + expect(Country::find($model1->getKey())->isDefault())->toBeFalse() + ->and(Country::find($model2->getKey())->isDefault())->toBeTrue(); +}); + +it('updates the default model', function() { + $model1 = Country::factory()->createQuietly(['is_default' => true]); + $model2 = Country::factory()->createQuietly(['is_default' => false]); + + Country::clearDefaultModels(); + Country::updateDefault($model2->getKey()); + + expect(Country::find($model1->getKey())->isDefault())->toBeFalse() + ->and(Country::getDefaultKey())->toBe($model2->getKey()); +}); + +it('gets the default model', function() { + Country::factory()->createQuietly(['is_default' => true]); + Country::factory()->createQuietly(['is_default' => false]); + + $default = Country::getDefault(); + expect($default)->toEqual(Country::getDefault()) + ->and($default->isDefault())->toBeTrue() + ->and($default->defaultableKeyName())->toBe('country_id'); +}); + +it('clears specific default model', function() { + Country::factory()->createQuietly(['is_default' => true]); + Country::getDefault(); + + expect(Country::$defaultModels[Country::class])->not()->toBeEmpty(); + + Country::clearDefaultModel(); + + expect(Country::$defaultModels)->toBeEmpty(); +}); + +it('clears all default models', function() { + Country::factory()->createQuietly(['is_default' => true]); + Currency::factory()->createQuietly(['is_default' => true]); + Country::getDefault(); + Currency::getDefault(); + + expect(Country::$defaultModels[Country::class])->not()->toBeEmpty() + ->and(Currency::$defaultModels[Currency::class])->not()->toBeEmpty(); + + Country::clearDefaultModels(); + + expect(Country::$defaultModels)->toBeEmpty(); +}); + +it('throws validation exception when making default without switchable', function() { + $model = Country::factory()->createQuietly(['status' => false]); + + expect(fn() => $model->makeDefault())->toThrow(ValidationException::class); +}); + +it('returns defaultable column name', function() { + $model = new class extends Model + { + use Defaultable; + + public const DEFAULTABLE_COLUMN = 'const_is_default'; + }; + + expect($model->defaultableGetColumn())->toBe('const_is_default'); +}); + +it('returns defaultable attribute name value', function() { + $model = new class extends Model + { + use Defaultable; + + protected $attributes = ['name' => 'default_name']; + }; + + expect($model->defaultableName())->toBe('default_name'); +}); + +it('applies defaultable scope correctly', function() { + Country::factory()->createQuietly(['is_default' => true]); + Country::factory()->createQuietly(['is_default' => false]); + + $defaultModelQuery = Country::make()->defaultable()->whereIsDefault(); + $nonDefaultModelQuery = Country::make()->defaultable()->whereNotDefault(); + + expect($defaultModelQuery->toSql())->toContain('where `countries`.`is_default` = ?') + ->and($nonDefaultModelQuery->toSql())->toContain('where `countries`.`is_default` != ?'); +}); diff --git a/tests/src/System/Models/Concerns/HasCountryTest.php b/tests/src/System/Models/Concerns/HasCountryTest.php new file mode 100644 index 00000000..7d6d83c8 --- /dev/null +++ b/tests/src/System/Models/Concerns/HasCountryTest.php @@ -0,0 +1,42 @@ +getCountryRelationName(); + } + }; + + expect($model->testCountryRelationName())->toBe('const_is_default'); +}); + +it('saves model with single relation type country', function() { + $model = Currency::factory()->create(); + + $country = Country::factory()->create(); + $model->country()->associate($country)->save(); + + expect($model->country_id)->toBe($country->getKey()); +}); + +it('filters query by country with single relation type', function() { + $country = Country::factory()->create(); + Currency::factory()->for($country, 'country')->create(); + + $result = Currency::whereCountry($country->getKey())->first(); + + expect($result->country_id)->toBe($country->getKey()); +}); diff --git a/tests/src/System/Models/Concerns/SendsMailTemplateTest.php b/tests/src/System/Models/Concerns/SendsMailTemplateTest.php new file mode 100644 index 00000000..86743af6 --- /dev/null +++ b/tests/src/System/Models/Concerns/SendsMailTemplateTest.php @@ -0,0 +1,87 @@ +mailGetReplyTo())->toBeArray()->toBeEmpty(); + + $model = new class + { + use SendsMailTemplate; + + public function mailGetReplyTo(?string $type = null): array + { + return ['test@example.com', 'Test User']; + } + }; + + expect($model->mailGetReplyTo())->toBeArray()->toContain('test@example.com', 'Test User'); +}); + +it('returns recipients correctly', function() { + $model = new class + { + use SendsMailTemplate; + }; + + expect($model->mailGetRecipients('admin'))->toBeArray()->toBeEmpty(); + + $model = new class + { + use SendsMailTemplate; + + public function mailGetRecipients(string $type): array + { + return [['test@example.com', 'Test User']]; + } + }; + + expect($model->mailGetRecipients('admin'))->toEqual([['test@example.com', 'Test User']]); +}); + +it('sends mail with additional variables', function() { + $model = new class + { + use SendsMailTemplate; + }; + + expect($model->mailGetData())->toBeArray()->toBeEmpty(); + + $model = new class + { + use SendsMailTemplate; + + public function mailGetData(): array + { + return ['key' => 'value']; + } + }; + + expect($model->mailGetData())->toEqual(['key' => 'value']); +}); + +it('sends mail to valid recipients', function() { + MailHelper::shouldReceive('queueTemplate')->once()->with('view', [], [ + new Address('test@example.com', 'Test User'), + ]); + $model = new class + { + use SendsMailTemplate; + + public function mailGetRecipients(string $type): array + { + return [['test@example.com', 'Test User']]; + } + }; + + $model->mailSend('view', 'admin'); +}); diff --git a/tests/src/System/Models/Concerns/SwitchableTest.php b/tests/src/System/Models/Concerns/SwitchableTest.php new file mode 100644 index 00000000..a82cff6d --- /dev/null +++ b/tests/src/System/Models/Concerns/SwitchableTest.php @@ -0,0 +1,70 @@ +switchableGetColumn(); + + expect($column)->toBe('custom_status'); +}); + +it('returns default switchable column when not defined', function() { + $model = new class + { + use Switchable; + }; + + $column = $model->switchableGetColumn(); + + expect($column)->toBe('status'); +}); + +it('checks if model is enabled', function() { + $model = new class + { + use Switchable; + + public $status = true; + }; + + $isEnabled = $model->isEnabled(); + + expect($isEnabled)->toBeTrue(); +}); + +it('checks if model is disabled', function() { + $model = new class + { + use Switchable; + + public $status = false; + }; + + $isDisabled = $model->isDisabled(); + + expect($isDisabled)->toBeTrue(); +}); + +it('applies scope to get enabled models', function() { + expect(Page::query()->isEnabled()->toSql()) + ->toContain('where `pages`.`status` is not null and `pages`.`status` = ?'); +}); + +it('applies scope to get disabled models', function() { + expect(Page::whereIsDisabled()->toSql())->toContain('where `pages`.`status` != ?'); +}); + +it('applies switchable scope correctly', function() { + expect(Page::applySwitchable(true)->toRawSql())->toContain('where `pages`.`status` = 1'); + expect(Page::applySwitchable(false)->toRawSql())->toContain('where `pages`.`status` = 0'); +}); diff --git a/tests/src/System/Models/CountryTest.php b/tests/src/System/Models/CountryTest.php new file mode 100644 index 00000000..473c8485 --- /dev/null +++ b/tests/src/System/Models/CountryTest.php @@ -0,0 +1,52 @@ +isNotEmpty())->toBeTrue(); +}); + +it('returns defaultable name as country name', function() { + $country = new Country(['country_name' => 'Country 1']); + + $defaultableName = $country->defaultableName(); + + expect($defaultableName)->toBe('Country 1'); +}); + +it('sorts countries by priority', function() { + Country::create(['country_name' => 'Country 1', 'priority' => 2]); + Country::create(['country_name' => 'Country 2', 'priority' => 1]); + + $sortedCountries = Country::sorted()->get(); + + expect($sortedCountries->first()->country_name)->toBe('Country 2'); +}); + +it('configures model correctly', function() { + $country = new Country; + + expect(class_uses_recursive($country)) + ->toContain(Defaultable::class) + ->toContain(Sortable::class) + ->toContain(Switchable::class) + ->and($country->getTable())->toBe('countries') + ->and($country->getKeyName())->toBe('country_id') + ->and($country->getGuarded())->toBe([]) + ->and($country->getCasts())->toEqual([ + 'country_id' => 'int', + 'priority' => 'integer', + 'is_default' => 'boolean', + ]) + ->and($country->relation['hasOne'])->toEqual([ + 'currency' => \Igniter\System\Models\Currency::class, + ]) + ->and($country->timestamps)->toBeTrue(); +}); diff --git a/tests/src/System/Models/CurrencyTest.php b/tests/src/System/Models/CurrencyTest.php new file mode 100644 index 00000000..3ff3a720 --- /dev/null +++ b/tests/src/System/Models/CurrencyTest.php @@ -0,0 +1,93 @@ +create(); + Currency::factory()->for($country, 'country')->create(['currency_name' => 'Currency 1', 'currency_code' => 'CUR1', 'currency_symbol' => '$', 'currency_status' => 1]); + Currency::factory()->for($country, 'country')->create(['currency_name' => 'Currency 2', 'currency_code' => 'CUR2', 'currency_symbol' => '€', 'currency_status' => 0]); + + $options = Currency::getDropdownOptions(); + + expect($options)->toContain($country->country_name.' - CUR - $'); +}); + +it('returns the converter dropdown options', function() { + $options = Currency::getConverterDropdownOptions(); + + expect($options)->toEqual([ + 'openexchangerates' => 'lang:igniter::system.settings.text_openexchangerates', + 'fixerio' => 'lang:igniter::system.settings.text_fixerio', + ]); +}); + +it('getters returns values correctly', function() { + $currency = Currency::factory()->create(); + + expect($currency->getId())->toBe($currency->currency_id) + ->and($currency->getName())->toBe($currency->currency_name) + ->and($currency->getCode())->toBe($currency->currency_code) + ->and($currency->getSymbol())->toBe($currency->currency_symbol) + ->and($currency->getSymbolPosition())->toBe($currency->symbol_position); +}); + +it('returns defaultable name as currency name', function() { + $currency = new Currency(['currency_name' => 'Currency 1']); + + $defaultableName = $currency->defaultableName(); + + expect($defaultableName)->toBe('Currency 1'); +}); + +it('updates currency rate', function() { + $currency = Currency::factory()->create(['currency_name' => 'Currency 1', 'currency_rate' => 1.0]); + + $currency->updateRate(1.5); + + expect($currency->getRate())->toBe(1.5); +}); + +it('returns correct currency format with symbol at the end', function() { + $currency = new Currency(['currency_symbol' => '$', 'symbol_position' => 1, 'thousand_sign' => ',', 'decimal_sign' => '.', 'decimal_position' => 2]); + + $format = $currency->getFormat(); + + expect($format)->toBe('1,0.00$'); +}); + +it('returns correct currency format with symbol at the beginning', function() { + $currency = new Currency(['currency_symbol' => '$', 'symbol_position' => 0, 'thousand_sign' => ',', 'decimal_sign' => '.', 'decimal_position' => 2]); + + $format = $currency->getFormat(); + + expect($format)->toBe('$1,0.00'); +}); + +it('configures model correctly', function() { + $currency = new Currency; + + expect(class_uses_recursive($currency)) + ->toContain(Defaultable::class) + ->toContain(HasCountry::class) + ->toContain(Switchable::class) + ->and($currency->getTable())->toBe('currencies') + ->and($currency->getKeyName())->toBe('currency_id') + ->and($currency->getGuarded())->toBe([]) + ->and($currency->getCasts())->toEqual([ + 'currency_id' => 'int', + 'country_id' => 'integer', + 'currency_rate' => 'float', + 'symbol_position' => 'integer', + 'is_default' => 'boolean', + ]) + ->and($currency->relation['belongsTo'])->toEqual([ + 'country' => \Igniter\System\Models\Country::class, + ]) + ->and($currency->timestamps)->toBeTrue(); +}); diff --git a/tests/src/System/Models/ExtensionTest.php b/tests/src/System/Models/ExtensionTest.php new file mode 100644 index 00000000..ed78b7b7 --- /dev/null +++ b/tests/src/System/Models/ExtensionTest.php @@ -0,0 +1,317 @@ +instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('getActiveTheme')->andReturnNull(); + + expect(Extension::onboardingIsComplete())->toBeFalse(); +}); + +it('onboardingIsComplete returns false when a required extension is missing', function() { + $theme = new Theme('/path/to/theme', [ + 'require' => ['TestExtension' => '*'], + ]); + $themeManager = mock(ThemeManager::class); + app()->instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('getActiveTheme')->andReturn($theme); + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('findExtension')->with('TestExtension')->andReturnNull(); + + expect(Extension::onboardingIsComplete())->toBeFalse(); +}); + +it('onboardingIsComplete returns false when a required extension is disabled', function() { + $theme = new Theme('/path/to/theme', [ + 'require' => ['TestExtension' => '*'], + ]); + $extension = new class(app()) extends BaseExtension + { + public bool $disabled = true; + }; + $themeManager = mock(ThemeManager::class); + app()->instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('getActiveTheme')->andReturn($theme); + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('findExtension')->with('TestExtension')->andReturn($extension); + + expect(Extension::onboardingIsComplete())->toBeFalse(); +}); + +it('onboardingIsComplete returns true when all required extensions are enabled', function() { + $theme = new Theme('/path/to/theme', [ + 'require' => ['TestExtension' => '*'], + ]); + $extension = new class(app()) extends BaseExtension + { + }; + $themeManager = mock(ThemeManager::class); + app()->instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('getActiveTheme')->andReturn($theme); + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('findExtension')->with('TestExtension')->andReturn($extension); + + expect(Extension::onboardingIsComplete())->toBeTrue(); +}); + +it('returns default version when version attribute is null', function() { + $extension = new Extension(['version' => null]); + + $version = $extension->version; + + expect($version)->toBe('0.1.0'); +}); + +it('returns correct title from meta attribute', function() { + $extension = new Extension(); + $extension->class = new class(app()) extends BaseExtension + { + public function extensionMeta(): array + { + return [ + 'name' => 'Test Extension', + 'author' => 'Igniter Labs', + 'description' => 'A test extension', + 'icon' => 'fa-cog', + ]; + } + }; + + expect($extension->title)->toBe('Test Extension'); +}); + +it('returns correct status when extension is enabled', function() { + $extension = new Extension(); + $extension->class = new class(app()) extends BaseExtension + { + public bool $disabled = false; + }; + + expect($extension->status)->toBeTrue(); +}); + +it('returns correct status when extension is disabled', function() { + $extension = new Extension(); + $extension->class = new class(app()) extends BaseExtension + { + public bool $disabled = true; + }; + + expect($extension->status)->toBeFalse(); +}); + +it('returns correct description from meta attribute', function() { + $extension = new Extension(); + $extension->class = new class(app()) extends BaseExtension + { + public function extensionMeta(): array + { + return ['description' => 'Test Description']; + } + }; + + expect($extension->description)->toBe('Test Description'); +}); + +it('returns correct required from extension', function() { + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('isRequired')->with('test_extension')->andReturnTrue(); + + $extension = new Extension(['name' => 'test_extension']); + + expect($extension->required)->toBeTrue(); +}); + +it('returns correct icon from meta attribute', function() { + $extension = new Extension(); + $extension->class = new class(app()) extends BaseExtension + { + public function extensionMeta(): array + { + return [ + 'icon' => 'fa-cog', + ]; + } + }; + + $icon = $extension->icon; + + expect($icon['class'])->toBe('fa fa-cog') + ->and($icon['image'])->toBeNull() + ->and($icon['backgroundImage'])->toBeNull(); +}); + +it('returns correct icon image from meta attribute', function() { + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('path')->with('test_extension', 'image.png')->andReturn('/path/to/image.png'); + File::shouldReceive('exists')->andReturn(true); + File::shouldReceive('get')->with('/path/to/image.png')->andReturn('image content'); + + $extension = new Extension(['name' => 'test_extension']); + $extension->class = new class(app()) extends BaseExtension + { + public function extensionMeta(): array + { + return [ + 'icon' => [ + 'image' => 'image.png', + ], + ]; + } + }; + + $icon = $extension->icon; + + expect($icon['class'])->toBe('fa') + ->and($icon['image'])->toBe('image.png') + ->and($icon['backgroundImage'])->toBe([ + 'image/png', base64_encode('image content'), + ]) + ->and($icon['styles'])->toBe("background-image:url('data:image/png;base64,".base64_encode('image content')."');"); +}); + +it('throws exception when icon mime type is invalid', function() { + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('path')->with('test_extension', 'image.jpg')->andReturn('/path/to/image.jpg'); + File::shouldReceive('exists')->andReturn(true); + File::shouldReceive('get')->with('/path/to/image.jpg')->andReturn('image content'); + + $extension = new Extension(['name' => 'test_extension']); + $extension->class = new class(app()) extends BaseExtension + { + public function extensionMeta(): array + { + return [ + 'icon' => [ + 'image' => 'image.jpg', + ], + ]; + } + }; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid extension icon file type in: test_extension. Only SVG and PNG images are supported'); + + $extension->icon; +}); + +it('returns undefined description when meta description is not set', function() { + $extension = new Extension(); + $extension->class = new class(app()) extends BaseExtension + { + public function extensionMeta(): array + { + return []; + } + }; + + expect($extension->description)->toBe('Undefined extension description'); +}); + +it('returns correct readme content when readme file exists', function() { + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('path')->with('test_extension', 'readme.md')->andReturn('/path/to/readme.md'); + File::shouldReceive('existsInsensitive')->andReturn(true); + File::shouldReceive('get')->andReturn('Test **Readme**'); + + $extension = new Extension(['name' => 'test_extension']); + + expect($extension->readme)->toBe("

Test Readme

\n"); +}); + +it('returns null readme content when readme file does not exist', function() { + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('path')->with('test_extension', 'readme.md')->andReturn('/path/to/readme.md'); + File::shouldReceive('existsInsensitive')->andReturn(false); + + $extension = new Extension(['name' => 'test_extension']); + + expect($extension->readme)->toBeNull(); +}); + +it('applies extension class on fetch', function() { + $extensionClass = new class(app()) extends BaseExtension + { + public function extensionMeta(): array + { + return ['name' => 'Test Extension']; + } + }; + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('findExtension')->with('test_extension')->andReturn($extensionClass); + + Extension::flushEventListeners(); + Extension::create(['name' => 'test_extension']); + $extension = Extension::firstWhere('name', 'test_extension'); + + expect($extension->getExtensionObject())->toBe($extensionClass); +}); + +it('does not apply extension class when extension is not found', function() { + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('findExtension')->with('test_extension')->andReturnNull(); + + Extension::create(['name' => 'test_extension']); + $extension = Extension::firstWhere('name', 'test_extension'); + + expect($extension->class)->toBeNull(); +}); + +it('syncs available extensions from filesystem', function() { + $packageManifest = mock(PackageManifest::class); + app()->instance(PackageManifest::class, $packageManifest); + $packageManifest->shouldReceive('getVersion')->andReturn('1.0.0'); + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('namespaces')->andReturn(['test_extension' => '/path/to/test_extension']); + $extensionManager->shouldReceive('getIdentifier')->with('test_extension')->andReturn('test.extension'); + $extensionManager->shouldReceive('findExtension')->with('test.extension')->andReturn(mock(BaseExtension::class)); + + Extension::syncAll(); + + expect(Extension::firstWhere('name', 'test.extension'))->not->toBeNull(); +}); + +it('skips syncing extension not found in filesystem', function() { + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('namespaces')->andReturn(['test_extension' => '/path/to/test_extension']); + $extensionManager->shouldReceive('getIdentifier')->with('test_extension')->andReturn('test.extension'); + $extensionManager->shouldReceive('findExtension')->with('test.extension')->andReturnNull(); + + Extension::syncAll(); + + expect(Extension::firstWhere('name', 'test.extension'))->toBeNull(); +}); + +it('configures extension model correctly', function() { + $extension = new Extension; + + expect(Extension::ICON_MIMETYPES)->toEqual([ + 'png' => 'image/png', + 'svg' => 'image/svg+xml', + ]) + ->and($extension->getTable())->toBe('extensions') + ->and($extension->getKeyName())->toBe('extension_id') + ->and($extension->getFillable())->toEqual(['name', 'version']); +}); diff --git a/tests/src/System/Models/LanguageTest.php b/tests/src/System/Models/LanguageTest.php new file mode 100644 index 00000000..26abdec1 --- /dev/null +++ b/tests/src/System/Models/LanguageTest.php @@ -0,0 +1,158 @@ +toBeNull(); +}); + +it('returns language when finding by valid code', function() { + Language::create(['code' => 'en', 'name' => 'English']); + $result = Language::findByCode('en'); + + expect($result->code)->toBe('en') + ->and($result->name)->toBe('English'); +}); + +it('returns active locale', function() { + Language::factory()->create(['code' => 'en', 'status' => 1]); + + $result = Language::getActiveLocale(); + + expect($result->getKey())->toBe(Language::getActiveLocale()->getKey()); +}); + +it('returns supported languages list', function() { + Language::create(['code' => 'en', 'name' => 'English', 'status' => 1]); + Language::create(['code' => 'fr', 'name' => 'French', 'status' => 1]); + + $result = Language::listSupported(); + + expect($result)->toHaveCount(2) + ->and($result)->toHaveKey('en') + ->and($result)->toHaveKey('fr'); +}); + +it('returns true when more than one supported language', function() { + Language::create(['code' => 'en', 'name' => 'English', 'status' => 1]); + Language::create(['code' => 'fr', 'name' => 'French', 'status' => 1]); + + $result = Language::supportsLocale(); + + expect($result)->toBeTrue(); +}); + +it('adds translations successfully', function() { + $language = Language::create(['code' => 'en']); + $result = $language->addTranslations(['en::group.key' => ['translation' => 'value']]); + + expect($result)->toBeTrue(); +}); + +it('skips invalid translation keys', function() { + $language = Language::create(['code' => 'en']); + $result = $language->addTranslations(['invalid_key' => ['translation' => 'value']]); + + expect($result)->toBeTrue(); +}); + +it('returns group options for a given locale', function() { + $language = new Language(); + $localePackage = (object)['code' => 'en', 'name' => 'English']; + $languageManager = mock(LanguageManager::class); + app()->instance(LanguageManager::class, $languageManager); + $languageManager->shouldReceive('listLocalePackages')->with('en')->andReturn([$localePackage]); + + $result = $language->getGroupOptions('en'); + + expect($result)->toHaveKey('en', 'English'); +}); + +it('returns lines for a given locale, group, and namespace', function() { + $language = new Language(); + $lines = ['key' => 'value']; + $loader = mock(FileLoader::class); + app()->instance('translation.loader', $loader); + $loader->shouldReceive('load')->with('en', 'group', 'namespace')->andReturn($lines); + + $result = $language->getLines('en', 'group', 'namespace'); + + expect($result)->toHaveKey('key') + ->and($result['key'])->toBe('value'); +}); + +it('returns empty lines when no translations found', function() { + $language = new Language(['code' => 'en']); + $loader = mock(FileLoader::class); + app()->instance('translation.loader', $loader); + $loader->shouldReceive('load')->with('en', 'group', 'namespace')->andReturn([]); + + $result = $language->getTranslations('group', 'namespace'); + + expect($result)->toBeEmpty(); +}); + +it('updates translations successfully', function() { + $language = Language::create(['code' => 'en']); + $result = $language->updateTranslations('group', 'namespace', ['key' => 'new value']); + + expect($result)->toHaveKey('key') + ->and($result['key'])->toBe('new value'); +}); + +it('does not update translation if text is the same', function() { + $language = Language::create(['code' => 'en']); + Lang::shouldReceive('get')->andReturn('same value'); + + expect($language->updateTranslation('group', 'namespace', 'key', 'same value'))->toBeFalse(); +}); + +it('updates translation if text is different', function() { + $language = Language::create(['code' => 'en']); + Lang::shouldReceive('get')->andReturn('old value'); + + expect($language->updateTranslation('group', 'namespace', 'key', 'new value'))->toBeTrue(); +}); + +it('deletes related translations on language delete', function() { + $language = Language::create(['code' => 'en']); + $language->translations()->saveMany([ + new Translation(['locale' => 'en', 'code' => 'group', 'namespace' => 'namespace', 'item' => 'key', 'text' => 'value']), + new Translation(['locale' => 'en', 'code' => 'group', 'namespace' => 'namespace', 'item' => 'key2', 'text' => 'value2']), + ]); + + $language->delete(); + + expect(Translation::where('locale', 'en')->count())->toBe(0); +}); + +it('configures language model correctly', function() { + $language = new Language; + + expect(class_uses_recursive($language)) + ->toContain(Defaultable::class) + ->toContain(Purgeable::class) + ->toContain(Switchable::class) + ->and($language->getTable())->toBe('languages') + ->and($language->getKeyName())->toBe('language_id') + ->and($language->getCasts())->toEqual([ + 'language_id' => 'int', + 'original_id' => 'integer', + 'version' => 'array', + 'is_default' => 'boolean', + ]) + ->and($language->relation['hasMany'])->toEqual([ + 'translations' => [Translation::class, 'foreignKey' => 'locale', 'otherKey' => 'code', 'delete' => true], + ]) + ->and($language->timestamps)->toBeTrue() + ->and($language->defaultableKeyName())->toBe('code'); +}); diff --git a/tests/src/System/Models/MailLayoutTest.php b/tests/src/System/Models/MailLayoutTest.php new file mode 100644 index 00000000..8c2f208e --- /dev/null +++ b/tests/src/System/Models/MailLayoutTest.php @@ -0,0 +1,124 @@ +create(['name' => 'Test Layout']); + $result = MailLayout::getDropdownOptions(); + + expect($result)->toHaveKey($layout->layout_id, 'Test Layout'); +}); + +it('returns cached list of codes', function() { + MailLayout::factory()->create(['code' => 'test_code']); + + $result = MailLayout::listCodes(); + + expect($result)->toBe(MailLayout::listCodes()); +}); + +it('returns id from code', function() { + MailLayout::$codeCache = null; + $layout = MailLayout::factory()->create(['code' => 'test_code']); + $result = MailLayout::getIdFromCode('test_code'); + + expect($result)->toBe($layout->layout_id); +}); + +it('throws exception when filling from invalid code', function() { + $mailManager = mock(MailManager::class); + app()->instance(MailManager::class, $mailManager); + $mailManager->shouldReceive('listRegisteredLayouts')->andReturn([]); + + $layout = new MailLayout(['code' => 'invalid_code']); + + expect(fn() => $layout->fillFromCode())->toThrow(SystemException::class); +}); + +it('returns null when code is null', function() { + $layout = new MailLayout; + + expect($layout->fillFromCode())->toBeNull(); +}); + +it('fills layout from valid code', function() { + $mailManager = mock(MailManager::class); + app()->instance(MailManager::class, $mailManager); + $mailManager->shouldReceive('listRegisteredLayouts')->andReturn(['test_code' => 'test_path']); + + $layout = new MailLayout(['code' => 'test_code']); + File::shouldReceive('get')->once()->andReturn("name = Hey\n===\ntext_content\n===\nhtml_content\n"); + View::shouldReceive('make->getPath')->andReturn('test_path'); + + $layout->fillFromCode(); + + expect($layout->name)->toBe('Hey') + ->and($layout->layout)->toBe('html_content') + ->and($layout->plain_layout)->toBe('text_content'); +}); + +it('fills layout from valid code on fetch', function() { + $mailManager = mock(MailManager::class); + app()->instance(MailManager::class, $mailManager); + $mailManager->shouldReceive('listRegisteredLayouts')->andReturn(['test_code' => 'test_path']); + + new MailLayout(['code' => 'test_code']); + File::shouldReceive('get')->once()->andReturn("name = Hey\n===\ntext_content\n===\nhtml_content\n"); + View::shouldReceive('make->getPath')->andReturn('test_path'); + + MailLayout::flushEventListeners(); + $layout = MailLayout::factory()->create(['code' => 'test_code', 'is_locked' => false]); + $layout = MailLayout::find($layout->getKey()); + + expect($layout->name)->toBe('Hey') + ->and($layout->layout)->toBe('html_content') + ->and($layout->plain_layout)->toBe('text_content'); +}); + +it('creates layouts if not exist', function() { + MailLayout::factory()->create(['code' => 'valid_code']); + $mailManager = mock(MailManager::class); + app()->instance(MailManager::class, $mailManager); + $mailManager->shouldReceive('listRegisteredLayouts')->andReturn([ + 'valid_code' => 'valid_path', + 'test_code' => 'test_path', + ]); + + File::shouldReceive('get')->andReturn('file_content'); + View::shouldReceive('make->getPath')->andReturn('test_path'); + + MailLayout::createLayouts(); + + $layout = MailLayout::where('code', 'test_code')->first(); + expect($layout)->not->toBeNull() + ->and($layout->name)->toBe('???'); +}); + +it('configures mail layout model correctly', function() { + $layout = new MailLayout; + + expect(class_uses_recursive($layout)) + ->toContain(Switchable::class) + ->and($layout->getTable())->toBe('mail_layouts') + ->and($layout->getKeyName())->toBe('layout_id') + ->and($layout->timestamps)->toBeTrue() + ->and($layout->getCasts())->toEqual([ + 'layout_id' => 'int', + 'language_id' => 'integer', + 'status' => 'boolean', + 'is_locked' => 'boolean', + ]) + ->and($layout->relation['hasMany'])->toEqual([ + 'templates' => [\Igniter\System\Models\MailTemplate::class, 'foreignKey' => 'layout_id'], + ]) + ->and($layout->relation['belongsTo'])->toEqual([ + 'language' => \Igniter\System\Models\Language::class, + ]); +}); diff --git a/tests/src/System/Models/MailPartialTest.php b/tests/src/System/Models/MailPartialTest.php new file mode 100644 index 00000000..f91db377 --- /dev/null +++ b/tests/src/System/Models/MailPartialTest.php @@ -0,0 +1,102 @@ +instance(MailManager::class, $mailManager); + $mailManager->shouldReceive('listRegisteredPartials')->andReturn(['_mail.test_partial' => 'test_path']); + + File::shouldReceive('get')->once()->andReturn("name = Test Partial\n===\ntext_content\n===\nhtml_content\n"); + View::shouldReceive('make->getPath')->andReturn('test_path'); + + MailPartial::create(['code' => '_mail.test_partial']); + $result = MailPartial::findOrMakePartial('_mail.test_partial'); + + expect($result->code)->toBe('_mail.test_partial'); +}); + +it('creates partial when not found by code', function() { + $mailManager = mock(MailManager::class); + app()->instance(MailManager::class, $mailManager); + $mailManager->shouldReceive('listRegisteredPartials')->andReturn(['test_code' => 'test_path']); + + File::shouldReceive('get')->once()->andReturn("name = Test Partial\n===\ntext_content\n===\nhtml_content\n"); + View::shouldReceive('make->getPath')->andReturn('test_path'); + + $result = MailPartial::findOrMakePartial('test_code'); + + expect($result->code)->toBe('test_code') + ->and($result->name)->toBe('Test Partial'); +}); + +it('throws exception when filling from invalid code', function() { + $mailManager = mock(MailManager::class); + app()->instance(MailManager::class, $mailManager); + $mailManager->shouldReceive('listRegisteredPartials')->andReturn([]); + + $partial = new MailPartial(['code' => 'test_code']); + + expect(fn() => $partial->fillFromCode())->toThrow(SystemException::class); +}); + +it('returns null when code is null', function() { + $partial = new MailPartial; + + expect($partial->fillFromCode())->toBeNull(); +}); + +it('fills partial from valid code', function() { + $mailManager = mock(MailManager::class); + app()->instance(MailManager::class, $mailManager); + $mailManager->shouldReceive('listRegisteredPartials')->andReturn(['test_code' => 'test_path']); + + File::shouldReceive('get')->once()->andReturn("name = Test Partial\n===\ntext_content\n===\nhtml_content\n"); + View::shouldReceive('make->getPath')->andReturn('test_path'); + + $partial = new MailPartial(['code' => 'test_code']); + $partial->fillFromCode(); + + expect($partial->name)->toBe('Test Partial') + ->and($partial->html)->toBe('html_content') + ->and($partial->text)->toBe('text_content'); +}); + +it('creates partials if not exist', function() { + MailPartial::create(['code' => 'valid_code']); + $mailManager = mock(MailManager::class); + app()->instance(MailManager::class, $mailManager); + $mailManager->shouldReceive('listRegisteredPartials')->andReturn([ + 'valid_code' => 'valid_path', + 'test_code' => 'test_path', + ]); + + File::shouldReceive('get')->once()->andReturn("name = Test Partial\n===\ntext_content\n===\nhtml_content\n"); + View::shouldReceive('make->getPath')->andReturn('test_path'); + + MailPartial::createPartials(); + + $partial = MailPartial::where('code', 'test_code')->first(); + + expect($partial)->not->toBeNull() + ->and($partial->name)->toBe('Test Partial'); +}); + +it('configures mail partial correctly', function() { + $partial = new MailPartial; + + expect($partial->getTable())->toBe('mail_partials') + ->and($partial->getKeyName())->toBe('partial_id') + ->and($partial->getGuarded())->toEqual([]) + ->and($partial->timestamps)->toBeTrue() + ->and($partial->getCasts())->toEqual([ + 'partial_id' => 'int', + 'is_custom' => 'boolean', + ]); +}); diff --git a/tests/src/System/Models/MailTemplateTest.php b/tests/src/System/Models/MailTemplateTest.php new file mode 100644 index 00000000..b4e89320 --- /dev/null +++ b/tests/src/System/Models/MailTemplateTest.php @@ -0,0 +1,123 @@ +instance(MailManager::class, $mailManager); + $mailManager->shouldReceive('listRegisteredVariables')->andReturn(['var1' => 'Variable 1']); + + $result = MailTemplate::getVariableOptions(); + + expect($result)->toHaveKey('var1', 'Variable 1'); +}); + +it('return title attribute value', function() { + $template = new MailTemplate(['label' => 'Test Subject']); + expect($template->title)->toBe('Test Subject'); +}); + +it('fills template from view on fetch', function() { + File::shouldReceive('get')->once()->andReturn("subject = Test Subject\n===\ntext_content\n===\nhtml_content\n"); + View::shouldReceive('make->getPath')->andReturn('test_path'); + + MailTemplate::flushEventListeners(); + $template = MailTemplate::create(['code' => 'test_code', 'is_custom' => false]); + $template = MailTemplate::find($template->getKey()); + + expect($template->subject)->toBe('Test Subject') + ->and($template->body)->toBe('html_content') + ->and($template->plain_body)->toBe('text_content'); +}); + +it('fills template from content', function() { + $template = new MailTemplate(); + $template->fillFromContent("subject = Test Subject\n===\ntext_content\n===\nhtml_content\n"); + + expect($template->subject)->toBe('Test Subject') + ->and($template->body)->toBe('html_content') + ->and($template->plain_body)->toBe('text_content'); +}); + +it('fills template from view', function() { + File::shouldReceive('get')->once()->andReturn("subject = Test Subject\n===\ntext_content\n===\nhtml_content\n"); + View::shouldReceive('make->getPath')->andReturn('test_path'); + + $template = new MailTemplate(['code' => 'test_code']); + $template->fillFromView(); + + expect($template->subject)->toBe('Test Subject') + ->and($template->body)->toBe('html_content') + ->and($template->plain_body)->toBe('text_content'); +}); + +it('synchronizes all templates to the database', function() { + $mailManager = mock(MailManager::class); + app()->instance(MailManager::class, $mailManager); + $mailManager->shouldReceive('listRegisteredLayouts')->andReturn(['test_code' => 'test_path']); + $mailManager->shouldReceive('listRegisteredPartials')->andReturn(['test_code' => 'test_path']); + $mailManager->shouldReceive('listRegisteredTemplates')->andReturn(['test_code' => 'Test Label']); + + File::shouldReceive('get')->andReturn("subject = Test Subject\n===\ntext_content\n===\nhtml_content\n"); + View::shouldReceive('make->getPath')->andReturn('test_path'); + + MailTemplate::create(['code' => 'custom_code', 'is_custom' => true]); + MailTemplate::create(['code' => 'existing_code']); + + MailTemplate::syncAll(); + + $template = MailTemplate::where('code', 'test_code')->first(); + expect($template)->not->toBeNull() + ->and($template->label)->toBe('Test Label') + ->and(MailTemplate::where('code', 'existing_code')->exists())->toBeFalse(); +}); + +it('returns template when found by code', function() { + MailTemplate::create(['code' => 'test_code']); + $result = MailTemplate::findOrMakeTemplate('test_code'); + + expect($result->code)->toBe('test_code'); +}); + +it('creates template when not found by code', function() { + File::shouldReceive('get')->once()->andReturn("subject = Test Subject\n===\ntext_content\n===\nhtml_content\n"); + View::shouldReceive('make->getPath')->andReturn('test_path'); + + $result = MailTemplate::findOrMakeTemplate('test_code'); + + expect($result->code)->toBe('test_code') + ->and($result->subject)->toBe('Test Subject'); +}); + +it('returns list of all templates', function() { + MailTemplate::create(['code' => 'test_code']); + $mailManager = mock(MailManager::class); + app()->instance(MailManager::class, $mailManager); + $mailManager->shouldReceive('listRegisteredTemplates')->andReturn(['registered_code' => 'Registered Label']); + + $result = MailTemplate::listAllTemplates(); + + expect($result)->toHaveKey('registered_code'); +}); + +it('configures mail template correctly', function() { + $template = new MailTemplate; + + expect($template->getTable())->toEqual('mail_templates') + ->and($template->getKeyName())->toEqual('template_id') + ->and($template->getGuarded())->toEqual([]) + ->and($template->getCasts())->toEqual([ + 'template_id' => 'int', + 'layout_id' => 'integer', + ]) + ->and($template->relation['belongsTo'])->toEqual([ + 'layout' => [\Igniter\System\Models\MailLayout::class, 'foreignKey' => 'layout_id'], + ]) + ->and($template->getAppends())->toEqual(['title']) + ->and($template->timestamps)->toBeTrue(); +}); diff --git a/tests/src/System/Models/MailThemeTest.php b/tests/src/System/Models/MailThemeTest.php index 8a383351..88114b24 100644 --- a/tests/src/System/Models/MailThemeTest.php +++ b/tests/src/System/Models/MailThemeTest.php @@ -2,8 +2,83 @@ namespace Igniter\Tests\System\Models; +use Igniter\Flame\Support\Facades\File; +use Igniter\System\Actions\SettingsModel; use Igniter\System\Models\MailTheme; +use Illuminate\Support\Facades\Cache; -it('compiles theme default css file', function() { - expect(MailTheme::compileCss())->toBeString(); +it('initializes settings data with default values', function() { + $mailTheme = new MailTheme(); + $mailTheme->initSettingsData(); + + expect($mailTheme->body_bg)->toBe(MailTheme::BODY_BG) + ->and($mailTheme->content_bg)->toBe(MailTheme::WHITE_COLOR); +}); + +it('resets cache after saving', function() { + Cache::put((new MailTheme)->cacheKey, 'cached_data'); + + MailTheme::flushEventListeners(); + $mailTheme = MailTheme::create(); + + expect(Cache::get($mailTheme->cacheKey))->toBeNull(); +}); + +it('renders CSS from cache if available', function() { + $cacheKey = (new MailTheme)->cacheKey; + Cache::put($cacheKey, 'cached_data'); + + $result = MailTheme::renderCss(); + + expect($result)->toBe('cached_data'); +}); + +it('compiles and caches CSS if not available in cache', function() { + File::shouldReceive('symbolizePath')->with('igniter::views/system/_mail/themes/default.css')->andReturn('file_path'); + File::shouldReceive('get')->with('file_path')->andReturn('compiled_css'); + + $result = MailTheme::renderCss(); + + expect($result)->toBe('compiled_css'); +}); + +it('throws exception when rendering css', function() { + File::shouldReceive('symbolizePath')->with('igniter::views/system/_mail/themes/default.css')->andReturn('file_path'); + File::shouldReceive('get')->with('file_path')->andThrow(new \Exception('Error compiling CSS')); + + $result = MailTheme::renderCss(); + + expect($result)->toBe('/* Error compiling CSS */'); +}); + +it('compiles CSS from file', function() { + File::shouldReceive('symbolizePath')->with('igniter::views/system/_mail/themes/default.css')->andReturn('file_path'); + File::shouldReceive('get')->with('file_path')->andReturn('file_css'); + + $result = MailTheme::compileCss(); + + expect($result)->toBe('file_css'); +}); + +it('makes CSS variable correctly', function() { + $mailTheme = new class extends MailTheme + { + public static function testMakeCssVars() + { + return static::makeCssVars(); + } + }; + + $result = $mailTheme::testMakeCssVars(); + + expect($result)->toHaveKey('body-bg', MailTheme::BODY_BG); +}); + +it('configures mail theme model correctly', function() { + $mailTheme = new MailTheme; + + expect($mailTheme->implement)->toContain(SettingsModel::class) + ->and($mailTheme->settingsCode)->toBe('system_mail_theme_settings') + ->and($mailTheme->settingsFieldsConfig)->toBe('mail_themes') + ->and($mailTheme->cacheKey)->toBe('system::mailtheme.custom_css'); }); diff --git a/tests/src/System/Models/Observers/LanguageObserverTest.php b/tests/src/System/Models/Observers/LanguageObserverTest.php new file mode 100644 index 00000000..8e7ff32d --- /dev/null +++ b/tests/src/System/Models/Observers/LanguageObserverTest.php @@ -0,0 +1,25 @@ + 'en']); + + (new LanguageObserver)->creating($language); + + expect($language->idiom)->toBe('en'); +}); + +it('applies supported languages after saving', function() { + $language = mock(Language::class)->makePartial(); + + $language->shouldReceive('restorePurgedValues')->once(); + $language->shouldReceive('getAttributes')->andReturn([ + 'translations' => [], + ]); + + (new LanguageObserver)->saved($language); +}); diff --git a/tests/src/System/Models/PageTest.php b/tests/src/System/Models/PageTest.php new file mode 100644 index 00000000..d1e9852d --- /dev/null +++ b/tests/src/System/Models/PageTest.php @@ -0,0 +1,40 @@ +hidden()->create(['title' => 'Test Page', 'permalink_slug' => 'test-page', 'language_id' => 1, 'status' => 1]); + + $result = Page::getDropdownOptions(); + + expect($result)->toHaveKey('test-page', 'Test Page'); +}); + +it('configures page model correctly', function() { + $page = new Page; + + expect(class_uses_recursive($page)) + ->toContain(HasPermalink::class) + ->toContain(Switchable::class) + ->and($page->getTable())->toBe('pages') + ->and($page->getKeyName())->toBe('page_id') + ->and($page->timestamps)->toBeTrue() + ->and($page->getGuarded())->toBe([]) + ->and($page->getCasts())->toEqual([ + 'page_id' => 'int', + 'language_id' => 'integer', + 'metadata' => 'json', + ]) + ->and($page->relation['belongsTo'])->toEqual([ + 'language' => \Igniter\System\Models\Language::class, + ]) + ->and($page->permalinkable())->toEqual([ + 'permalink_slug' => [ + 'source' => 'title', + ], + ]); +}); diff --git a/tests/src/System/Models/RequestLogTest.php b/tests/src/System/Models/RequestLogTest.php new file mode 100644 index 00000000..88606e48 --- /dev/null +++ b/tests/src/System/Models/RequestLogTest.php @@ -0,0 +1,62 @@ +andReturn(true); + setting()->set(['enable_request_log' => true]); + request()->headers->set('referer', 'http://referrer.com'); + + $log = RequestLog::createLog(); + + expect($log)->not->toBeNull() + ->and($log->url)->toBe('http://localhost') + ->and($log->status_code)->toBe(404) + ->and($log->referrer)->toContain('http://referrer.com') + ->and($log->count)->toBe(1); +}); + +it('increments count for existing log entry', function() { + Igniter::shouldReceive('hasDatabase')->andReturn(true); + setting()->set(['enable_request_log' => true]); + request()->headers->set('referer', 'http://referrer.com'); + + RequestLog::create(['url' => 'http://localhost', 'status_code' => 404, 'count' => 1]); + + $log = RequestLog::createLog(); + + expect($log)->not->toBeNull() + ->and($log->count)->toBe(2); +}); + +it('does not create log entry if database is not available', function() { + Igniter::shouldReceive('hasDatabase')->andReturn(false); + + $log = RequestLog::createLog(); + + expect($log)->toBeNull(); +}); + +it('does not create log entry if logging is disabled', function() { + Igniter::shouldReceive('hasDatabase')->andReturn(true); + setting()->set(['enable_request_log' => false]); + + $log = RequestLog::createLog(); + + expect($log)->toBeNull(); +}); + +it('prunes old log entries', function() { + setting()->set(['activity_log_timeout' => 30]); + RequestLog::create(['created_at' => now()->subDays(31)]); + RequestLog::create(['created_at' => now()->subDays(29)]); + + (new RequestLog)->pruneAll(); + + $logs = RequestLog::all(); + expect($logs)->toHaveCount(1) + ->and($logs->first()->created_at->diffInDays(now()))->toBeLessThan(30); +}); diff --git a/tests/src/System/Models/SettingsTest.php b/tests/src/System/Models/SettingsTest.php new file mode 100644 index 00000000..8d34e3fb --- /dev/null +++ b/tests/src/System/Models/SettingsTest.php @@ -0,0 +1,192 @@ +registerSettingItems('core', [ + 'settings' => [], + ]); + + $result = Settings::listMenuSettingItems(null, null, null); + + expect($result)->toBeArray(); +}); + +it('returns date format options', function() { + $result = Settings::getDateFormatOptions(); + + expect($result)->toHaveKey('d M Y') + ->and($result['d M Y'])->toBe(Carbon::now()->format('d M Y')); +}); + +it('returns time format options', function() { + $result = Settings::getTimeFormatOptions(); + + expect($result)->toHaveKey('h:i A', Carbon::now()->format('h:i A')); +}); + +it('returns page limit options', function() { + $result = Settings::getPageLimitOptions(); + + expect($result)->toHaveKey('10', '10'); +}); + +it('returns menus page options when theme is active', function() { + $themeManager = mock(ThemeManager::class); + app()->instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('getActiveThemeCode')->andReturn('test_theme'); + + $result = Settings::getMenusPageOptions(); + + expect($result)->toBeArray(); +}); + +it('returns empty menus page options when no active theme', function() { + $themeManager = mock(ThemeManager::class); + app()->instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('getActiveThemeCode')->andReturnNull(); + + $result = Settings::getMenusPageOptions(); + + expect($result)->toBeEmpty(); +}); + +it('returns reservation page options when theme is active', function() { + $themeManager = mock(ThemeManager::class); + app()->instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('getActiveThemeCode')->andReturn('test_theme'); + + $result = Settings::getReservationPageOptions(); + + expect($result)->toBeArray(); +}); + +it('returns empty reservation page options when no active theme', function() { + $themeManager = mock(ThemeManager::class); + app()->instance(ThemeManager::class, $themeManager); + $themeManager->shouldReceive('getActiveThemeCode')->andReturnNull(); + + $result = Settings::getReservationPageOptions(); + + expect($result)->toBeEmpty(); +}); + +it('checks if onboarding is complete', function() { + Session::shouldReceive('has')->with('settings.errors')->andReturn(true); + Session::shouldReceive('get')->with('settings.errors')->andReturn([]); + + $result = Settings::onboardingIsComplete(); + + expect($result)->toBeTrue(); +}); + +it('checks if onboarding is not complete', function() { + Session::shouldReceive('has')->with('settings.errors')->andReturn(false); + + $result = Settings::onboardingIsComplete(); + + expect($result)->toBeFalse(); +}); + +it('gets value attribute as unserialized', function() { + $settings = new Settings(['value' => serialize(['key' => 'value'])]); + + $result = $settings->value; + + expect($result)->toBeArray() + ->and($result)->toHaveKey('key', 'value'); +}); + +it('gets value attribute as original', function() { + $settings = new Settings(['value' => 'original_value']); + + $result = $settings->value; + + expect($result)->toBe('original_value'); +}); + +it('sets and gets configuration value', function() { + Settings::set('test_key', 'test_value'); + + $result = Settings::get('test_key'); + + expect($result)->toBe('test_value'); +}); + +it('sets and gets preference value', function() { + Settings::setPref('pref_key', 'pref_value'); + + $result = Settings::getPref('pref_key'); + + expect($result)->toBe('pref_value'); +}); + +it('returns null for field values when database is not configured', function() { + Igniter::shouldReceive('hasDatabase')->andReturnFalse(); + $result = (new Settings)->getFieldValues(); + + expect($result)->toBe([]); +}); + +it('removes core setting item', function() { + $settings = new Settings; + $settings->loadSettingItems(); + $settings->removeSettingItem('core.general'); + + expect($settings->getSettingItem('core.general'))->toBeNull(); +}); + +it('removes extension setting item', function() { + $settings = new Settings; + $settings->loadSettingItems(); + $settings->removeSettingItem('igniter.payregister.settings'); + + expect($settings->getSettingItem('igniter.payregister.settings'))->toBeNull(); +}); + +it('returns list of timezones', function() { + $result = Settings::listTimezones(); + + expect($result)->toHaveKey('UTC', 'UTC (UTC -00:00)'); +}); + +it('returns default extensions', function() { + $result = Settings::defaultExtensions(); + + expect($result)->toContain('jpg') + ->and($result)->toContain('mp4'); +}); + +it('returns image extensions', function() { + $result = Settings::imageExtensions(); + + expect($result)->toContain('jpg') + ->and($result)->toContain('png'); +}); + +it('returns video extensions', function() { + $result = Settings::videoExtensions(); + + expect($result)->toContain('mp4') + ->and($result)->toContain('avi'); +}); + +it('returns audio extensions', function() { + $result = Settings::audioExtensions(); + + expect($result)->toContain('mp3') + ->and($result)->toContain('wav'); +}); + +it('configures settings model correctly', function() { + $settings = new Settings; + + expect($settings->getTable())->toEqual('settings') + ->and($settings->getKeyName())->toEqual('setting_id'); +}); diff --git a/tests/src/System/Models/TranslationTest.php b/tests/src/System/Models/TranslationTest.php new file mode 100644 index 00000000..0988a02c --- /dev/null +++ b/tests/src/System/Models/TranslationTest.php @@ -0,0 +1,14 @@ + 'Old Text', 'locked' => false]); + $result = $translation->updateAndLock('New Text'); + + expect($result)->toBeTrue() + ->and($translation->fresh()->text)->toBe('New Text') + ->and($translation->fresh()->locked)->toBeTrue(); +}); diff --git a/tests/src/System/Notifications/UpdateFoundNotificationTest.php b/tests/src/System/Notifications/UpdateFoundNotificationTest.php new file mode 100644 index 00000000..8b2baf38 --- /dev/null +++ b/tests/src/System/Notifications/UpdateFoundNotificationTest.php @@ -0,0 +1,67 @@ +superUser()->create(['status' => true]); + $disabledSuperUser = User::factory()->superUser()->create(['status' => false]); + User::factory()->create(['status' => true]); + + $notification = new UpdateFoundNotification(); + $recipients = $notification->getRecipients(); + + expect(collect($recipients)->pluck('user_id'))->toContain($enabledSuperUser->getKey()) + ->and(collect($recipients)->pluck('user_id'))->not->toContain($disabledSuperUser->getKey()); +}); + +it('returns correct title', function() { + $notification = new UpdateFoundNotification(); + $title = $notification->getTitle(); + + expect($title)->toBe(lang('igniter::system.updates.notify_new_update_found_title')); +}); + +it('returns correct URL', function() { + $notification = new UpdateFoundNotification(); + $url = $notification->getUrl(); + + expect($url)->toBe(admin_url('updates')); +}); + +it('returns correct message for single update', function() { + $notification = new UpdateFoundNotification(1); + $message = $notification->getMessage(); + + expect($message)->toBe(lang('igniter::system.updates.notify_new_update_found')); +}); + +it('returns correct message for multiple updates', function() { + $notification = new UpdateFoundNotification(5); + $message = $notification->getMessage(); + + expect($message)->toBe(sprintf(lang('igniter::system.updates.notify_new_updates_found'), 5)); +}); + +it('returns correct icon', function() { + $notification = new UpdateFoundNotification(); + $icon = $notification->getIcon(); + + expect($icon)->toBe('fa-cloud-arrow-down'); +}); + +it('returns correct icon color', function() { + $notification = new UpdateFoundNotification(); + $iconColor = $notification->getIconColor(); + + expect($iconColor)->toBe('success'); +}); + +it('returns correct alias', function() { + $notification = new UpdateFoundNotification(); + $alias = $notification->getAlias(); + + expect($alias)->toBe('update-found'); +}); diff --git a/tests/src/System/Providers/EventServiceProviderTest.php b/tests/src/System/Providers/EventServiceProviderTest.php new file mode 100644 index 00000000..4e178e8d --- /dev/null +++ b/tests/src/System/Providers/EventServiceProviderTest.php @@ -0,0 +1,29 @@ +once(); + + Event::dispatch('cache:cleared'); +}); + +it('registers model observer correctly', function() { + $eventServiceProvider = new class(app()) extends EventServiceProvider + { + public function listObservers() + { + return $this->observers; + } + }; + + expect($eventServiceProvider->listObservers())->toBe([ + Language::class => LanguageObserver::class, + ]); +}); diff --git a/tests/src/System/Providers/ExtensionServiceProviderTest.php b/tests/src/System/Providers/ExtensionServiceProviderTest.php new file mode 100644 index 00000000..846aa17d --- /dev/null +++ b/tests/src/System/Providers/ExtensionServiceProviderTest.php @@ -0,0 +1,48 @@ +instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('getExtensions')->andReturn([$extension1, $extension2]); + + $appMock = mock(Application::class); + $appMock->shouldReceive('register')->with($extension1)->once(); + $appMock->shouldReceive('register')->with($extension2)->once(); + + (new ExtensionServiceProvider($appMock))->register(); +}); + +it('allows extensions to use the scheduler', function() { + $extension1 = mock(BaseExtension::class); + $extension2 = mock(BaseExtension::class); + $extensionManager = mock(ExtensionManager::class); + app()->instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('getExtensions')->andReturn([$extension1, $extension2]); + + $appMock = mock(Application::class); + $appMock->shouldReceive('register')->with($extension1)->once(); + $appMock->shouldReceive('register')->with($extension2)->once(); + + $schedule = mock(Schedule::class); + $extension1->shouldReceive('registerSchedule')->with($schedule)->once(); + $extension2->shouldReceive('registerSchedule')->with($schedule)->once(); + + Event::shouldReceive('listen')->withArgs(function($event, $callback) use ($schedule) { + $callback($schedule); + + return $event === 'console.schedule'; + }); + + (new ExtensionServiceProvider($appMock))->register(); +}); diff --git a/tests/src/System/Providers/ValidationServiceProviderTest.php b/tests/src/System/Providers/ValidationServiceProviderTest.php new file mode 100644 index 00000000..77eaf5da --- /dev/null +++ b/tests/src/System/Providers/ValidationServiceProviderTest.php @@ -0,0 +1,24 @@ +instance(ExtensionManager::class, $extensionManager); + $extensionManager->shouldReceive('getRegistrationMethodValues') + ->with('registerValidationRules') + ->andReturn([ + ['custom_rule' => function($attribute, $value, $parameters) { + return $value === 'custom'; + }], + ]); + + (new ValidationServiceProvider(app()))->register(); + + app()->forgetInstance('validator'); + $validator = resolve('validator')->make(['field' => 'custom'], ['field' => 'custom_rule']); + expect($validator->passes())->toBeTrue(); +}); diff --git a/tests/src/System/Traits/AssetMakerTest.php b/tests/src/System/Traits/AssetMakerTest.php new file mode 100644 index 00000000..b25f8d44 --- /dev/null +++ b/tests/src/System/Traits/AssetMakerTest.php @@ -0,0 +1,117 @@ +once(); + $assetMaker = new class + { + use AssetMaker; + }; + + $assetMaker->flushAssets(); +}); + +it('returns full URL if file name starts with http', function() { + $assetMaker = new class + { + use AssetMaker; + }; + + $result = $assetMaker->getAssetPath('http://example.com/file.js'); + expect($result)->toBe('http://example.com/file.js'); +}); + +it('returns symbolized path if file name is symbolized', function() { + File::shouldReceive('symbolizePath')->with('~/file.js', null)->andReturn('/symbolized/path/file.js'); + $assetMaker = new class + { + use AssetMaker; + }; + + $result = $assetMaker->getAssetPath('~/file.js'); + expect($result)->toBe('/symbolized/path/file.js'); +}); + +it('returns file path from asset path array', function() { + File::shouldReceive('symbolizePath')->with('file.js', null)->andReturnNull(); + File::shouldReceive('symbolizePath')->with('/assets/file.js')->andReturn('/assets/file.js'); + File::shouldReceive('isFile')->with('/assets/file.js')->andReturn(true); + $assetMaker = new class + { + use AssetMaker; + }; + $assetMaker->assetPath = ['/assets']; + + $result = $assetMaker->getAssetPath('file.js'); + expect($result)->toBe('/assets/file.js'); +}); + +it('returns original file name if file not found in asset path', function() { + File::shouldReceive('symbolizePath')->andReturnNull(); + File::shouldReceive('isFile')->andReturn(false); + $assetMaker = new class + { + use AssetMaker; + }; + $assetMaker->assetPath = ['/assets']; + + $result = $assetMaker->getAssetPath('file.js'); + expect($result)->toBe('file.js'); +}); + +it('adds JavaScript asset', function() { + Assets::shouldReceive('addJs')->with('/assets/file.js', null)->once(); + File::shouldReceive('symbolizePath')->with('file.js', null)->andReturnNull(); + File::shouldReceive('symbolizePath')->with('/assets/file.js')->andReturn('/assets/file.js'); + File::shouldReceive('isFile')->with('/assets/file.js')->andReturn(true); + $assetMaker = new class + { + use AssetMaker; + }; + $assetMaker->assetPath = ['/assets']; + + $assetMaker->addJs('file.js'); +}); + +it('adds CSS asset', function() { + Assets::shouldReceive('addCss')->with('/assets/file.css', null)->once(); + File::shouldReceive('symbolizePath')->with('file.css', null)->andReturnNull(); + File::shouldReceive('symbolizePath')->with('/assets/file.css')->andReturn('/assets/file.css'); + File::shouldReceive('isFile')->with('/assets/file.css')->andReturn(true); + $assetMaker = new class + { + use AssetMaker; + }; + $assetMaker->assetPath = ['/assets']; + + $assetMaker->addCss('file.css'); +}); + +it('adds RSS asset', function() { + Assets::shouldReceive('addRss')->with('/assets/feed.rss', [])->once(); + File::shouldReceive('symbolizePath')->with('feed.rss', null)->andReturnNull(); + File::shouldReceive('symbolizePath')->with('/assets/feed.rss')->andReturn('/assets/feed.rss'); + File::shouldReceive('isFile')->with('/assets/feed.rss')->andReturn(true); + $assetMaker = new class + { + use AssetMaker; + }; + $assetMaker->assetPath = ['/assets']; + + $assetMaker->addRss('feed.rss'); +}); + +it('adds meta asset', function() { + Assets::shouldReceive('addMeta')->with(['name' => 'value'])->once(); + $assetMaker = new class + { + use AssetMaker; + }; + + $assetMaker->addMeta(['name' => 'value']); +}); diff --git a/tests/src/System/Traits/CombinesAssetsTest.php b/tests/src/System/Traits/CombinesAssetsTest.php new file mode 100644 index 00000000..c555e090 --- /dev/null +++ b/tests/src/System/Traits/CombinesAssetsTest.php @@ -0,0 +1,88 @@ +combine('css', $assets); + + expect($result)->toContain('/_assets/'); +}); + +it('combines assets to file', function() { + $combinesAssetsObject = new Assets(); + + $assets = ['igniter.tests::/scss/style.scss']; + $destination = base_path('/style.css'); + $combinesAssetsObject->combineToFile($assets, $destination); + + expect(File::exists($destination))->toBeTrue(); + File::delete($destination); +}); + +it('throws exception when cache key not found', function() { + $combinesAssetsObject = new Assets(); + + expect(fn() => $combinesAssetsObject->combineGetContents('invalid_cache_key')) + ->toThrow(SystemException::class, sprintf(lang('igniter::system.not_found.combiner'), 'invalid_cache_key')); +}); + +it('combines assets and returns correct contents', function() { + Cache::put('ti.combiner.assets_cache', base64_encode(serialize([ + 'eTag' => 'eTag', + 'type' => 'css', + 'lastMod' => time(), + 'files' => ['igniter.tests::/scss/style.scss'], + ]))); + + $combinesAssetsObject = new Assets(); + $result = $combinesAssetsObject->combineGetContents('assets_cache'); + + expect($result->getContent())->toContain('body {'); +}); + +it('builds bundles and returns notes', function() { + $theme = resolve(ThemeManager::class)->findTheme('igniter-orange'); + + $result = (new Assets())->buildBundles($theme); + + expect($result)->toContain('app.scss', ' -> /vendor/igniter-orange/css/app.css'); +}); + +it('flashes error when build bundles fails', function() { + Event::listen(AssetsBeforePrepareCombinerEvent::class, function($event) { + throw new \Exception('Error'); + }); + + $theme = resolve(ThemeManager::class)->findTheme('igniter-orange'); + + $combinesAssetsObject = new Assets(); + $combinesAssetsObject->buildBundles($theme); + $combinesAssetsObject->resetFilters('css'); + $combinesAssetsObject->resetFilters(); + + expect(flash()->messages()->first())->message->toBe('Building assets bundle error: Error'); +}); + +it('registers and retrieves bundles correctly', function() { + $combinesAssetsObject = new Assets(); + + $combinesAssetsObject->registerBundle('js', ['igniter.tests::/js/script.js']); + $combinesAssetsObject->registerBundle('css', ['igniter.tests::/scss/style.scss']); + $cssFilters = $combinesAssetsObject->getBundles('css'); + $jsFilters = $combinesAssetsObject->getBundles('js'); + + expect($combinesAssetsObject->getBundles())->toHaveCount(2) + ->and($jsFilters)->toHaveKey('igniter.tests::/js/script.min.js') + ->and($cssFilters)->toHaveKey('igniter.tests::/scss/../css/style.css'); +}); diff --git a/tests/src/System/Traits/ConfigMakerTest.php b/tests/src/System/Traits/ConfigMakerTest.php new file mode 100644 index 00000000..8f6400b5 --- /dev/null +++ b/tests/src/System/Traits/ConfigMakerTest.php @@ -0,0 +1,124 @@ +loadConfig(['key' => 'value']); + expect($config)->toBe(['key' => 'value']); +}); + +it('loads config from file', function() { + File::shouldReceive('symbolizePath')->with('config.php')->andReturn('config.php'); + File::shouldReceive('symbolizePath')->with('/path/to')->andReturn('/path/to'); + File::shouldReceive('isFile')->with('/path/to/config.php')->andReturn(true); + File::shouldReceive('getRequire')->with('/path/to/config.php')->andReturn(['key' => 'value']); + File::shouldReceive('isLocalPath')->andReturnFalse(); + $configMaker = new class + { + use ConfigMaker; + }; + $configMaker->configPath = ['/path/to']; + + expect($configMaker->loadConfig('config'))->toBe(['key' => 'value']); +}); + +it('throws exception if config file not found', function() { + File::shouldReceive('symbolizePath')->with('config.php')->andReturn('config.php'); + File::shouldReceive('symbolizePath')->with('/path/to')->andReturn('/path/to'); + File::shouldReceive('isFile')->with('/path/to/config.php')->andReturn(false); + File::shouldReceive('isFile')->with('config.php')->andReturn(false); + File::shouldReceive('isLocalPath')->andReturnFalse(); + $configMaker = new class + { + use ConfigMaker; + }; + + expect(fn() => $configMaker->loadConfig('config'))->toThrow(SystemException::class); +}); + +it('throws exception if required config key is missing', function() { + $configMaker = new class + { + use ConfigMaker; + }; + + expect(fn() => $configMaker->loadConfig(['key' => 'value'], ['missingKey']))->toThrow(SystemException::class); +}); + +it('merges two config arrays', function() { + $configMaker = new class + { + use ConfigMaker; + }; + + $mergedConfig = $configMaker->mergeConfig(['key1' => 'value1'], ['key2' => 'value2']); + expect($mergedConfig)->toBe(['key1' => 'value1', 'key2' => 'value2']); +}); + +it('returns local path is file is local', function() { + File::shouldReceive('isLocalPath')->andReturnTrue(); + File::shouldReceive('symbolizePath')->with('config.php')->andReturn('config.php'); + $configMaker = new class + { + use ConfigMaker; + }; + + expect($configMaker->getConfigPath('config.php'))->toBe('config.php'); +}); + +it('returns full path if file name starts with ~', function() { + File::shouldReceive('isLocalPath')->andReturnFalse(); + File::shouldReceive('symbolizePath')->with('~/config.php')->andReturn('/full/path/config.php'); + $configMaker = new class + { + use ConfigMaker; + }; + + expect($configMaker->getConfigPath('~/config.php'))->toBe('/full/path/config.php'); +}); + +it('returns file path from config path array', function() { + File::shouldReceive('isLocalPath')->andReturnFalse(); + File::shouldReceive('symbolizePath')->with('config.php')->andReturn('config.php'); + File::shouldReceive('symbolizePath')->with('/path/to')->andReturn('/path/to'); + File::shouldReceive('isFile')->with('/path/to/config.php')->andReturn(true); + $configMaker = new class + { + use ConfigMaker; + }; + $configMaker->configPath = ['/path/to']; + + expect($configMaker->getConfigPath('config.php'))->toBe('/path/to/config.php'); +}); + +it('returns original file name if file not found in config path', function() { + File::shouldReceive('isLocalPath')->andReturnFalse(); + File::shouldReceive('symbolizePath')->with('config.php')->andReturn('config.php'); + File::shouldReceive('symbolizePath')->with('/path/to')->andReturn('/path/to'); + File::shouldReceive('isFile')->with('/path/to/config.php')->andReturn(false); + $configMaker = new class + { + use ConfigMaker; + }; + $configMaker->configPath = ['/path/to']; + + expect($configMaker->getConfigPath('config.php'))->toBe('config.php'); +}); + +it('makes config from object correctly', function() { + $configMaker = new class + { + use ConfigMaker; + }; + + expect($configMaker->makeConfig((object)['key' => 'value']))->toBe(['key' => 'value']); +}); diff --git a/tests/src/System/Traits/ManagesUpdatesTest.php b/tests/src/System/Traits/ManagesUpdatesTest.php new file mode 100644 index 00000000..ab403f81 --- /dev/null +++ b/tests/src/System/Traits/ManagesUpdatesTest.php @@ -0,0 +1,398 @@ +instance(UpdateManager::class, $updateManager); + $updateManager->shouldReceive('searchItems')->with('extension', 'igniter.api')->andReturn([ + [ + 'name' => 'Igniter.Api', + 'code' => 'igniter.api', + 'description' => 'description', + ], + ]); + + actingAsSuperUser() + ->get(route('igniter.system.extensions', ['slug' => 'search']) + .'?'.http_build_query(['filter' => ['search' => 'igniter.api']])) + ->assertOk() + ->assertSee('Igniter.Api'); +}); + +it('returns error when search fails', function() { + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + $updateManager->shouldReceive('searchItems')->andThrow(new \Exception('Search failed')); + + actingAsSuperUser() + ->get(route('igniter.system.extensions', ['slug' => 'search']) + .'?'.http_build_query(['filter' => ['search' => 'igniter.api']])) + ->assertOk() + ->assertSee('Search failed'); +}); + +it('throws exception when no selected recommended items', function() { + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onApplyRecommended', + ]) + ->assertStatus(406) + ->assertSee(lang('igniter::system.updates.alert_no_items')); +}); + +it('applies recommended extensions correctly', function() { + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + $updateManager->shouldReceive('requestApplyItems')->andReturn(collect([ + 'data' => [ + ['code' => 'igniter.test'], + ['code' => 'igniter.anothertest'], + ], + ])); + + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [ + 'install_items' => ['igniter.test', 'igniter.anothertest'], + 'items' => [ + [ + 'name' => 'igniter.test', + 'type' => 'extension', + 'ver' => '1.0.0', + 'action' => 'install', + ], + [ + 'name' => 'igniter.anothertest', + 'type' => 'theme', + 'ver' => '1.0.0', + 'action' => 'install', + ], + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onApplyRecommended', + ]) + ->assertStatus(200) + ->assertJson([ + 'steps' => [ + 'check' => [ + 'meta' => [ + ['code' => 'igniter.test'], + ['code' => 'igniter.anothertest'], + ], + 'process' => 'check', + 'progress' => 'Performing pre installation checks...', + ], + 'install' => [ + 'meta' => [ + ['code' => 'igniter.test'], + ['code' => 'igniter.anothertest'], + ], + 'process' => 'install', + 'progress' => 'Updating composer requirements...', + ], + 'complete' => [ + 'meta' => [ + ['code' => 'igniter.test'], + ['code' => 'igniter.anothertest'], + ], + 'process' => 'complete', + 'progress' => 'Finishing installation...', + ], + ]]); +}); + +it('returns process steps when install items are applied', function() { + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + $updateManager->shouldReceive('requestApplyItems')->andReturn(collect([ + 'data' => [ + ['code' => 'igniter.test'], + ['code' => 'igniter.anothertest'], + ], + ])); + + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [ + 'items' => [ + [ + 'name' => 'igniter.test', + 'type' => 'extension', + 'ver' => '1.0.0', + 'action' => 'install', + ], + [ + 'name' => 'igniter.anothertest', + 'type' => 'theme', + 'ver' => '1.0.0', + 'action' => 'install', + ], + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onApplyItems', + ]) + ->assertStatus(200) + ->assertJson([ + 'steps' => [ + 'check' => [ + 'meta' => [ + ['code' => 'igniter.test'], + ['code' => 'igniter.anothertest'], + ], + 'process' => 'check', + 'progress' => 'Performing pre installation checks...', + ], + 'install' => [ + 'meta' => [ + ['code' => 'igniter.test'], + ['code' => 'igniter.anothertest'], + ], + 'process' => 'install', + 'progress' => 'Updating composer requirements...', + ], + 'complete' => [ + 'meta' => [ + ['code' => 'igniter.test'], + ['code' => 'igniter.anothertest'], + ], + 'process' => 'complete', + 'progress' => 'Finishing installation...', + ], + ]]); +}); + +it('throws exception when no selected items to install', function() { + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onApplyItems', + ]) + ->assertStatus(406) + ->assertSee(lang('igniter::system.updates.alert_no_items')); +}); + +it('throws exception when no items to install', function() { + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + $updateManager->shouldReceive('requestApplyItems')->andReturn(collect()); + + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [ + 'items' => [ + [ + 'name' => 'igniter.test', + 'type' => 'extension', + 'ver' => '1.0.0', + 'action' => 'install', + ], + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onApplyItems', + ]) + ->assertStatus(406) + ->assertSee(lang('igniter::system.updates.alert_no_items')); +}); + +it('returns process steps when update items are applied', function() { + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + $updateManager->shouldReceive('requestUpdateList')->andReturn([ + 'items' => collect([ + ['code' => 'igniter.test'], + ['code' => 'igniter.anothertest'], + ]), + ]); + + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onApplyUpdate', + ]) + ->assertStatus(200) + ->assertJson([ + 'steps' => [ + 'check' => [ + 'meta' => [ + ['code' => 'igniter.test'], + ['code' => 'igniter.anothertest'], + ], + 'process' => 'check', + 'progress' => 'Performing pre installation checks...', + ], + 'install' => [ + 'meta' => [ + ['code' => 'igniter.test'], + ['code' => 'igniter.anothertest'], + ], + 'process' => 'install', + 'progress' => 'Updating composer requirements...', + ], + 'complete' => [ + 'meta' => [ + ['code' => 'igniter.test'], + ['code' => 'igniter.anothertest'], + ], + 'process' => 'complete', + 'progress' => 'Finishing installation...', + ], + ]]); +}); + +it('throws exception when no items to update', function() { + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + $updateManager->shouldReceive('requestUpdateList')->andReturn([ + 'items' => collect(), + ]); + + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onApplyUpdate', + ]) + ->assertStatus(406) + ->assertSee(lang('igniter::system.updates.alert_item_to_update')); +}); + +it('redirects after checking updates', function() { + $updateManager = mock(UpdateManager::class); + $updateManager->shouldReceive('requestUpdateList')->with(true); + app()->instance(UpdateManager::class, $updateManager); + + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onCheckUpdates', + ]) + ->assertStatus(200) + ->assertSee('X_IGNITER_REDIRECT'); +}); + +it('throws exception when no item code to ignore', function() { + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onIgnoreUpdate', + ]) + ->assertStatus(406) + ->assertSee(lang('igniter::system.updates.alert_item_to_ignore')); +}); + +it('redirects after ignoring update', function() { + $updateManager = mock(UpdateManager::class); + $updateManager->shouldReceive('markedAsIgnored')->with('item_code', true); + app()->instance(UpdateManager::class, $updateManager); + + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [ + 'code' => 'item_code', + 'remove' => 1, + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onIgnoreUpdate', + ]) + ->assertStatus(200) + ->assertSee('X_IGNITER_REDIRECT'); +}); + +it('throws exception when no carte key is specified', function() { + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onApplyCarte', + ]) + ->assertStatus(406) + ->assertSee(lang('igniter::system.updates.alert_no_carte_key')); +}); + +it('redirects after applying carte key', function() { + $updateManager = mock(UpdateManager::class); + $updateManager->shouldReceive('applySiteDetail')->with('carte_key'); + app()->instance(UpdateManager::class, $updateManager); + + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [ + 'carte_key' => 'carte_key', + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onApplyCarte', + ]) + ->assertStatus(200) + ->assertSee('X_IGNITER_REDIRECT'); +}); + +it('returns process steps when processing items', function($process) { + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + $updateManager->shouldReceive('getLogs')->andReturn([]); + $updateManager->shouldReceive('preInstall'); + $updateManager->shouldReceive('install'); + $updateManager->shouldReceive('completeInstall'); + + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [ + 'process' => $process, + 'meta' => [ + [ + 'code' => 'igniter.test', + 'name' => 'Test Extension', + 'package' => 'igniter/test', + 'author' => 'Igniter Labs', + 'type' => 'extension', + 'version' => '1.0.0', + 'hash' => 'hash', + ], + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onProcessItems', + ]) + ->assertStatus(200) + ->assertJson([ + 'success' => true, + ]); +})->with([ + ['check'], + ['install'], + ['complete'], +]); + +it('throws exception when composer install fails', function() { + $updateManager = mock(UpdateManager::class); + app()->instance(UpdateManager::class, $updateManager); + $updateManager->shouldReceive('getLogs')->andReturn([]); + $updateManager->shouldReceive('install')->andThrow(new ComposerException(new Exception('Composer install failed'), new BufferIO())); + + actingAsSuperUser() + ->post(route('igniter.system.extensions'), [ + 'process' => 'install', + 'meta' => [ + [ + 'code' => 'igniter.test', + 'name' => 'Test Extension', + 'package' => 'igniter/test', + 'author' => 'Igniter Labs', + 'type' => 'extension', + 'version' => '1.0.0', + 'hash' => 'hash', + ], + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onProcessItems', + ]) + ->assertStatus(200) + ->assertJson([ + 'success' => false, + 'message' => "Error updating composer requirements: Composer install failed
\nOutput: " + ."\n\nTroubleshoot\n\n", + ]); +}); diff --git a/tests/src/System/Traits/PropertyContainerTest.php b/tests/src/System/Traits/PropertyContainerTest.php new file mode 100644 index 00000000..470788ea --- /dev/null +++ b/tests/src/System/Traits/PropertyContainerTest.php @@ -0,0 +1,95 @@ + ['default' => 'default1'], + 'property2' => ['default' => 'default2'], + ]; + } + }; + + $result = $propertyContainer->validateProperties(['property1' => 'value1']); + expect($result)->toBe(['property1' => 'value1', 'property2' => 'default2']); +}); + +it('sets multiple properties', function() { + $propertyContainer = new class + { + use PropertyContainer; + + public function defineProperties(): array + { + return [ + 'property1' => ['default' => 'default1'], + 'property2' => ['default' => 'default2'], + ]; + } + }; + + $propertyContainer->setProperties(['property1' => 'value1']); + expect($propertyContainer->getProperties())->toBe(['property1' => 'value1', 'property2' => 'default2']); +}); + +it('merges multiple properties', function() { + $propertyContainer = new class + { + use PropertyContainer; + + public function defineProperties(): array + { + return [ + 'property1' => ['default' => 'default1'], + 'property2' => ['default' => 'default2'], + ]; + } + }; + + $propertyContainer->mergeProperties(['property2' => 'value2']); + expect($propertyContainer->getProperties())->toBe(['property1' => 'default1', 'property2' => 'value2']); +}); + +it('sets a single property value', function() { + $propertyContainer = new class + { + use PropertyContainer; + }; + + $propertyContainer->setProperty('property1', 'value1'); + + expect($propertyContainer->defineProperties())->toBeArray() + ->and($propertyContainer->getProperties())->toBe(['property1' => 'value1']); +}); + +it('returns a defined property value or default', function() { + $propertyContainer = new class + { + use PropertyContainer; + }; + + $propertyContainer->setProperty('property1', 'value1'); + $result = $propertyContainer->property('property1', 'default'); + expect($result)->toBe('value1'); + + $result = $propertyContainer->property('property2', 'default'); + expect($result)->toBe('default'); +}); + +it('returns empty array for property options', function() { + $propertyContainer = new class + { + use PropertyContainer; + }; + + $result = $propertyContainer::getPropertyOptions('form', 'field'); + expect($result)->toBe([]); +}); diff --git a/tests/src/System/Traits/SessionMakerTest.php b/tests/src/System/Traits/SessionMakerTest.php new file mode 100644 index 00000000..f132e35b --- /dev/null +++ b/tests/src/System/Traits/SessionMakerTest.php @@ -0,0 +1,105 @@ +put('class_id.key', 'value'); + $sessionMaker = new class + { + use SessionMaker; + + protected $sessionKey = 'class_id'; + }; + + $result = $sessionMaker->getSession('key'); + expect($result)->toBe('value'); +}); + +it('retrieves default value when key not in session', function() { + $sessionMaker = new class + { + use SessionMaker; + }; + + $result = $sessionMaker->getSession('nonexistent_key', 'default_value'); + expect($result)->toBe('default_value'); +}); + +it('saves key value pair in session', function() { + $sessionMaker = new class + { + use SessionMaker; + + protected $sessionKey = 'class_id'; + }; + + $sessionMaker->putSession('key', 'value'); + expect(session()->get('class_id.key'))->toBe('value'); +}); + +it('checks if session has key', function() { + session()->put('class_id.key', 'value'); + $sessionMaker = new class + { + use SessionMaker; + + protected $sessionKey = 'class_id'; + }; + + $result = $sessionMaker->hasSession('key'); + expect($result)->toBeTrue(); +}); + +it('flashes key value pair in session', function() { + $sessionMaker = new class + { + use SessionMaker; + + protected $sessionKey = 'class_id'; + }; + + $sessionMaker->flashSession('key', 'value'); + expect(session()->get('class_id.key'))->toBe('value'); +}); + +it('forgets key from session', function() { + session()->put('class_id.key', 'value'); + $sessionMaker = new class + { + use SessionMaker; + + protected $sessionKey = 'class_id'; + }; + + $sessionMaker->forgetSession('key'); + expect(session()->has('class_id.key'))->toBeFalse(); +}); + +it('resets session', function() { + session()->put('class_id.key1', 'value1'); + session()->put('class_id.key2', 'value2'); + $sessionMaker = new class + { + use SessionMaker; + + protected $sessionKey = 'class_id'; + }; + + $sessionMaker->resetSession(); + expect(session()->has('class_id.key1'))->toBeFalse() + ->and(session()->has('class_id.key2'))->toBeFalse(); +}); + +it('sets custom session key', function() { + $sessionMaker = new class + { + use SessionMaker; + }; + + $sessionMaker->setSessionKey('custom_key'); + $sessionMaker->putSession('key', 'value'); + + expect(session()->get('custom_key.key'))->toBe('value'); +}); diff --git a/tests/src/System/Traits/ViewMakerTest.php b/tests/src/System/Traits/ViewMakerTest.php new file mode 100644 index 00000000..94de6eca --- /dev/null +++ b/tests/src/System/Traits/ViewMakerTest.php @@ -0,0 +1,194 @@ +getViewPath('tests.admin::test', null); + expect($result)->toEndWith('/views/test.blade.php'); +}); + +it('returns correct view path when view is index', function() { + $viewMaker = new class + { + use ViewMaker; + }; + + $result = $viewMaker->getViewPath('tests.admin::testcontroller', null); + expect($result)->toEndWith('/views/testcontroller/index.blade.php'); +}); + +it('returns correct view path when unable to guess view name', function() { + View::shouldReceive('exists')->with('test')->andReturn(false, false, true); + View::shouldReceive('exists')->with('test.index')->andReturn(false); + View::shouldReceive('getFinder->find')->with('test')->andReturn('/path/to/test.blade.php'); + $viewMaker = new class + { + use ViewMaker; + }; + + $result = $viewMaker->getViewPath('test', null); + expect($result)->toEndWith('/path/to/test.blade.php'); +}); + +it('returns correct view name when view exists', function() { + $viewMaker = new class + { + use ViewMaker; + }; + + $result = $viewMaker->getViewName('tests.admin::test'); + expect($result)->toBe('tests.admin::test'); +}); + +it('returns correct view name when view is index', function() { + $viewMaker = new class + { + use ViewMaker; + }; + + $result = $viewMaker->getViewName('tests.admin::testcontroller', null); + expect($result)->toBe('tests.admin::testcontroller.index'); +}); + +it('returns correct view name when unable to guess view name', function() { + View::shouldReceive('exists')->with('test')->andReturn(false, false, true); + View::shouldReceive('exists')->with('test.index')->andReturn(false); + View::shouldReceive('getFinder->find')->with('test')->andReturn('/path/to/test.blade.php'); + $viewMaker = new class + { + use ViewMaker; + }; + + expect($viewMaker->getViewName('test', null))->toBe('test'); +}); + +it('throws exception when partial view not found', function() { + View::shouldReceive('exists')->with('_partials.partial.name')->andReturn(false); + View::shouldReceive('exists')->with('_partials.partial.name.index')->andReturn(false); + View::shouldReceive('exists')->with('partial.name')->andReturn(false); + + $viewMaker = new class + { + use ViewMaker; + }; + + expect(fn() => $viewMaker->makePartial('partial.name'))->toThrow(SystemException::class); +}); + +it('returns empty string when partial view not found and throw exception is disabled', function() { + $viewMaker = new class + { + use ViewMaker; + }; + + expect($viewMaker->makePartial('partial.name', [], false))->toBe(''); +}); + +it('returns empty string when layout name is empty', function() { + $viewMaker = new class + { + use ViewMaker; + }; + + expect($viewMaker->makeLayout(''))->toBe(''); +}); + +it('renders view and layout correctly', function() { + $viewMaker = new class + { + use ViewMaker; + }; + $viewMaker->layout = 'tests.admin::layout'; + + $result = $viewMaker->makeView('tests.admin::test', ['key' => 'value']); + expect($result)->toContain('This is a test view content'); +}); + +it('renders view with no layout correctly', function() { + $viewMaker = new class + { + use ViewMaker; + }; + $viewMaker->layout = ''; + + $result = $viewMaker->makeView('tests.admin::test', ['key' => 'value']); + expect($result)->toContain('This is a test view content'); +}); + +it('renders view content correctly', function() { + $viewMaker = new class + { + use ViewMaker; + }; + + $result = $viewMaker->makeViewContent('test', [ + 'key' => 'value', + 'view' => view('tests.admin::_partials.test-partial'), + ]); + expect($result)->toBeString(); +}); + +it('renders partial content correctly', function() { + $viewMaker = new class + { + use ViewMaker; + }; + $viewMaker->controller = new class extends AdminController + { + public array $vars = ['key' => 'value']; + }; + + $result = $viewMaker->makePartial('tests.admin::test-partial', ['key' => 'value']); + expect($result)->toContain('This is a test partial content'); +}); + +it('returns empty string when file path is invalid', function() { + $viewMaker = new class + { + use ViewMaker; + }; + + $result = $viewMaker->makeFileContent('index.php'); + expect($result)->toBe(''); +}); + +it('throws exception when evaluating view contents', function() { + $viewMaker = new class + { + use ViewMaker; + }; + + expect(fn() => $viewMaker->makeFileContent(__DIR__.'/../../../resources/views/testcontroller/view-with-exception.blade.php')) + ->toThrow(Exception::class, 'This is a test exception'); +}); + +it('throws throwable when evaluating view contents', function() { + $viewMaker = new class + { + use ViewMaker; + }; + + expect(fn() => $viewMaker->makeFileContent(__DIR__.'/../../../resources/views/testcontroller/view-with-throwable.blade.php')) + ->toThrow(Exception::class, 'This is a test error'); +}); + +it('compiles file content if expired', function() { + $viewMaker = new class + { + use ViewMaker; + }; + + $result = $viewMaker->compileFileContent(__DIR__.'/../../../resources/views/test.blade.php'); + expect($result)->toContain('/storage/framework/views'); +}); diff --git a/tests/src/TestCase.php b/tests/src/TestCase.php index 7c02bc1c..aade4cfb 100644 --- a/tests/src/TestCase.php +++ b/tests/src/TestCase.php @@ -4,7 +4,11 @@ use Igniter\Flame\Support\Facades\Igniter; use Igniter\Main\Classes\ThemeManager; +use Igniter\System\Classes\ComponentManager; use Igniter\System\Classes\PackageManifest; +use Igniter\Tests\System\Fixtures\TestComponent; +use Igniter\Tests\System\Fixtures\TestComponentWithLifecycle; +use Igniter\Tests\System\Fixtures\TestLivewireComponent; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\View; use Livewire\LivewireServiceProvider; @@ -29,10 +33,17 @@ protected function defineEnvironment($app) $app['config']->set('view.paths', $viewPaths); Igniter::loadControllersFrom(__DIR__.'/Fixtures/Controllers', 'Igniter\\Tests\\Fixtures\\Controllers'); + Igniter::loadResourcesFrom(__DIR__.'/../resources', 'igniter.tests'); View::addNamespace('tests.admin', __DIR__.'/../resources/views'); ThemeManager::addDirectory(__DIR__.'/../resources/themes'); $app['config']->set('igniter-system.defaultTheme', 'tests-theme'); + + resolve(ComponentManager::class)->registerCallback(function(ComponentManager $manager) { + $manager->registerComponent(TestComponent::class, TestComponent::componentMeta()); + $manager->registerComponent(TestComponentWithLifecycle::class, TestComponentWithLifecycle::componentMeta()); + $manager->registerComponent(TestLivewireComponent::class, TestLivewireComponent::componentMeta()); + }); } protected function defineDatabaseMigrations()