diff --git a/src/Cms/Container/CmsServiceProvider.php b/src/Cms/Container/CmsServiceProvider.php index e13402f..5ddb19d 100644 --- a/src/Cms/Container/CmsServiceProvider.php +++ b/src/Cms/Container/CmsServiceProvider.php @@ -26,7 +26,7 @@ use Neuron\Cms\Services\User\Deleter; use Neuron\Cms\Auth\PasswordHasher; use Neuron\Cms\Auth\SessionManager; -use Neuron\Cms\Auth\ResendVerificationThrottle; +use Neuron\Cms\Services\Security\ResendVerificationThrottle; use Neuron\Cms\Services\Content\EditorJsRenderer; use Neuron\Cms\Services\Content\ShortcodeParser; use Neuron\Cms\Services\Widget\WidgetRenderer; diff --git a/src/Cms/Container/Container.php b/src/Cms/Container/Container.php index f94d37e..8ad7459 100644 --- a/src/Cms/Container/Container.php +++ b/src/Cms/Container/Container.php @@ -4,6 +4,12 @@ use DI\ContainerBuilder; use Neuron\Cms\Services\SlugGenerator; +use Neuron\Cms\Services\Content\EditorJsRenderer; +use Neuron\Cms\Services\Media\CloudinaryUploader; +use Neuron\Cms\Services\Media\MediaValidator; +use Neuron\Cms\Services\Security\ResendVerificationThrottle; +use Neuron\Routing\IIpResolver; +use Neuron\Routing\DefaultIpResolver; use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Auth\PasswordHasher; use Neuron\Cms\Services\Auth\CsrfToken; @@ -230,6 +236,17 @@ public static function build( SettingManager $settings ): IContainer // Interface Bindings - EventCategory Services IEventCategoryCreator::class => \DI\autowire( EventCategoryCreator::class ), IEventCategoryUpdater::class => \DI\autowire( EventCategoryUpdater::class ), + + // Content Services + EditorJsRenderer::class => \DI\autowire( EditorJsRenderer::class ), + + // Media Services + CloudinaryUploader::class => \DI\autowire( CloudinaryUploader::class ), + MediaValidator::class => \DI\autowire( MediaValidator::class ), + + // Security Services + ResendVerificationThrottle::class => \DI\autowire( ResendVerificationThrottle::class ), + IIpResolver::class => \DI\autowire( DefaultIpResolver::class ), ]); $psr11Container = $builder->build(); diff --git a/src/Cms/Controllers/Admin/Categories.php b/src/Cms/Controllers/Admin/Categories.php index f08a005..e11ef6d 100644 --- a/src/Cms/Controllers/Admin/Categories.php +++ b/src/Cms/Controllers/Admin/Categories.php @@ -2,12 +2,14 @@ namespace Neuron\Cms\Controllers\Admin; +use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Enums\FlashMessageType; use Neuron\Cms\Controllers\Content; use Neuron\Cms\Repositories\ICategoryRepository; use Neuron\Cms\Services\Category\ICategoryCreator; use Neuron\Cms\Services\Category\ICategoryUpdater; use Neuron\Core\Exceptions\NotFound; +use Neuron\Data\Settings\SettingManager; use Neuron\Mvc\Application; use Neuron\Mvc\Requests\Request; use Neuron\Mvc\Responses\HttpResponseStatus; @@ -34,21 +36,37 @@ class Categories extends Content * @param ICategoryRepository|null $categoryRepository * @param ICategoryCreator|null $categoryCreator * @param ICategoryUpdater|null $categoryUpdater + * @param SettingManager|null $settings + * @param SessionManager|null $sessionManager */ public function __construct( ?Application $app = null, ?ICategoryRepository $categoryRepository = null, ?ICategoryCreator $categoryCreator = null, - ?ICategoryUpdater $categoryUpdater = null + ?ICategoryUpdater $categoryUpdater = null, + ?SettingManager $settings = null, + ?SessionManager $sessionManager = null ) { - parent::__construct( $app ); + parent::__construct( $app, $settings, $sessionManager ); - // Use dependency injection when available (container provides dependencies) - // Otherwise resolve from container (fallback for compatibility) - $this->_categoryRepository = $categoryRepository ?? $app?->getContainer()?->get( ICategoryRepository::class ); - $this->_categoryCreator = $categoryCreator ?? $app?->getContainer()?->get( ICategoryCreator::class ); - $this->_categoryUpdater = $categoryUpdater ?? $app?->getContainer()?->get( ICategoryUpdater::class ); + if( $categoryRepository === null ) + { + throw new \InvalidArgumentException( 'ICategoryRepository must be injected' ); + } + $this->_categoryRepository = $categoryRepository; + + if( $categoryCreator === null ) + { + throw new \InvalidArgumentException( 'ICategoryCreator must be injected' ); + } + $this->_categoryCreator = $categoryCreator; + + if( $categoryUpdater === null ) + { + throw new \InvalidArgumentException( 'ICategoryUpdater must be injected' ); + } + $this->_categoryUpdater = $categoryUpdater; } /** diff --git a/src/Cms/Controllers/Admin/Dashboard.php b/src/Cms/Controllers/Admin/Dashboard.php index 2ce64ec..9aad030 100644 --- a/src/Cms/Controllers/Admin/Dashboard.php +++ b/src/Cms/Controllers/Admin/Dashboard.php @@ -2,8 +2,10 @@ namespace Neuron\Cms\Controllers\Admin; +use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Controllers\Content; use Neuron\Core\Exceptions\NotFound; +use Neuron\Data\Settings\SettingManager; use Neuron\Mvc\Application; use Neuron\Mvc\Requests\Request; use Neuron\Mvc\Responses\HttpResponseStatus; @@ -20,12 +22,18 @@ class Dashboard extends Content { /** * @param Application|null $app + * @param SettingManager|null $settings + * @param SessionManager|null $sessionManager * @return void * @throws \Exception */ - public function __construct( ?Application $app = null ) + public function __construct( + ?Application $app = null, + ?SettingManager $settings = null, + ?SessionManager $sessionManager = null + ) { - parent::__construct( $app ); + parent::__construct( $app, $settings, $sessionManager ); } diff --git a/src/Cms/Controllers/Admin/EventCategories.php b/src/Cms/Controllers/Admin/EventCategories.php index a5a5f55..11a9d9f 100644 --- a/src/Cms/Controllers/Admin/EventCategories.php +++ b/src/Cms/Controllers/Admin/EventCategories.php @@ -2,11 +2,13 @@ namespace Neuron\Cms\Controllers\Admin; +use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Enums\FlashMessageType; use Neuron\Cms\Controllers\Content; use Neuron\Cms\Repositories\IEventCategoryRepository; use Neuron\Cms\Services\EventCategory\IEventCategoryCreator; use Neuron\Cms\Services\EventCategory\IEventCategoryUpdater; +use Neuron\Data\Settings\SettingManager; use Neuron\Log\Log; use Neuron\Mvc\Application; use Neuron\Mvc\Requests\Request; @@ -34,22 +36,38 @@ class EventCategories extends Content * @param IEventCategoryRepository|null $repository * @param IEventCategoryCreator|null $creator * @param IEventCategoryUpdater|null $updater + * @param SettingManager|null $settings + * @param SessionManager|null $sessionManager * @throws \Exception */ public function __construct( ?Application $app = null, ?IEventCategoryRepository $repository = null, ?IEventCategoryCreator $creator = null, - ?IEventCategoryUpdater $updater = null + ?IEventCategoryUpdater $updater = null, + ?SettingManager $settings = null, + ?SessionManager $sessionManager = null ) { - parent::__construct( $app ); + parent::__construct( $app, $settings, $sessionManager ); - // Use dependency injection when available (container provides dependencies) - // Otherwise resolve from container (fallback for compatibility) - $this->_repository = $repository ?? $app?->getContainer()?->get( IEventCategoryRepository::class ); - $this->_creator = $creator ?? $app?->getContainer()?->get( IEventCategoryCreator::class ); - $this->_updater = $updater ?? $app?->getContainer()?->get( IEventCategoryUpdater::class ); + if( $repository === null ) + { + throw new \InvalidArgumentException( 'IEventCategoryRepository must be injected' ); + } + $this->_repository = $repository; + + if( $creator === null ) + { + throw new \InvalidArgumentException( 'IEventCategoryCreator must be injected' ); + } + $this->_creator = $creator; + + if( $updater === null ) + { + throw new \InvalidArgumentException( 'IEventCategoryUpdater must be injected' ); + } + $this->_updater = $updater; } /** diff --git a/src/Cms/Controllers/Admin/Events.php b/src/Cms/Controllers/Admin/Events.php index 25e0813..c8d5513 100644 --- a/src/Cms/Controllers/Admin/Events.php +++ b/src/Cms/Controllers/Admin/Events.php @@ -2,6 +2,7 @@ namespace Neuron\Cms\Controllers\Admin; +use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Enums\FlashMessageType; use Neuron\Cms\Controllers\Content; use Neuron\Cms\Repositories\IEventRepository; @@ -9,6 +10,7 @@ use Neuron\Cms\Services\Event\IEventCreator; use Neuron\Cms\Services\Event\IEventUpdater; use Neuron\Cms\Services\Auth\CsrfToken; +use Neuron\Data\Settings\SettingManager; use Neuron\Log\Log; use Neuron\Mvc\Application; use Neuron\Mvc\Requests\Request; @@ -39,23 +41,44 @@ class Events extends Content * @param IEventCategoryRepository|null $categoryRepository * @param IEventCreator|null $creator * @param IEventUpdater|null $updater + * @param SettingManager|null $settings + * @param SessionManager|null $sessionManager */ public function __construct( ?Application $app = null, ?IEventRepository $eventRepository = null, ?IEventCategoryRepository $categoryRepository = null, ?IEventCreator $creator = null, - ?IEventUpdater $updater = null + ?IEventUpdater $updater = null, + ?SettingManager $settings = null, + ?SessionManager $sessionManager = null ) { - parent::__construct( $app ); - - // Use dependency injection when available (container provides dependencies) - // Otherwise resolve from container (fallback for compatibility) - $this->_eventRepository = $eventRepository ?? $app?->getContainer()?->get( IEventRepository::class ); - $this->_categoryRepository = $categoryRepository ?? $app?->getContainer()?->get( IEventCategoryRepository::class ); - $this->_creator = $creator ?? $app?->getContainer()?->get( IEventCreator::class ); - $this->_updater = $updater ?? $app?->getContainer()?->get( IEventUpdater::class ); + parent::__construct( $app, $settings, $sessionManager ); + + if( $eventRepository === null ) + { + throw new \InvalidArgumentException( 'IEventRepository must be injected' ); + } + $this->_eventRepository = $eventRepository; + + if( $categoryRepository === null ) + { + throw new \InvalidArgumentException( 'IEventCategoryRepository must be injected' ); + } + $this->_categoryRepository = $categoryRepository; + + if( $creator === null ) + { + throw new \InvalidArgumentException( 'IEventCreator must be injected' ); + } + $this->_creator = $creator; + + if( $updater === null ) + { + throw new \InvalidArgumentException( 'IEventUpdater must be injected' ); + } + $this->_updater = $updater; } /** diff --git a/src/Cms/Controllers/Admin/Media.php b/src/Cms/Controllers/Admin/Media.php index 24ed424..ef44837 100644 --- a/src/Cms/Controllers/Admin/Media.php +++ b/src/Cms/Controllers/Admin/Media.php @@ -2,10 +2,12 @@ namespace Neuron\Cms\Controllers\Admin; +use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Enums\FlashMessageType; use Neuron\Cms\Controllers\Content; use Neuron\Cms\Services\Media\CloudinaryUploader; use Neuron\Cms\Services\Media\MediaValidator; +use Neuron\Data\Settings\SettingManager; use Neuron\Log\Log; use Neuron\Mvc\Application; use Neuron\Mvc\Requests\Request; @@ -24,8 +26,8 @@ #[RouteGroup(prefix: '/admin', filters: ['auth'])] class Media extends Content { - private ?CloudinaryUploader $_uploader = null; - private ?MediaValidator $_validator = null; + private CloudinaryUploader $_uploader; + private MediaValidator $_validator; /** * Constructor @@ -33,20 +35,31 @@ class Media extends Content * @param Application|null $app * @param CloudinaryUploader|null $uploader * @param MediaValidator|null $validator + * @param SettingManager|null $settings + * @param SessionManager|null $sessionManager * @throws \Exception */ public function __construct( ?Application $app = null, ?CloudinaryUploader $uploader = null, - ?MediaValidator $validator = null + ?MediaValidator $validator = null, + ?SettingManager $settings = null, + ?SessionManager $sessionManager = null ) { - parent::__construct( $app ); + parent::__construct( $app, $settings, $sessionManager ); - // Use dependency injection when available (container provides dependencies) - // Otherwise resolve from container (fallback for compatibility) - $this->_uploader = $uploader ?? $app?->getContainer()?->get( CloudinaryUploader::class ); - $this->_validator = $validator ?? $app?->getContainer()?->get( MediaValidator::class ); + if( $uploader === null ) + { + throw new \InvalidArgumentException( 'CloudinaryUploader must be injected' ); + } + $this->_uploader = $uploader; + + if( $validator === null ) + { + throw new \InvalidArgumentException( 'MediaValidator must be injected' ); + } + $this->_validator = $validator; } /** diff --git a/src/Cms/Controllers/Admin/Pages.php b/src/Cms/Controllers/Admin/Pages.php index 4061569..2f8ff23 100644 --- a/src/Cms/Controllers/Admin/Pages.php +++ b/src/Cms/Controllers/Admin/Pages.php @@ -2,6 +2,7 @@ namespace Neuron\Cms\Controllers\Admin; +use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Controllers\Content; use Neuron\Cms\Enums\FlashMessageType; use Neuron\Cms\Models\Page; @@ -9,6 +10,7 @@ use Neuron\Cms\Services\Page\IPageCreator; use Neuron\Cms\Services\Page\IPageUpdater; use Neuron\Cms\Services\Auth\CsrfToken; +use Neuron\Data\Settings\SettingManager; use Neuron\Mvc\Application; use Neuron\Mvc\Requests\Request; use Neuron\Mvc\Responses\HttpResponseStatus; @@ -38,21 +40,37 @@ class Pages extends Content * @param IPageRepository|null $pageRepository * @param IPageCreator|null $pageCreator * @param IPageUpdater|null $pageUpdater + * @param SettingManager|null $settings + * @param SessionManager|null $sessionManager */ public function __construct( ?Application $app = null, ?IPageRepository $pageRepository = null, ?IPageCreator $pageCreator = null, - ?IPageUpdater $pageUpdater = null + ?IPageUpdater $pageUpdater = null, + ?SettingManager $settings = null, + ?SessionManager $sessionManager = null ) { - parent::__construct( $app ); + parent::__construct( $app, $settings, $sessionManager ); - // Use dependency injection when available (container provides dependencies) - // Otherwise resolve from container (fallback for compatibility) - $this->_pageRepository = $pageRepository ?? $app?->getContainer()?->get( IPageRepository::class ); - $this->_pageCreator = $pageCreator ?? $app?->getContainer()?->get( IPageCreator::class ); - $this->_pageUpdater = $pageUpdater ?? $app?->getContainer()?->get( IPageUpdater::class ); + if( $pageRepository === null ) + { + throw new \InvalidArgumentException( 'IPageRepository must be injected' ); + } + $this->_pageRepository = $pageRepository; + + if( $pageCreator === null ) + { + throw new \InvalidArgumentException( 'IPageCreator must be injected' ); + } + $this->_pageCreator = $pageCreator; + + if( $pageUpdater === null ) + { + throw new \InvalidArgumentException( 'IPageUpdater must be injected' ); + } + $this->_pageUpdater = $pageUpdater; } /** diff --git a/src/Cms/Controllers/Admin/Posts.php b/src/Cms/Controllers/Admin/Posts.php index e3709f5..87e7f78 100644 --- a/src/Cms/Controllers/Admin/Posts.php +++ b/src/Cms/Controllers/Admin/Posts.php @@ -2,6 +2,7 @@ namespace Neuron\Cms\Controllers\Admin; +use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Enums\FlashMessageType; use Neuron\Cms\Controllers\Content; use Neuron\Cms\Models\Post; @@ -13,6 +14,7 @@ use Neuron\Cms\Services\Post\IPostDeleter; use Neuron\Cms\Services\Tag\Resolver as TagResolver; use Neuron\Cms\Services\Auth\CsrfToken; +use Neuron\Data\Settings\SettingManager; use Neuron\Mvc\Application; use Neuron\Mvc\Requests\Request; use Neuron\Mvc\Responses\HttpResponseStatus; @@ -46,6 +48,8 @@ class Posts extends Content * @param IPostCreator|null $postCreator * @param IPostUpdater|null $postUpdater * @param IPostDeleter|null $postDeleter + * @param SettingManager|null $settings + * @param SessionManager|null $sessionManager */ public function __construct( ?Application $app = null, @@ -54,19 +58,48 @@ public function __construct( ?ITagRepository $tagRepository = null, ?IPostCreator $postCreator = null, ?IPostUpdater $postUpdater = null, - ?IPostDeleter $postDeleter = null + ?IPostDeleter $postDeleter = null, + ?SettingManager $settings = null, + ?SessionManager $sessionManager = null ) { - parent::__construct( $app ); - - // Use dependency injection when available (container provides dependencies) - // Otherwise resolve from container (fallback for compatibility) - $this->_postRepository = $postRepository ?? $app?->getContainer()?->get( IPostRepository::class ); - $this->_categoryRepository = $categoryRepository ?? $app?->getContainer()?->get( ICategoryRepository::class ); - $this->_tagRepository = $tagRepository ?? $app?->getContainer()?->get( ITagRepository::class ); - $this->_postCreator = $postCreator ?? $app?->getContainer()?->get( IPostCreator::class ); - $this->_postUpdater = $postUpdater ?? $app?->getContainer()?->get( IPostUpdater::class ); - $this->_postDeleter = $postDeleter ?? $app?->getContainer()?->get( IPostDeleter::class ); + parent::__construct( $app, $settings, $sessionManager ); + + if( $postRepository === null ) + { + throw new \InvalidArgumentException( 'IPostRepository must be injected' ); + } + $this->_postRepository = $postRepository; + + if( $categoryRepository === null ) + { + throw new \InvalidArgumentException( 'ICategoryRepository must be injected' ); + } + $this->_categoryRepository = $categoryRepository; + + if( $tagRepository === null ) + { + throw new \InvalidArgumentException( 'ITagRepository must be injected' ); + } + $this->_tagRepository = $tagRepository; + + if( $postCreator === null ) + { + throw new \InvalidArgumentException( 'IPostCreator must be injected' ); + } + $this->_postCreator = $postCreator; + + if( $postUpdater === null ) + { + throw new \InvalidArgumentException( 'IPostUpdater must be injected' ); + } + $this->_postUpdater = $postUpdater; + + if( $postDeleter === null ) + { + throw new \InvalidArgumentException( 'IPostDeleter must be injected' ); + } + $this->_postDeleter = $postDeleter; } /** diff --git a/src/Cms/Controllers/Admin/Profile.php b/src/Cms/Controllers/Admin/Profile.php index 78f3f8f..2a26d82 100644 --- a/src/Cms/Controllers/Admin/Profile.php +++ b/src/Cms/Controllers/Admin/Profile.php @@ -2,12 +2,14 @@ namespace Neuron\Cms\Controllers\Admin; +use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Enums\FlashMessageType; use Neuron\Cms\Controllers\Content; use Neuron\Cms\Controllers\Traits\UsesDtos; use Neuron\Cms\Repositories\IUserRepository; use Neuron\Cms\Services\User\IUserUpdater; use Neuron\Cms\Auth\PasswordHasher; +use Neuron\Data\Settings\SettingManager; use Neuron\Mvc\Application; use Neuron\Mvc\Requests\Request; use Neuron\Mvc\Responses\HttpResponseStatus; @@ -34,22 +36,38 @@ class Profile extends Content * @param IUserRepository|null $repository * @param PasswordHasher|null $hasher * @param IUserUpdater|null $userUpdater + * @param SettingManager|null $settings + * @param SessionManager|null $sessionManager * @throws \Exception */ public function __construct( ?Application $app = null, ?IUserRepository $repository = null, ?PasswordHasher $hasher = null, - ?IUserUpdater $userUpdater = null + ?IUserUpdater $userUpdater = null, + ?SettingManager $settings = null, + ?SessionManager $sessionManager = null ) { - parent::__construct( $app ); + parent::__construct( $app, $settings, $sessionManager ); - // Use dependency injection when available (container provides dependencies) - // Otherwise resolve from container (fallback for compatibility) - $this->_repository = $repository ?? $app?->getContainer()?->get( IUserRepository::class ); - $this->_hasher = $hasher ?? $app?->getContainer()?->get( PasswordHasher::class ); - $this->_userUpdater = $userUpdater ?? $app?->getContainer()?->get( IUserUpdater::class ); + if( $repository === null ) + { + throw new \InvalidArgumentException( 'IUserRepository must be injected' ); + } + $this->_repository = $repository; + + if( $hasher === null ) + { + throw new \InvalidArgumentException( 'PasswordHasher must be injected' ); + } + $this->_hasher = $hasher; + + if( $userUpdater === null ) + { + throw new \InvalidArgumentException( 'IUserUpdater must be injected' ); + } + $this->_userUpdater = $userUpdater; } /** diff --git a/src/Cms/Controllers/Admin/Tags.php b/src/Cms/Controllers/Admin/Tags.php index b971df3..b2bf356 100644 --- a/src/Cms/Controllers/Admin/Tags.php +++ b/src/Cms/Controllers/Admin/Tags.php @@ -2,11 +2,13 @@ namespace Neuron\Cms\Controllers\Admin; +use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Enums\FlashMessageType; use Neuron\Cms\Controllers\Content; use Neuron\Cms\Models\Tag; use Neuron\Cms\Repositories\ITagRepository; use Neuron\Cms\Services\SlugGenerator; +use Neuron\Data\Settings\SettingManager; use Neuron\Mvc\Application; use Neuron\Mvc\Requests\Request; use Neuron\Mvc\Responses\HttpResponseStatus; @@ -31,20 +33,31 @@ class Tags extends Content * @param Application|null $app * @param ITagRepository|null $tagRepository * @param SlugGenerator|null $slugGenerator + * @param SettingManager|null $settings + * @param SessionManager|null $sessionManager * @throws \Exception */ public function __construct( ?Application $app = null, ?ITagRepository $tagRepository = null, - ?SlugGenerator $slugGenerator = null + ?SlugGenerator $slugGenerator = null, + ?SettingManager $settings = null, + ?SessionManager $sessionManager = null ) { - parent::__construct( $app ); + parent::__construct( $app, $settings, $sessionManager ); - // Use dependency injection when available (container provides dependencies) - // Otherwise resolve from container (fallback for compatibility) - $this->_tagRepository = $tagRepository ?? $app?->getContainer()?->get( ITagRepository::class ); - $this->_slugGenerator = $slugGenerator ?? new SlugGenerator(); + if( $tagRepository === null ) + { + throw new \InvalidArgumentException( 'ITagRepository must be injected' ); + } + $this->_tagRepository = $tagRepository; + + if( $slugGenerator === null ) + { + throw new \InvalidArgumentException( 'SlugGenerator must be injected' ); + } + $this->_slugGenerator = $slugGenerator; } /** diff --git a/src/Cms/Controllers/Admin/Users.php b/src/Cms/Controllers/Admin/Users.php index ff0e0ad..b22ab34 100644 --- a/src/Cms/Controllers/Admin/Users.php +++ b/src/Cms/Controllers/Admin/Users.php @@ -2,12 +2,14 @@ namespace Neuron\Cms\Controllers\Admin; +use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Enums\FlashMessageType; use Neuron\Cms\Controllers\Content; use Neuron\Cms\Repositories\IUserRepository; use Neuron\Cms\Services\User\IUserCreator; use Neuron\Cms\Services\User\IUserUpdater; use Neuron\Cms\Services\User\IUserDeleter; +use Neuron\Data\Settings\SettingManager; use Neuron\Mvc\Application; use Neuron\Mvc\Requests\Request; use Neuron\Cms\Enums\UserRole; @@ -36,23 +38,44 @@ class Users extends Content * @param IUserCreator|null $userCreator * @param IUserUpdater|null $userUpdater * @param IUserDeleter|null $userDeleter + * @param SettingManager|null $settings + * @param SessionManager|null $sessionManager */ public function __construct( ?Application $app = null, ?IUserRepository $repository = null, ?IUserCreator $userCreator = null, ?IUserUpdater $userUpdater = null, - ?IUserDeleter $userDeleter = null + ?IUserDeleter $userDeleter = null, + ?SettingManager $settings = null, + ?SessionManager $sessionManager = null ) { - parent::__construct( $app ); - - // Use dependency injection when available (container provides dependencies) - // Otherwise resolve from container (fallback for compatibility) - $this->_repository = $repository ?? $app?->getContainer()?->get( IUserRepository::class ); - $this->_userCreator = $userCreator ?? $app?->getContainer()?->get( IUserCreator::class ); - $this->_userUpdater = $userUpdater ?? $app?->getContainer()?->get( IUserUpdater::class ); - $this->_userDeleter = $userDeleter ?? $app?->getContainer()?->get( IUserDeleter::class ); + parent::__construct( $app, $settings, $sessionManager ); + + if( $repository === null ) + { + throw new \InvalidArgumentException( 'IUserRepository must be injected' ); + } + $this->_repository = $repository; + + if( $userCreator === null ) + { + throw new \InvalidArgumentException( 'IUserCreator must be injected' ); + } + $this->_userCreator = $userCreator; + + if( $userUpdater === null ) + { + throw new \InvalidArgumentException( 'IUserUpdater must be injected' ); + } + $this->_userUpdater = $userUpdater; + + if( $userDeleter === null ) + { + throw new \InvalidArgumentException( 'IUserDeleter must be injected' ); + } + $this->_userDeleter = $userDeleter; } /** diff --git a/src/Cms/Controllers/Auth/Login.php b/src/Cms/Controllers/Auth/Login.php index 70b64fc..0d282de 100644 --- a/src/Cms/Controllers/Auth/Login.php +++ b/src/Cms/Controllers/Auth/Login.php @@ -2,11 +2,13 @@ namespace Neuron\Cms\Controllers\Auth; +use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Controllers\Content; use Neuron\Cms\Controllers\Traits\UsesDtos; use Neuron\Cms\Enums\FlashMessageType; use Neuron\Cms\Services\Auth\IAuthenticationService; use Neuron\Core\Exceptions\NotFound; +use Neuron\Data\Settings\SettingManager; use Neuron\Mvc\Application; use Neuron\Mvc\Responses\HttpResponseStatus; use Neuron\Mvc\Requests\Request; @@ -29,18 +31,24 @@ class Login extends Content /** * @param Application|null $app * @param IAuthenticationService|null $authentication + * @param SettingManager|null $settings + * @param SessionManager|null $sessionManager * @throws \Exception */ public function __construct( ?Application $app = null, - ?IAuthenticationService $authentication = null + ?IAuthenticationService $authentication = null, + ?SettingManager $settings = null, + ?SessionManager $sessionManager = null ) { - parent::__construct( $app ); + parent::__construct( $app, $settings, $sessionManager ); - // Use dependency injection when available (container provides dependencies) - // Otherwise resolve from container (fallback for compatibility) - $this->_authentication = $authentication ?? $app?->getContainer()?->get( IAuthenticationService::class ); + if( $authentication === null ) + { + throw new \InvalidArgumentException( 'IAuthenticationService must be injected' ); + } + $this->_authentication = $authentication; } /** diff --git a/src/Cms/Controllers/Auth/PasswordReset.php b/src/Cms/Controllers/Auth/PasswordReset.php index fbef208..0474a2a 100644 --- a/src/Cms/Controllers/Auth/PasswordReset.php +++ b/src/Cms/Controllers/Auth/PasswordReset.php @@ -2,11 +2,13 @@ namespace Neuron\Cms\Controllers\Auth; +use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Controllers\Content; use Neuron\Cms\Controllers\Traits\UsesDtos; use Neuron\Cms\Enums\FlashMessageType; use Neuron\Cms\Services\Auth\IPasswordResetter; use Neuron\Core\Exceptions\NotFound; +use Neuron\Data\Settings\SettingManager; use Neuron\Log\Log; use Neuron\Mvc\Application; use Neuron\Mvc\Requests\Request; @@ -32,18 +34,24 @@ class PasswordReset extends Content /** * @param Application|null $app * @param IPasswordResetter|null $passwordResetter + * @param SettingManager|null $settings + * @param SessionManager|null $sessionManager * @throws \Exception */ public function __construct( ?Application $app = null, - ?IPasswordResetter $passwordResetter = null + ?IPasswordResetter $passwordResetter = null, + ?SettingManager $settings = null, + ?SessionManager $sessionManager = null ) { - parent::__construct( $app ); + parent::__construct( $app, $settings, $sessionManager ); - // Use dependency injection when available (container provides dependencies) - // Otherwise resolve from container (fallback for compatibility) - $this->_passwordResetter = $passwordResetter ?? $app?->getContainer()?->get( IPasswordResetter::class ); + if( $passwordResetter === null ) + { + throw new \InvalidArgumentException( 'IPasswordResetter must be injected' ); + } + $this->_passwordResetter = $passwordResetter; } /** diff --git a/src/Cms/Controllers/Blog.php b/src/Cms/Controllers/Blog.php index 50913e8..36fe156 100644 --- a/src/Cms/Controllers/Blog.php +++ b/src/Cms/Controllers/Blog.php @@ -2,6 +2,7 @@ namespace Neuron\Cms\Controllers; use JetBrains\PhpStorm\NoReturn; +use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Models\Post; use Neuron\Cms\Repositories\IPostRepository; use Neuron\Cms\Repositories\ICategoryRepository; @@ -11,6 +12,7 @@ use Neuron\Cms\Services\Content\ShortcodeParser; use Neuron\Cms\Services\Widget\WidgetRenderer; use Neuron\Core\Exceptions\NotFound; +use Neuron\Data\Settings\SettingManager; use Neuron\Mvc\Application; use Neuron\Mvc\Requests\Request; use Neuron\Mvc\Responses\HttpResponseStatus; @@ -21,11 +23,11 @@ #[RouteGroup(prefix: '/blog')] class Blog extends Content { - private ?IPostRepository $_postRepository = null; - private ?ICategoryRepository $_categoryRepository = null; - private ?ITagRepository $_tagRepository = null; - private ?IUserRepository $_userRepository = null; - private ?EditorJsRenderer $_renderer = null; + private IPostRepository $_postRepository; + private ICategoryRepository $_categoryRepository; + private ITagRepository $_tagRepository; + private IUserRepository $_userRepository; + private EditorJsRenderer $_renderer; /** * @param Application|null $app @@ -34,6 +36,8 @@ class Blog extends Content * @param ITagRepository|null $tagRepository * @param IUserRepository|null $userRepository * @param EditorJsRenderer|null $renderer + * @param SettingManager|null $settings + * @param SessionManager|null $sessionManager * @throws \Exception */ public function __construct( @@ -42,18 +46,35 @@ public function __construct( ?ICategoryRepository $categoryRepository = null, ?ITagRepository $tagRepository = null, ?IUserRepository $userRepository = null, - ?EditorJsRenderer $renderer = null + ?EditorJsRenderer $renderer = null, + ?SettingManager $settings = null, + ?SessionManager $sessionManager = null ) { - parent::__construct( $app ); - - // Use dependency injection when available (container provides dependencies) - // Otherwise resolve from container (fallback for compatibility) - $this->_postRepository = $postRepository ?? $app?->getContainer()?->get( IPostRepository::class ); - $this->_categoryRepository = $categoryRepository ?? $app?->getContainer()?->get( ICategoryRepository::class ); - $this->_tagRepository = $tagRepository ?? $app?->getContainer()?->get( ITagRepository::class ); - $this->_userRepository = $userRepository ?? $app?->getContainer()?->get( IUserRepository::class ); - $this->_renderer = $renderer ?? $app?->getContainer()?->get( EditorJsRenderer::class ); + parent::__construct( $app, $settings, $sessionManager ); + + // Pure dependency injection - no service locator fallback + if( $postRepository === null ) { + throw new \InvalidArgumentException( 'IPostRepository must be injected' ); + } + if( $categoryRepository === null ) { + throw new \InvalidArgumentException( 'ICategoryRepository must be injected' ); + } + if( $tagRepository === null ) { + throw new \InvalidArgumentException( 'ITagRepository must be injected' ); + } + if( $userRepository === null ) { + throw new \InvalidArgumentException( 'IUserRepository must be injected' ); + } + if( $renderer === null ) { + throw new \InvalidArgumentException( 'EditorJsRenderer must be injected' ); + } + + $this->_postRepository = $postRepository; + $this->_categoryRepository = $categoryRepository; + $this->_tagRepository = $tagRepository; + $this->_userRepository = $userRepository; + $this->_renderer = $renderer; } /** diff --git a/src/Cms/Controllers/Calendar.php b/src/Cms/Controllers/Calendar.php index 6091b51..4f771c1 100644 --- a/src/Cms/Controllers/Calendar.php +++ b/src/Cms/Controllers/Calendar.php @@ -2,8 +2,10 @@ namespace Neuron\Cms\Controllers; +use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Repositories\IEventRepository; use Neuron\Cms\Repositories\IEventCategoryRepository; +use Neuron\Data\Settings\SettingManager; use Neuron\Mvc\Application; use Neuron\Mvc\Requests\Request; use Neuron\Mvc\Responses\HttpResponseStatus; @@ -28,20 +30,31 @@ class Calendar extends Content * @param Application|null $app * @param IEventRepository|null $eventRepository * @param IEventCategoryRepository|null $categoryRepository + * @param SettingManager|null $settings + * @param SessionManager|null $sessionManager * @throws \Exception */ public function __construct( ?Application $app = null, ?IEventRepository $eventRepository = null, - ?IEventCategoryRepository $categoryRepository = null + ?IEventCategoryRepository $categoryRepository = null, + ?SettingManager $settings = null, + ?SessionManager $sessionManager = null ) { - parent::__construct( $app ); + parent::__construct( $app, $settings, $sessionManager ); - // Use dependency injection when available (container provides dependencies) - // Otherwise resolve from container (fallback for compatibility) - $this->_eventRepository = $eventRepository ?? $app?->getContainer()?->get( IEventRepository::class ); - $this->_categoryRepository = $categoryRepository ?? $app?->getContainer()?->get( IEventCategoryRepository::class ); + if( $eventRepository === null ) + { + throw new \InvalidArgumentException( 'IEventRepository must be injected' ); + } + $this->_eventRepository = $eventRepository; + + if( $categoryRepository === null ) + { + throw new \InvalidArgumentException( 'IEventCategoryRepository must be injected' ); + } + $this->_categoryRepository = $categoryRepository; } /** diff --git a/src/Cms/Controllers/Content.php b/src/Cms/Controllers/Content.php index 7e11396..ce14a5d 100644 --- a/src/Cms/Controllers/Content.php +++ b/src/Cms/Controllers/Content.php @@ -66,8 +66,8 @@ class Content extends Base private string $_description = ''; private string $_url = 'example.com/bog'; private string $_rssUrl = 'example.com/blog/rss'; - protected ?SessionManager $_sessionManager = null; - protected ?SettingManager $_settings = null; + protected SessionManager $_sessionManager; + protected SettingManager $_settings; /** * @param Application|null $app @@ -82,15 +82,23 @@ public function __construct( { parent::__construct( $app ); - // Use dependency injection when available (container provides dependencies) - // Otherwise resolve from container (fallback for compatibility) - $this->_settings = $settings ?? $app?->getContainer()?->get( SettingManager::class ); - $this->_sessionManager = $sessionManager ?? $app?->getContainer()?->get( SessionManager::class ); + // Pure dependency injection - no service locator fallback + if( $settings === null ) + { + throw new \InvalidArgumentException( 'SettingManager must be injected' ); + } + if( $sessionManager === null ) + { + throw new \InvalidArgumentException( 'SessionManager must be injected' ); + } + + $this->_settings = $settings; + $this->_sessionManager = $sessionManager; - $this->setName( $this->_settings?->get( 'site', 'name' ) ?? 'Neuron CMS' ) - ->setTitle( $this->_settings?->get( 'site', 'title' ) ?? 'Neuron CMS' ) - ->setDescription( $this->_settings?->get( 'site', 'description' ) ?? '' ) - ->setUrl( $this->_settings?->get( 'site', 'url' ) ?? '' ) + $this->setName( $this->_settings->get( 'site', 'name' ) ?? 'Neuron CMS' ) + ->setTitle( $this->_settings->get( 'site', 'title' ) ?? 'Neuron CMS' ) + ->setDescription( $this->_settings->get( 'site', 'description' ) ?? '' ) + ->setUrl( $this->_settings->get( 'site', 'url' ) ?? '' ) ->setRssUrl($this->getUrl() . "/blog/rss" ); // Note: Registry is intentionally used here as a view data bag for global template variables. @@ -98,7 +106,8 @@ public function __construct( // Future improvement: Consider using a dedicated ViewContext service instead. try { - $version = Factories\Version::fromFile( "../.version.json" ); + $versionFilePath = $this->_settings->get( 'paths', 'version_file' ) ?? "../.version.json"; + $version = Factories\Version::fromFile( $versionFilePath ); Registry::getInstance()->set( 'version', 'v'.$version->getAsString() ); } catch( \Exception $e ) diff --git a/src/Cms/Controllers/Home.php b/src/Cms/Controllers/Home.php index ba5b660..db08aa6 100644 --- a/src/Cms/Controllers/Home.php +++ b/src/Cms/Controllers/Home.php @@ -1,8 +1,10 @@ _registrationService = $registrationService ?? $app?->getContainer()?->get( IRegistrationService::class ); + // Pure dependency injection - no service locator fallback + if( $registrationService === null ) + { + throw new \InvalidArgumentException( 'IRegistrationService must be injected' ); + } + + $this->_registrationService = $registrationService; } /** @@ -49,7 +59,7 @@ public function __construct( public function index( Request $request ): string { // Check if registration is enabled - $registrationEnabled = $this->_registrationService?->isRegistrationEnabled() ?? false; + $registrationEnabled = $this->_registrationService->isRegistrationEnabled(); return $this->renderHtml( HttpResponseStatus::OK, diff --git a/src/Cms/Controllers/Member/Dashboard.php b/src/Cms/Controllers/Member/Dashboard.php index 22a59af..9e59d85 100644 --- a/src/Cms/Controllers/Member/Dashboard.php +++ b/src/Cms/Controllers/Member/Dashboard.php @@ -2,9 +2,11 @@ namespace Neuron\Cms\Controllers\Member; +use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Controllers\Content; use Neuron\Cms\Services\Auth\CsrfToken; use Neuron\Core\Exceptions\NotFound; +use Neuron\Data\Settings\SettingManager; use Neuron\Mvc\Application; use Neuron\Mvc\Requests\Request; use Neuron\Mvc\Responses\HttpResponseStatus; @@ -22,12 +24,18 @@ class Dashboard extends Content { /** * @param Application|null $app + * @param SettingManager|null $settings + * @param SessionManager|null $sessionManager * @return void * @throws \Exception */ - public function __construct( ?Application $app = null ) + public function __construct( + ?Application $app = null, + ?SettingManager $settings = null, + ?SessionManager $sessionManager = null + ) { - parent::__construct( $app ); + parent::__construct( $app, $settings, $sessionManager ); } /** diff --git a/src/Cms/Controllers/Member/Profile.php b/src/Cms/Controllers/Member/Profile.php index f7ea8e1..06bd0ac 100644 --- a/src/Cms/Controllers/Member/Profile.php +++ b/src/Cms/Controllers/Member/Profile.php @@ -2,10 +2,12 @@ namespace Neuron\Cms\Controllers\Member; +use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Controllers\Content; use Neuron\Cms\Repositories\IUserRepository; use Neuron\Cms\Services\User\IUserUpdater; use Neuron\Cms\Auth\PasswordHasher; +use Neuron\Data\Settings\SettingManager; use Neuron\Mvc\Application; use Neuron\Mvc\Requests\Request; use Neuron\Mvc\Responses\HttpResponseStatus; @@ -30,22 +32,38 @@ class Profile extends Content * @param IUserRepository|null $repository * @param PasswordHasher|null $hasher * @param IUserUpdater|null $userUpdater + * @param SettingManager|null $settings + * @param SessionManager|null $sessionManager * @throws \Exception */ public function __construct( ?Application $app = null, ?IUserRepository $repository = null, ?PasswordHasher $hasher = null, - ?IUserUpdater $userUpdater = null + ?IUserUpdater $userUpdater = null, + ?SettingManager $settings = null, + ?SessionManager $sessionManager = null ) { - parent::__construct( $app ); + parent::__construct( $app, $settings, $sessionManager ); - // Use dependency injection when available (container provides dependencies) - // Otherwise resolve from container (fallback for compatibility) - $this->_repository = $repository ?? $app?->getContainer()?->get( IUserRepository::class ); - $this->_hasher = $hasher ?? $app?->getContainer()?->get( PasswordHasher::class ); - $this->_userUpdater = $userUpdater ?? $app?->getContainer()?->get( IUserUpdater::class ); + if( $repository === null ) + { + throw new \InvalidArgumentException( 'IUserRepository must be injected' ); + } + $this->_repository = $repository; + + if( $hasher === null ) + { + throw new \InvalidArgumentException( 'PasswordHasher must be injected' ); + } + $this->_hasher = $hasher; + + if( $userUpdater === null ) + { + throw new \InvalidArgumentException( 'IUserUpdater must be injected' ); + } + $this->_userUpdater = $userUpdater; } /** diff --git a/src/Cms/Controllers/Member/Registration.php b/src/Cms/Controllers/Member/Registration.php index e0ba839..5eb6170 100644 --- a/src/Cms/Controllers/Member/Registration.php +++ b/src/Cms/Controllers/Member/Registration.php @@ -4,7 +4,7 @@ use Neuron\Cms\Controllers\Content; use Neuron\Cms\Controllers\Traits\UsesDtos; -use Neuron\Cms\Auth\ResendVerificationThrottle; +use Neuron\Cms\Services\Security\ResendVerificationThrottle; use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Services\Member\IRegistrationService; use Neuron\Cms\Services\Auth\IEmailVerifier; @@ -56,12 +56,29 @@ public function __construct( { parent::__construct( $app, $settings, $sessionManager ); - // Use dependency injection when available (container provides dependencies) - // Otherwise resolve from container (fallback for compatibility) - $this->_registrationService = $registrationService ?? $app?->getContainer()?->get( IRegistrationService::class ); - $this->_emailVerifier = $emailVerifier ?? $app?->getContainer()?->get( IEmailVerifier::class ); - $this->_resendThrottle = $resendThrottle ?? $app?->getContainer()?->get( ResendVerificationThrottle::class ); - $this->_ipResolver = $ipResolver ?? $app?->getContainer()?->get( IIpResolver::class ); + if( $registrationService === null ) + { + throw new \InvalidArgumentException( 'IRegistrationService must be injected' ); + } + $this->_registrationService = $registrationService; + + if( $emailVerifier === null ) + { + throw new \InvalidArgumentException( 'IEmailVerifier must be injected' ); + } + $this->_emailVerifier = $emailVerifier; + + if( $resendThrottle === null ) + { + throw new \InvalidArgumentException( 'ResendVerificationThrottle must be injected' ); + } + $this->_resendThrottle = $resendThrottle; + + if( $ipResolver === null ) + { + throw new \InvalidArgumentException( 'IIpResolver must be injected' ); + } + $this->_ipResolver = $ipResolver; } /** diff --git a/src/Cms/Controllers/Pages.php b/src/Cms/Controllers/Pages.php index 0a72ac3..4d56177 100644 --- a/src/Cms/Controllers/Pages.php +++ b/src/Cms/Controllers/Pages.php @@ -2,6 +2,7 @@ namespace Neuron\Cms\Controllers; +use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Models\Page as PageModel; use Neuron\Cms\Repositories\IPageRepository; use Neuron\Cms\Repositories\IPostRepository; @@ -9,6 +10,7 @@ use Neuron\Cms\Services\Content\ShortcodeParser; use Neuron\Cms\Services\Widget\WidgetRenderer; use Neuron\Core\Exceptions\NotFound; +use Neuron\Data\Settings\SettingManager; use Neuron\Mvc\Application; use Neuron\Mvc\Requests\Request; use Neuron\Mvc\Responses\HttpResponseStatus; @@ -32,20 +34,33 @@ class Pages extends Content * @param Application|null $app * @param IPageRepository|null $pageRepository * @param EditorJsRenderer|null $renderer + * @param SettingManager|null $settings + * @param SessionManager|null $sessionManager * @throws \Exception */ public function __construct( ?Application $app = null, ?IPageRepository $pageRepository = null, - ?EditorJsRenderer $renderer = null + ?EditorJsRenderer $renderer = null, + ?SettingManager $settings = null, + ?SessionManager $sessionManager = null ) { - parent::__construct( $app ); + parent::__construct( $app, $settings, $sessionManager ); - // Use dependency injection when available (container provides dependencies) - // Otherwise resolve from container (fallback for compatibility) - $this->_pageRepository = $pageRepository ?? $app?->getContainer()?->get( IPageRepository::class ); - $this->_renderer = $renderer ?? $app?->getContainer()?->get( EditorJsRenderer::class ); + // Pure dependency injection - no service locator fallback + if( $pageRepository === null ) + { + throw new \InvalidArgumentException( 'IPageRepository must be injected' ); + } + + if( $renderer === null ) + { + throw new \InvalidArgumentException( 'EditorJsRenderer must be injected' ); + } + + $this->_pageRepository = $pageRepository; + $this->_renderer = $renderer; } /** diff --git a/src/Cms/Auth/ResendVerificationThrottle.php b/src/Cms/Services/Security/ResendVerificationThrottle.php similarity index 98% rename from src/Cms/Auth/ResendVerificationThrottle.php rename to src/Cms/Services/Security/ResendVerificationThrottle.php index 3533214..4554bb8 100644 --- a/src/Cms/Auth/ResendVerificationThrottle.php +++ b/src/Cms/Services/Security/ResendVerificationThrottle.php @@ -1,6 +1,6 @@ root = vfsStream::setup( 'test' ); // Store original environment $this->originalEnv = $_ENV ?? []; - + // Clear environment variable putenv( 'SYSTEM_BASE_PATH' ); } diff --git a/tests/Unit/Cms/BlogControllerTest.php b/tests/Unit/Cms/BlogControllerTest.php index 8fb51ad..92ab1c5 100644 --- a/tests/Unit/Cms/BlogControllerTest.php +++ b/tests/Unit/Cms/BlogControllerTest.php @@ -260,58 +260,14 @@ public function __construct( PDO $PDO ) private function createBlogWithInjectedRepositories(): Blog { - // Get SettingManager from Registry for the test + // Get SettingManager and SessionManager from Registry for the test $settingManager = Registry::getInstance()->get( 'Settings' ); + $sessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + $renderer = $this->createMock( \Neuron\Cms\Services\Content\EditorJsRenderer::class ); - // Create Blog controller with SettingManager - $blog = new Blog( null, null, null, null, null ); - - // Inject our test repositories and settings using reflection - $reflection = new \ReflectionClass( $blog ); - - $postRepoProp = $reflection->getProperty( '_postRepository' ); - $postRepoProp->setAccessible( true ); - $postRepoProp->setValue( $blog, $this->_postRepository ); - - $categoryRepoProp = $reflection->getProperty( '_categoryRepository' ); - $categoryRepoProp->setAccessible( true ); - $categoryRepoProp->setValue( $blog, $this->_categoryRepository ); - - $tagRepoProp = $reflection->getProperty( '_tagRepository' ); - $tagRepoProp->setAccessible( true ); - $tagRepoProp->setValue( $blog, $this->_tagRepository ); - - $userRepoProp = $reflection->getProperty( '_userRepository' ); - $userRepoProp->setAccessible( true ); - $userRepoProp->setValue( $blog, $this->_userRepository ); - - // Inject SettingManager into parent Content class - $parentReflection = $reflection->getParentClass(); - $settingsProp = $parentReflection->getProperty( '_settings' ); - $settingsProp->setAccessible( true ); - $settingsProp->setValue( $blog, $settingManager ); - - // Re-initialize the Content properties with the injected SettingManager - // Use reflection to call setName, setTitle, etc. or just set them directly - $nameProp = $parentReflection->getProperty( '_name' ); - $nameProp->setAccessible( true ); - $nameProp->setValue( $blog, $settingManager->get( 'site', 'name' ) ); - - $titleProp = $parentReflection->getProperty( '_title' ); - $titleProp->setAccessible( true ); - $titleProp->setValue( $blog, $settingManager->get( 'site', 'title' ) ); - - $descProp = $parentReflection->getProperty( '_description' ); - $descProp->setAccessible( true ); - $descProp->setValue( $blog, $settingManager->get( 'site', 'description' ) ); - - $urlProp = $parentReflection->getProperty( '_url' ); - $urlProp->setAccessible( true ); - $urlProp->setValue( $blog, $settingManager->get( 'site', 'url' ) ); - - $rssUrlProp = $parentReflection->getProperty( '_rssUrl' ); - $rssUrlProp->setAccessible( true ); - $rssUrlProp->setValue( $blog, $settingManager->get( 'site', 'url' ) . '/blog/rss' ); + // Create Blog controller with required dependencies - no reflection needed! + $blog = new Blog( null, $this->_postRepository, $this->_categoryRepository, + $this->_tagRepository, $this->_userRepository, $renderer, $settingManager, $sessionManager ); // CRITICAL: Restore the test PDO on Model class // Blog constructor created repositories which called Model::setPdo() with ConnectionFactory PDO, diff --git a/tests/Unit/Cms/Container/CmsServiceProviderTest.php b/tests/Unit/Cms/Container/CmsServiceProviderTest.php index 4736702..8fe1378 100644 --- a/tests/Unit/Cms/Container/CmsServiceProviderTest.php +++ b/tests/Unit/Cms/Container/CmsServiceProviderTest.php @@ -3,7 +3,7 @@ namespace Tests\Cms\Container; use Neuron\Cms\Auth\PasswordHasher; -use Neuron\Cms\Auth\ResendVerificationThrottle; +use Neuron\Cms\Services\Security\ResendVerificationThrottle; use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Container\CmsServiceProvider; use Neuron\Cms\Repositories\DatabaseCategoryRepository; diff --git a/tests/Unit/Cms/ContentControllerTest.php b/tests/Unit/Cms/ContentControllerTest.php index 6136674..cdbb698 100644 --- a/tests/Unit/Cms/ContentControllerTest.php +++ b/tests/Unit/Cms/ContentControllerTest.php @@ -14,13 +14,21 @@ class ContentControllerTest extends TestCase { private SettingManager $_settingManager; + private string $_versionFilePath; + private $mockSessionManager; protected function setUp(): void { parent::setUp(); - // Create virtual filesystem (local variable, not stored) - $root = vfsStream::setup( 'test' ); + // Create version file in temp directory + $this->_versionFilePath = sys_get_temp_dir() . '/neuron-test-version-' . uniqid() . '.json'; + $versionContent = json_encode([ + 'major' => 1, + 'minor' => 2, + 'patch' => 3 + ]); + file_put_contents( $this->_versionFilePath, $versionContent ); // Create mock settings $settings = new Memory(); @@ -28,31 +36,16 @@ protected function setUp(): void $settings->set( 'site', 'title', 'Test Title' ); $settings->set( 'site', 'description', 'Test Description' ); $settings->set( 'site', 'url', 'http://test.com' ); + $settings->set( 'paths', 'version_file', $this->_versionFilePath ); // Wrap in SettingManager $this->_settingManager = new SettingManager( $settings ); + // Create mock SessionManager + $this->mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + // Store settings in registry for backward compatibility Registry::getInstance()->set( 'Settings', $this->_settingManager ); - - // Create version file - $versionContent = json_encode([ - 'major' => 1, - 'minor' => 2, - 'patch' => 3 - ]); - - vfsStream::newFile( '.version.json' ) - ->at( $root ) - ->setContent( $versionContent ); - - // Create a real version file in parent directory for the controller - // ContentController loads from "../.version.json" - $parentDir = dirname( getcwd() ); - if ( !file_exists( $parentDir . '/.version.json' ) ) - { - file_put_contents( $parentDir . '/.version.json', $versionContent ); - } } protected function tearDown(): void @@ -65,8 +58,10 @@ protected function tearDown(): void Registry::getInstance()->set( 'DtoFactoryService', null ); // Clean up temp version file - $parentDir = dirname( getcwd() ); - @unlink( $parentDir . '/.version.json' ); + if( isset( $this->_versionFilePath ) && file_exists( $this->_versionFilePath ) ) + { + unlink( $this->_versionFilePath ); + } parent::tearDown(); } @@ -76,7 +71,7 @@ protected function tearDown(): void */ public function testConstructor() { - $controller = new Content( null, $this->_settingManager ); + $controller = new Content( null, $this->_settingManager, $this->mockSessionManager ); // Check that properties were set from settings $this->assertEquals( 'Test Site', $controller->getName() ); @@ -96,7 +91,7 @@ public function testConstructor() */ public function testSettersAndGetters() { - $controller = new Content(); + $controller = new Content( null, $this->_settingManager, $this->mockSessionManager ); // Test Name $controller->setName( 'New Name' ); @@ -124,7 +119,7 @@ public function testSettersAndGetters() */ public function testMethodChaining() { - $controller = new Content(); + $controller = new Content( null, $this->_settingManager, $this->mockSessionManager ); $result = $controller ->setName( 'Chained Name' ) @@ -152,6 +147,7 @@ public function testMethodChaining() public function testMarkdownMethod() { $controller = $this->getMockBuilder( Content::class ) + ->setConstructorArgs( [ null, $this->_settingManager, $this->mockSessionManager ] ) ->onlyMethods( [ 'renderMarkdown' ] ) ->getMock(); @@ -178,7 +174,7 @@ public function testMarkdownMethod() */ public function testGetSessionManager() { - $controller = new Content(); + $controller = new Content( null, $this->_settingManager, $this->mockSessionManager ); // Use reflection to access protected method $reflection = new \ReflectionClass( $controller ); @@ -201,7 +197,7 @@ public function testGetSessionManager() */ public function testFlash() { - $controller = new Content(); + $controller = new Content( null, $this->_settingManager, $this->mockSessionManager ); // Use reflection to access protected methods $reflection = new \ReflectionClass( $controller ); diff --git a/tests/Unit/Cms/Controllers/Admin/CategoriesTest.php b/tests/Unit/Cms/Controllers/Admin/CategoriesTest.php index 28a24e4..fed309c 100644 --- a/tests/Unit/Cms/Controllers/Admin/CategoriesTest.php +++ b/tests/Unit/Cms/Controllers/Admin/CategoriesTest.php @@ -56,29 +56,44 @@ protected function setUp(): void public function testConstructorWithAllDependencies(): void { + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $controller = new Categories( $this->mockApp, $this->mockCategoryRepo, $this->mockCategoryCreator, - $this->mockCategoryUpdater + $this->mockCategoryUpdater, + $mockSettingManager, + $mockSessionManager ); $this->assertInstanceOf( Categories::class, $controller ); } - public function testConstructorResolvesFromContainer(): void + public function testConstructorThrowsExceptionWithoutSettingManager(): void { - $controller = new Categories( $this->mockApp ); - $this->assertInstanceOf( Categories::class, $controller ); + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'SettingManager must be injected' ); + + new Categories( null ); } - public function testConstructorWithPartialDependencies(): void + public function testConstructorThrowsExceptionWithoutCategoryRepository(): void { - $controller = new Categories( + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'ICategoryRepository must be injected' ); + + $mockSettings = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + + new Categories( $this->mockApp, - $this->mockCategoryRepo + null, + null, + null, + $mockSettings, + $mockSessionManager ); - - $this->assertInstanceOf( Categories::class, $controller ); } } diff --git a/tests/Unit/Cms/Controllers/Admin/DashboardTest.php b/tests/Unit/Cms/Controllers/Admin/DashboardTest.php index 02ce741..f3aa843 100644 --- a/tests/Unit/Cms/Controllers/Admin/DashboardTest.php +++ b/tests/Unit/Cms/Controllers/Admin/DashboardTest.php @@ -1,55 +1,107 @@ createMock( SettingManager::class ); - $mockSettings->method( 'get' )->willReturnCallback( function( $section, $key = null ) { - if( $section === 'views' && $key === 'path' ) return '/tmp/views'; - if( $section === 'cache' && $key === 'enabled' ) return false; - return 'Test Site'; - }); - Registry::getInstance()->set( 'Settings', $mockSettings ); - - // Create mock application - $this->mockApp = $this->createMock( Application::class ); + // Create version file in temp directory + $this->_versionFilePath = sys_get_temp_dir() . '/neuron-test-version-' . uniqid() . '.json'; + $versionContent = json_encode([ 'major' => 1, 'minor' => 0, 'patch' => 0 ]); + file_put_contents( $this->_versionFilePath, $versionContent ); + + // Create mock settings + $settings = new Memory(); + $settings->set( 'site', 'name', 'Test Site' ); + $settings->set( 'site', 'title', 'Test Title' ); + $settings->set( 'site', 'description', 'Test Description' ); + $settings->set( 'site', 'url', 'http://test.com' ); + $settings->set( 'paths', 'version_file', $this->_versionFilePath ); + + $this->_settingManager = new SettingManager( $settings ); + Registry::getInstance()->set( 'Settings', $this->_settingManager ); + + $this->mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $this->mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); } protected function tearDown(): void { - Registry::getInstance()->reset(); + Registry::getInstance()->set( 'Settings', null ); + Registry::getInstance()->set( 'version', null ); + Registry::getInstance()->set( 'name', null ); + Registry::getInstance()->set( 'rss_url', null ); + Registry::getInstance()->set( 'DtoFactoryService', null ); + Registry::getInstance()->set( 'CsrfToken', null ); + + // Clean up temp version file + if( isset( $this->_versionFilePath ) && file_exists( $this->_versionFilePath ) ) + { + unlink( $this->_versionFilePath ); + } + parent::tearDown(); } - public function testConstructorWithApplication(): void + public function testConstructor(): void { - $controller = new Dashboard( $this->mockApp ); + $controller = new Dashboard( null, $this->mockSettingManager, $this->mockSessionManager ); $this->assertInstanceOf( Dashboard::class, $controller ); } - public function testConstructorWithNullApplication(): void + public function testConstructorWithApplication(): void { - $controller = new Dashboard( null ); + $mockApp = $this->createMock( Application::class ); + $controller = new Dashboard( $mockApp, $this->mockSettingManager, $this->mockSessionManager ); $this->assertInstanceOf( Dashboard::class, $controller ); } - public function testConstructorExtendsContentController(): void + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testIndexRendersView(): void { - $controller = new Dashboard( $this->mockApp ); - $this->assertInstanceOf( \Neuron\Cms\Controllers\Content::class, $controller ); + // Mock the controller to test view() method chain + $controller = $this->getMockBuilder( Dashboard::class ) + ->setConstructorArgs( [ null, $this->mockSettingManager, $this->mockSessionManager ] ) + ->onlyMethods( [ 'view' ] ) + ->getMock(); + + // Create a mock ViewContext that supports the fluent interface + $mockViewContext = $this->getMockBuilder( ViewContext::class ) + ->disableOriginalConstructor() + ->onlyMethods( [ 'title', 'description', 'withCurrentUser', 'withCsrfToken', 'render' ] ) + ->getMock(); + + $mockViewContext->method( 'title' )->willReturn( $mockViewContext ); + $mockViewContext->method( 'description' )->willReturn( $mockViewContext ); + $mockViewContext->method( 'withCurrentUser' )->willReturn( $mockViewContext ); + $mockViewContext->method( 'withCsrfToken' )->willReturn( $mockViewContext ); + $mockViewContext->method( 'render' )->willReturn( 'Dashboard' ); + + $controller->method( 'view' )->willReturn( $mockViewContext ); + + $request = new Request(); + $result = $controller->index( $request ); + + $this->assertEquals( 'Dashboard', $result ); } } diff --git a/tests/Unit/Cms/Controllers/Admin/EventCategoriesTest.php b/tests/Unit/Cms/Controllers/Admin/EventCategoriesTest.php index 96c2cd1..f3bbc59 100644 --- a/tests/Unit/Cms/Controllers/Admin/EventCategoriesTest.php +++ b/tests/Unit/Cms/Controllers/Admin/EventCategoriesTest.php @@ -48,19 +48,26 @@ protected function setUp(): void public function testConstructorWithAllDependencies(): void { + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $controller = new EventCategories( $this->mockApp, $this->createMock( IEventCategoryRepository::class ), $this->createMock( IEventCategoryCreator::class ), - $this->createMock( IEventCategoryUpdater::class ) + $this->createMock( IEventCategoryUpdater::class ), + $mockSettingManager, + $mockSessionManager ); $this->assertInstanceOf( EventCategories::class, $controller ); } - public function testConstructorResolvesFromContainer(): void + public function testConstructorThrowsExceptionWithoutSettingManager(): void { - $controller = new EventCategories( $this->mockApp ); - $this->assertInstanceOf( EventCategories::class, $controller ); + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'SettingManager must be injected' ); + + new EventCategories( null ); } } diff --git a/tests/Unit/Cms/Controllers/Admin/EventsTest.php b/tests/Unit/Cms/Controllers/Admin/EventsTest.php index 5a254bf..78ec1e2 100644 --- a/tests/Unit/Cms/Controllers/Admin/EventsTest.php +++ b/tests/Unit/Cms/Controllers/Admin/EventsTest.php @@ -50,20 +50,27 @@ protected function setUp(): void public function testConstructorWithAllDependencies(): void { + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $controller = new Events( $this->mockApp, $this->createMock( IEventRepository::class ), $this->createMock( IEventCategoryRepository::class ), $this->createMock( IEventCreator::class ), - $this->createMock( IEventUpdater::class ) + $this->createMock( IEventUpdater::class ), + $mockSettingManager, + $mockSessionManager ); $this->assertInstanceOf( Events::class, $controller ); } - public function testConstructorResolvesFromContainer(): void + public function testConstructorThrowsExceptionWithoutSettingManager(): void { - $controller = new Events( $this->mockApp ); - $this->assertInstanceOf( Events::class, $controller ); + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'SettingManager must be injected' ); + + new Events( null ); } } diff --git a/tests/Unit/Cms/Controllers/Admin/PagesTest.php b/tests/Unit/Cms/Controllers/Admin/PagesTest.php index 85bf896..158c8b7 100644 --- a/tests/Unit/Cms/Controllers/Admin/PagesTest.php +++ b/tests/Unit/Cms/Controllers/Admin/PagesTest.php @@ -48,19 +48,26 @@ protected function setUp(): void public function testConstructorWithAllDependencies(): void { + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $controller = new Pages( $this->mockApp, $this->createMock( IPageRepository::class ), $this->createMock( IPageCreator::class ), - $this->createMock( IPageUpdater::class ) + $this->createMock( IPageUpdater::class ), + $mockSettingManager, + $mockSessionManager ); $this->assertInstanceOf( Pages::class, $controller ); } - public function testConstructorResolvesFromContainer(): void + public function testConstructorThrowsExceptionWithoutSettingManager(): void { - $controller = new Pages( $this->mockApp ); - $this->assertInstanceOf( Pages::class, $controller ); + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'SettingManager must be injected' ); + + new Pages( null ); } } diff --git a/tests/Unit/Cms/Controllers/Admin/PostsTest.php b/tests/Unit/Cms/Controllers/Admin/PostsTest.php index 61debf3..1fab7d5 100644 --- a/tests/Unit/Cms/Controllers/Admin/PostsTest.php +++ b/tests/Unit/Cms/Controllers/Admin/PostsTest.php @@ -55,6 +55,9 @@ protected function setUp(): void public function testConstructorWithAllDependencies(): void { + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $controller = new Posts( $this->mockApp, $this->createMock( IPostRepository::class ), @@ -62,25 +65,40 @@ public function testConstructorWithAllDependencies(): void $this->createMock( ITagRepository::class ), $this->createMock( IPostCreator::class ), $this->createMock( IPostUpdater::class ), - $this->createMock( IPostDeleter::class ) + $this->createMock( IPostDeleter::class ), + $mockSettingManager, + $mockSessionManager ); $this->assertInstanceOf( Posts::class, $controller ); } - public function testConstructorResolvesFromContainer(): void + public function testConstructorThrowsExceptionWithoutSettingManager(): void { - $controller = new Posts( $this->mockApp ); - $this->assertInstanceOf( Posts::class, $controller ); + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'SettingManager must be injected' ); + + new Posts( null ); } - public function testConstructorWithPartialDependencies(): void + public function testConstructorThrowsExceptionWithoutPostRepository(): void { - $controller = new Posts( + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'IPostRepository must be injected' ); + + $mockSettings = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + + new Posts( $this->mockApp, - $this->createMock( IPostRepository::class ) + null, + null, + null, + null, + null, + null, + $mockSettings, + $mockSessionManager ); - - $this->assertInstanceOf( Posts::class, $controller ); } } diff --git a/tests/Unit/Cms/Controllers/Admin/ProfileTest.php b/tests/Unit/Cms/Controllers/Admin/ProfileTest.php new file mode 100644 index 0000000..172c6a2 --- /dev/null +++ b/tests/Unit/Cms/Controllers/Admin/ProfileTest.php @@ -0,0 +1,99 @@ +_versionFilePath = sys_get_temp_dir() . '/neuron-test-version-' . uniqid() . '.json'; + $versionContent = json_encode([ 'major' => 1, 'minor' => 0, 'patch' => 0 ]); + file_put_contents( $this->_versionFilePath, $versionContent ); + + $settings = new Memory(); + $settings->set( 'site', 'name', 'Test Site' ); + $settings->set( 'site', 'title', 'Test Title' ); + $settings->set( 'site', 'description', 'Test Description' ); + $settings->set( 'site', 'url', 'http://test.com' ); + $settings->set( 'paths', 'version_file', $this->_versionFilePath ); + + $this->_settingManager = new SettingManager( $settings ); + Registry::getInstance()->set( 'Settings', $this->_settingManager ); + } + + protected function tearDown(): void + { + Registry::getInstance()->set( 'Settings', null ); + Registry::getInstance()->set( 'version', null ); + Registry::getInstance()->set( 'name', null ); + Registry::getInstance()->set( 'rss_url', null ); + Registry::getInstance()->set( 'DtoFactoryService', null ); + Registry::getInstance()->set( 'CsrfToken', null ); + Registry::getInstance()->set( 'User', null ); + + // Clean up temp version file + if( isset( $this->_versionFilePath ) && file_exists( $this->_versionFilePath ) ) + { + unlink( $this->_versionFilePath ); + } + + parent::tearDown(); + } + + /** + * Note: Admin\Profile controller requires integration testing due to: + * - Global auth() function dependency + * - Global group_timezones_for_select() function dependency + * - Complex DTO handling with YAML configuration + * - Redirect mechanisms that terminate execution + * + * Unit testing these methods would require extensive mocking infrastructure. + * Integration tests are more appropriate for this controller. + */ + public function testConstructorWithDependencies(): void + { + $mockRepository = $this->createMock( IUserRepository::class ); + $mockHasher = $this->createMock( PasswordHasher::class ); + $mockUpdater = $this->createMock( IUserUpdater::class ); + $mockSessionManager = $this->createMock( SessionManager::class ); + + $controller = new Profile( + null, + $mockRepository, + $mockHasher, + $mockUpdater, + $this->_settingManager, + $mockSessionManager + ); + + $this->assertInstanceOf( Profile::class, $controller ); + } + + public function testConstructorThrowsExceptionWithoutSettingManager(): void + { + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'SettingManager must be injected' ); + + new Profile( null ); + } +} diff --git a/tests/Unit/Cms/Controllers/Admin/TagsTest.php b/tests/Unit/Cms/Controllers/Admin/TagsTest.php index 0befb78..6208617 100644 --- a/tests/Unit/Cms/Controllers/Admin/TagsTest.php +++ b/tests/Unit/Cms/Controllers/Admin/TagsTest.php @@ -46,18 +46,25 @@ protected function setUp(): void public function testConstructorWithAllDependencies(): void { + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $controller = new Tags( $this->mockApp, $this->createMock( ITagRepository::class ), - $this->createMock( SlugGenerator::class ) + $this->createMock( SlugGenerator::class ), + $mockSettingManager, + $mockSessionManager ); $this->assertInstanceOf( Tags::class, $controller ); } - public function testConstructorResolvesFromContainer(): void + public function testConstructorThrowsExceptionWithoutSettingManager(): void { - $controller = new Tags( $this->mockApp ); - $this->assertInstanceOf( Tags::class, $controller ); + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'SettingManager must be injected' ); + + new Tags( null ); } } diff --git a/tests/Unit/Cms/Controllers/Admin/UsersTest.php b/tests/Unit/Cms/Controllers/Admin/UsersTest.php index 2bf9eb0..436324c 100644 --- a/tests/Unit/Cms/Controllers/Admin/UsersTest.php +++ b/tests/Unit/Cms/Controllers/Admin/UsersTest.php @@ -50,30 +50,46 @@ protected function setUp(): void public function testConstructorWithAllDependencies(): void { + $mockSettings = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $controller = new Users( $this->mockApp, $this->createMock( IUserRepository::class ), $this->createMock( IUserCreator::class ), $this->createMock( IUserUpdater::class ), - $this->createMock( IUserDeleter::class ) + $this->createMock( IUserDeleter::class ), + $mockSettings, + $mockSessionManager ); $this->assertInstanceOf( Users::class, $controller ); } - public function testConstructorResolvesFromContainer(): void + public function testConstructorThrowsExceptionWithoutSettingManager(): void { - $controller = new Users( $this->mockApp ); - $this->assertInstanceOf( Users::class, $controller ); + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'SettingManager must be injected' ); + + new Users( null ); } - public function testConstructorWithPartialDependencies(): void + public function testConstructorThrowsExceptionWithoutUserRepository(): void { - $controller = new Users( + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'IUserRepository must be injected' ); + + $mockSettings = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + + new Users( $this->mockApp, - $this->createMock( IUserRepository::class ) + null, + null, + null, + null, + $mockSettings, + $mockSessionManager ); - - $this->assertInstanceOf( Users::class, $controller ); } } diff --git a/tests/Unit/Cms/Controllers/Auth/LoginTest.php b/tests/Unit/Cms/Controllers/Auth/LoginTest.php index 8f366af..6110251 100644 --- a/tests/Unit/Cms/Controllers/Auth/LoginTest.php +++ b/tests/Unit/Cms/Controllers/Auth/LoginTest.php @@ -56,19 +56,27 @@ protected function setUp(): void ->willReturn( $this->mockContainer ); // Create controller with dependency injection - $this->controller = new Login( $this->mockApp, $this->mockAuth ); + $this->controller = new Login( $this->mockApp, $this->mockAuth, $mockSettings, $this->mockSession ); } public function testConstructorWithDependencies(): void { - $controller = new Login( $this->mockApp, $this->mockAuth ); + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = new Login( $this->mockApp, $this->mockAuth, $mockSettingManager, $mockSessionManager ); $this->assertInstanceOf( Login::class, $controller ); } - public function testConstructorResolvesFromContainer(): void + public function testConstructorThrowsExceptionWithoutAuthenticationService(): void { - $controller = new Login( $this->mockApp ); - $this->assertInstanceOf( Login::class, $controller ); + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'IAuthenticationService must be injected' ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + new Login( $this->mockApp, null, $mockSettingManager, $mockSessionManager ); } public function testIsValidRedirectUrlAcceptsValidRelativeUrls(): void diff --git a/tests/Unit/Cms/Controllers/Auth/PasswordResetTest.php b/tests/Unit/Cms/Controllers/Auth/PasswordResetTest.php index 8b42bba..5e674a4 100644 --- a/tests/Unit/Cms/Controllers/Auth/PasswordResetTest.php +++ b/tests/Unit/Cms/Controllers/Auth/PasswordResetTest.php @@ -90,7 +90,7 @@ public function getRules(): array { ->willReturn( $this->mockContainer ); // Create controller with dependency injection - $this->controller = new PasswordReset( $this->mockApp, $this->mockPasswordResetter ); + $this->controller = new PasswordReset( $this->mockApp, $this->mockPasswordResetter, $mockSettings, $this->mockSession ); } protected function tearDown(): void @@ -102,14 +102,22 @@ protected function tearDown(): void public function testConstructorWithDependencies(): void { - $controller = new PasswordReset( $this->mockApp, $this->mockPasswordResetter ); + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = new PasswordReset( $this->mockApp, $this->mockPasswordResetter, $mockSettingManager, $mockSessionManager ); $this->assertInstanceOf( PasswordReset::class, $controller ); } - public function testConstructorResolvesFromContainer(): void + public function testConstructorThrowsExceptionWithoutPasswordResetter(): void { - $controller = new PasswordReset( $this->mockApp ); - $this->assertInstanceOf( PasswordReset::class, $controller ); + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'IPasswordResetter must be injected' ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + new PasswordReset( $this->mockApp, null, $mockSettingManager, $mockSessionManager ); } public function testShowForgotPasswordFormReturnsView(): void diff --git a/tests/Unit/Cms/Controllers/CalendarTest.php b/tests/Unit/Cms/Controllers/CalendarTest.php new file mode 100644 index 0000000..166127b --- /dev/null +++ b/tests/Unit/Cms/Controllers/CalendarTest.php @@ -0,0 +1,344 @@ +_versionFilePath = sys_get_temp_dir() . '/neuron-test-version-' . uniqid() . '.json'; + $versionContent = json_encode([ 'major' => 1, 'minor' => 0, 'patch' => 0 ]); + file_put_contents( $this->_versionFilePath, $versionContent ); + + // Create mock settings + $settings = new Memory(); + $settings->set( 'site', 'name', 'Test Site' ); + $settings->set( 'site', 'title', 'Test Title' ); + $settings->set( 'site', 'description', 'Test Description' ); + $settings->set( 'site', 'url', 'http://test.com' ); + $settings->set( 'paths', 'version_file', $this->_versionFilePath ); + + $this->_settingManager = new SettingManager( $settings ); + Registry::getInstance()->set( 'Settings', $this->_settingManager ); + } + + protected function tearDown(): void + { + Registry::getInstance()->set( 'Settings', null ); + Registry::getInstance()->set( 'version', null ); + Registry::getInstance()->set( 'name', null ); + Registry::getInstance()->set( 'rss_url', null ); + Registry::getInstance()->set( 'DtoFactoryService', null ); + + // Clean up temp version file + if( isset( $this->_versionFilePath ) && file_exists( $this->_versionFilePath ) ) + { + unlink( $this->_versionFilePath ); + } + + parent::tearDown(); + } + + public function testConstructorWithDependencies(): void + { + $mockEventRepository = $this->createMock( IEventRepository::class ); + $mockCategoryRepository = $this->createMock( IEventCategoryRepository::class ); + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = new Calendar( null, $mockEventRepository, $mockCategoryRepository, $mockSettingManager, $mockSessionManager ); + + $this->assertInstanceOf( Calendar::class, $controller ); + } + + public function testIndexRendersCalendarForCurrentMonth(): void + { + $mockEventRepository = $this->createMock( IEventRepository::class ); + $mockCategoryRepository = $this->createMock( IEventCategoryRepository::class ); + + $mockEventRepository->method( 'getByDateRange' )->willReturn( [] ); + $mockCategoryRepository->method( 'all' )->willReturn( [] ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = $this->getMockBuilder( Calendar::class ) + ->setConstructorArgs( [ null, $mockEventRepository, $mockCategoryRepository, $mockSettingManager, $mockSessionManager ] ) + ->onlyMethods( [ 'renderHtml' ] ) + ->getMock(); + + $controller->expects( $this->once() ) + ->method( 'renderHtml' ) + ->with( + $this->anything(), + $this->callback( function( $data ) { + return isset( $data['Title'] ) && + isset( $data['events'] ) && + isset( $data['categories'] ) && + isset( $data['currentMonth'] ) && + isset( $data['currentYear'] ); + } ), + 'index', + 'default' + ) + ->willReturn( 'Calendar' ); + + $request = new Request(); + $result = $controller->index( $request ); + + $this->assertEquals( 'Calendar', $result ); + } + + public function testIndexRendersCalendarForSpecificMonth(): void + { + $mockEventRepository = $this->createMock( IEventRepository::class ); + $mockCategoryRepository = $this->createMock( IEventCategoryRepository::class ); + + // Expect date range for March 2024 + $mockEventRepository->expects( $this->once() ) + ->method( 'getByDateRange' ) + ->with( + $this->callback( function( $date ) { + return $date->format( 'Y-m-d' ) === '2024-03-01'; + } ), + $this->callback( function( $date ) { + return $date->format( 'Y-m-d' ) === '2024-03-31'; + } ), + 'published' + ) + ->willReturn( [] ); + + $mockCategoryRepository->method( 'all' )->willReturn( [] ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = $this->getMockBuilder( Calendar::class ) + ->setConstructorArgs( [ null, $mockEventRepository, $mockCategoryRepository, $mockSettingManager, $mockSessionManager ] ) + ->onlyMethods( [ 'renderHtml' ] ) + ->getMock(); + + $controller->method( 'renderHtml' )->willReturn( 'Calendar March 2024' ); + + $request = $this->getMockBuilder( Request::class ) + ->onlyMethods( [ 'get' ] ) + ->getMock(); + $request->method( 'get' ) + ->willReturnCallback( function( $key, $default ) { + return match( $key ) { + 'month' => '3', + 'year' => '2024', + default => $default + }; + } ); + + $result = $controller->index( $request ); + + $this->assertEquals( 'Calendar March 2024', $result ); + } + + public function testShowRendersPublishedEvent(): void + { + $mockEvent = $this->createMock( Event::class ); + $mockEvent->method( 'getTitle' )->willReturn( 'Test Event' ); + $mockEvent->method( 'getDescription' )->willReturn( 'Event description' ); + $mockEvent->method( 'isPublished' )->willReturn( true ); + + $mockEventRepository = $this->createMock( IEventRepository::class ); + $mockEventRepository->method( 'findBySlug' )->with( 'test-event' )->willReturn( $mockEvent ); + $mockEventRepository->expects( $this->once() ) + ->method( 'incrementViewCount' ) + ->with( $mockEvent ); + + $mockCategoryRepository = $this->createMock( IEventCategoryRepository::class ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = $this->getMockBuilder( Calendar::class ) + ->setConstructorArgs( [ null, $mockEventRepository, $mockCategoryRepository, $mockSettingManager, $mockSessionManager ] ) + ->onlyMethods( [ 'renderHtml' ] ) + ->getMock(); + + $controller->expects( $this->once() ) + ->method( 'renderHtml' ) + ->with( + $this->anything(), + $this->callback( function( $data ) use ( $mockEvent ) { + return $data['event'] === $mockEvent && + isset( $data['Title'] ) && + isset( $data['Description'] ); + } ), + 'show', + 'default' + ) + ->willReturn( 'Event Detail' ); + + $request = new Request(); + $request->setRouteParameters( [ 'slug' => 'test-event' ] ); + $result = $controller->show( $request ); + + $this->assertEquals( 'Event Detail', $result ); + } + + public function testShowThrowsExceptionForNonexistentEvent(): void + { + $mockEventRepository = $this->createMock( IEventRepository::class ); + $mockEventRepository->method( 'findBySlug' )->willReturn( null ); + + $mockCategoryRepository = $this->createMock( IEventCategoryRepository::class ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = new Calendar( null, $mockEventRepository, $mockCategoryRepository, $mockSettingManager, $mockSessionManager ); + + $this->expectException( \RuntimeException::class ); + $this->expectExceptionMessage( 'Event not found' ); + + $request = new Request(); + $request->setRouteParameters( [ 'slug' => 'nonexistent' ] ); + $controller->show( $request ); + } + + public function testShowThrowsExceptionForUnpublishedEvent(): void + { + $mockEvent = $this->createMock( Event::class ); + $mockEvent->method( 'isPublished' )->willReturn( false ); + + $mockEventRepository = $this->createMock( IEventRepository::class ); + $mockEventRepository->method( 'findBySlug' )->willReturn( $mockEvent ); + + $mockCategoryRepository = $this->createMock( IEventCategoryRepository::class ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = new Calendar( null, $mockEventRepository, $mockCategoryRepository, $mockSettingManager, $mockSessionManager ); + + $this->expectException( \RuntimeException::class ); + $this->expectExceptionMessage( 'Event not found' ); + + $request = new Request(); + $request->setRouteParameters( [ 'slug' => 'unpublished-event' ] ); + $controller->show( $request ); + } + + public function testCategoryRendersEventsForCategory(): void + { + $mockCategory = $this->createMock( EventCategory::class ); + $mockCategory->method( 'getId' )->willReturn( 5 ); + $mockCategory->method( 'getName' )->willReturn( 'Workshops' ); + + $mockEventRepository = $this->createMock( IEventRepository::class ); + $mockEventRepository->method( 'getByCategory' )->with( 5, 'published' )->willReturn( [] ); + + $mockCategoryRepository = $this->createMock( IEventCategoryRepository::class ); + $mockCategoryRepository->method( 'findBySlug' )->with( 'workshops' )->willReturn( $mockCategory ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = $this->getMockBuilder( Calendar::class ) + ->setConstructorArgs( [ null, $mockEventRepository, $mockCategoryRepository, $mockSettingManager, $mockSessionManager ] ) + ->onlyMethods( [ 'renderHtml' ] ) + ->getMock(); + + $controller->expects( $this->once() ) + ->method( 'renderHtml' ) + ->with( + $this->anything(), + $this->callback( function( $data ) use ( $mockCategory ) { + return $data['category'] === $mockCategory && + isset( $data['events'] ) && + isset( $data['Title'] ) && + isset( $data['Description'] ); + } ), + 'category', + 'default' + ) + ->willReturn( 'Category Events' ); + + $request = new Request(); + $request->setRouteParameters( [ 'slug' => 'workshops' ] ); + $result = $controller->category( $request ); + + $this->assertEquals( 'Category Events', $result ); + } + + public function testCategoryThrowsExceptionForNonexistentCategory(): void + { + $mockEventRepository = $this->createMock( IEventRepository::class ); + $mockCategoryRepository = $this->createMock( IEventCategoryRepository::class ); + $mockCategoryRepository->method( 'findBySlug' )->willReturn( null ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = new Calendar( null, $mockEventRepository, $mockCategoryRepository, $mockSettingManager, $mockSessionManager ); + + $this->expectException( \RuntimeException::class ); + $this->expectExceptionMessage( 'Category not found' ); + + $request = new Request(); + $request->setRouteParameters( [ 'slug' => 'nonexistent' ] ); + $controller->category( $request ); + } + + public function testShowUsesEventTitleWhenDescriptionNotSet(): void + { + $mockEvent = $this->createMock( Event::class ); + $mockEvent->method( 'getTitle' )->willReturn( 'Event Title' ); + $mockEvent->method( 'getDescription' )->willReturn( null ); // No description + $mockEvent->method( 'isPublished' )->willReturn( true ); + + $mockEventRepository = $this->createMock( IEventRepository::class ); + $mockEventRepository->method( 'findBySlug' )->willReturn( $mockEvent ); + $mockEventRepository->method( 'incrementViewCount' ); + + $mockCategoryRepository = $this->createMock( IEventCategoryRepository::class ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = $this->getMockBuilder( Calendar::class ) + ->setConstructorArgs( [ null, $mockEventRepository, $mockCategoryRepository, $mockSettingManager, $mockSessionManager ] ) + ->onlyMethods( [ 'renderHtml' ] ) + ->getMock(); + + $controller->expects( $this->once() ) + ->method( 'renderHtml' ) + ->with( + $this->anything(), + $this->callback( function( $data ) { + // Description should default to event title when null + return $data['Description'] === 'Event Title'; + } ), + 'show', + 'default' + ) + ->willReturn( 'Event' ); + + $request = new Request(); + $request->setRouteParameters( [ 'slug' => 'test-event' ] ); + $controller->show( $request ); + } +} diff --git a/tests/Unit/Cms/Controllers/CategoriesControllerTest.php b/tests/Unit/Cms/Controllers/CategoriesControllerTest.php index 0d3fbf3..44537c2 100644 --- a/tests/Unit/Cms/Controllers/CategoriesControllerTest.php +++ b/tests/Unit/Cms/Controllers/CategoriesControllerTest.php @@ -81,7 +81,9 @@ public function testIndexReturnsAllCategories(): void $creator = $this->createMock( Creator::class ); $updater = $this->createMock( Updater::class ); - $deleter = $this->createMock( Deleter::class ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); $controller = $this->getMockBuilder( Categories::class ) ->setConstructorArgs([ @@ -89,7 +91,8 @@ public function testIndexReturnsAllCategories(): void $repository, $creator, $updater, - $deleter + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); @@ -131,7 +134,9 @@ public function testCreateReturnsForm(): void $repository = $this->createMock( DatabaseCategoryRepository::class ); $creator = $this->createMock( Creator::class ); $updater = $this->createMock( Updater::class ); - $deleter = $this->createMock( Deleter::class ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); $controller = $this->getMockBuilder( Categories::class ) ->setConstructorArgs([ @@ -139,7 +144,8 @@ public function testCreateReturnsForm(): void $repository, $creator, $updater, - $deleter + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); @@ -182,14 +188,17 @@ public function testEditThrowsExceptionWhenCategoryNotFound(): void $creator = $this->createMock( Creator::class ); $updater = $this->createMock( Updater::class ); - $deleter = $this->createMock( Deleter::class ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); $controller = new Categories( null, $repository, $creator, $updater, - $deleter + $mockSettingManager, + $mockSessionManager ); $request = $this->createMock( Request::class ); diff --git a/tests/Unit/Cms/Controllers/EventCategoriesControllerTest.php b/tests/Unit/Cms/Controllers/EventCategoriesControllerTest.php index 244d45a..b8e3c4e 100644 --- a/tests/Unit/Cms/Controllers/EventCategoriesControllerTest.php +++ b/tests/Unit/Cms/Controllers/EventCategoriesControllerTest.php @@ -81,7 +81,9 @@ public function testIndexReturnsAllCategories(): void $creator = $this->createMock( Creator::class ); $updater = $this->createMock( Updater::class ); - $deleter = $this->createMock( Deleter::class ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); $controller = $this->getMockBuilder( EventCategories::class ) ->setConstructorArgs([ @@ -89,7 +91,8 @@ public function testIndexReturnsAllCategories(): void $repository, $creator, $updater, - $deleter + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); @@ -132,7 +135,9 @@ public function testCreateReturnsForm(): void $repository = $this->createMock( DatabaseEventCategoryRepository::class ); $creator = $this->createMock( Creator::class ); $updater = $this->createMock( Updater::class ); - $deleter = $this->createMock( Deleter::class ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); $controller = $this->getMockBuilder( EventCategories::class ) ->setConstructorArgs([ @@ -140,7 +145,8 @@ public function testCreateReturnsForm(): void $repository, $creator, $updater, - $deleter + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); diff --git a/tests/Unit/Cms/Controllers/EventsControllerTest.php b/tests/Unit/Cms/Controllers/EventsControllerTest.php index 7ed1fa3..3e77b42 100644 --- a/tests/Unit/Cms/Controllers/EventsControllerTest.php +++ b/tests/Unit/Cms/Controllers/EventsControllerTest.php @@ -86,7 +86,9 @@ public function testIndexReturnsAllEventsForAdmin(): void $categoryRepository = $this->createMock( DatabaseEventCategoryRepository::class ); $creator = $this->createMock( Creator::class ); $updater = $this->createMock( Updater::class ); - $deleter = $this->createMock( Deleter::class ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); $controller = $this->getMockBuilder( Events::class ) ->setConstructorArgs([ @@ -95,7 +97,8 @@ public function testIndexReturnsAllEventsForAdmin(): void $categoryRepository, $creator, $updater, - $deleter + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); @@ -151,7 +154,9 @@ public function testIndexFiltersEventsForNonAdmin(): void $categoryRepository = $this->createMock( DatabaseEventCategoryRepository::class ); $creator = $this->createMock( Creator::class ); $updater = $this->createMock( Updater::class ); - $deleter = $this->createMock( Deleter::class ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); $controller = $this->getMockBuilder( Events::class ) ->setConstructorArgs([ @@ -160,7 +165,8 @@ public function testIndexFiltersEventsForNonAdmin(): void $categoryRepository, $creator, $updater, - $deleter + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); @@ -207,7 +213,9 @@ public function testCreateReturnsFormForAuthenticatedUser(): void $creator = $this->createMock( Creator::class ); $updater = $this->createMock( Updater::class ); - $deleter = $this->createMock( Deleter::class ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); $controller = $this->getMockBuilder( Events::class ) ->setConstructorArgs([ @@ -216,7 +224,8 @@ public function testCreateReturnsFormForAuthenticatedUser(): void $categoryRepository, $creator, $updater, - $deleter + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); diff --git a/tests/Unit/Cms/Controllers/HomeTest.php b/tests/Unit/Cms/Controllers/HomeTest.php new file mode 100644 index 0000000..343cde2 --- /dev/null +++ b/tests/Unit/Cms/Controllers/HomeTest.php @@ -0,0 +1,180 @@ +_versionFilePath = sys_get_temp_dir() . '/neuron-test-version-' . uniqid() . '.json'; + $versionContent = json_encode([ + 'major' => 1, + 'minor' => 0, + 'patch' => 0 + ]); + file_put_contents( $this->_versionFilePath, $versionContent ); + + // Create mock settings + $settings = new Memory(); + $settings->set( 'site', 'name', 'Test Site' ); + $settings->set( 'site', 'title', 'Test Title' ); + $settings->set( 'site', 'description', 'Test Description' ); + $settings->set( 'site', 'url', 'http://test.com' ); + $settings->set( 'paths', 'version_file', $this->_versionFilePath ); + + // Wrap in SettingManager + $this->_settingManager = new SettingManager( $settings ); + + // Store settings in registry + Registry::getInstance()->set( 'Settings', $this->_settingManager ); + } + + protected function tearDown(): void + { + // Clear registry + Registry::getInstance()->set( 'Settings', null ); + Registry::getInstance()->set( 'version', null ); + Registry::getInstance()->set( 'name', null ); + Registry::getInstance()->set( 'rss_url', null ); + Registry::getInstance()->set( 'DtoFactoryService', null ); + + // Clean up temp version file + if( isset( $this->_versionFilePath ) && file_exists( $this->_versionFilePath ) ) + { + unlink( $this->_versionFilePath ); + } + + parent::tearDown(); + } + + public function testConstructorWithRegistrationService(): void + { + $mockRegistrationService = $this->createMock( IRegistrationService::class ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = new Home( null, $mockRegistrationService, $this->_settingManager, $mockSessionManager ); + + $this->assertInstanceOf( Home::class, $controller ); + } + + public function testConstructorThrowsExceptionWithoutDependencies(): void + { + $this->expectException( \InvalidArgumentException::class ); + // Either SettingManager or IRegistrationService exception will be thrown + // depending on check order (SettingManager is checked in parent first) + + new Home( null, null, null, null ); + } + + public function testIndexWithRegistrationEnabled(): void + { + $mockRegistrationService = $this->createMock( IRegistrationService::class ); + $mockRegistrationService->method( 'isRegistrationEnabled' )->willReturn( true ); + + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + // Mock the controller to test renderHtml is called with correct params + $controller = $this->getMockBuilder( Home::class ) + ->setConstructorArgs( [ null, $mockRegistrationService, $this->_settingManager, $mockSessionManager ] ) + ->onlyMethods( [ 'renderHtml' ] ) + ->getMock(); + + $controller->expects( $this->once() ) + ->method( 'renderHtml' ) + ->with( + $this->anything(), + $this->callback( function( $data ) { + return isset( $data['RegistrationEnabled'] ) && + $data['RegistrationEnabled'] === true && + isset( $data['Title'] ) && + isset( $data['Name'] ) && + isset( $data['Description'] ); + } ), + 'index' + ) + ->willReturn( 'Home Page' ); + + $request = new Request(); + $result = $controller->index( $request ); + + $this->assertEquals( 'Home Page', $result ); + } + + public function testIndexWithRegistrationDisabled(): void + { + $mockRegistrationService = $this->createMock( IRegistrationService::class ); + $mockRegistrationService->method( 'isRegistrationEnabled' )->willReturn( false ); + + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = $this->getMockBuilder( Home::class ) + ->setConstructorArgs( [ null, $mockRegistrationService, $this->_settingManager, $mockSessionManager ] ) + ->onlyMethods( [ 'renderHtml' ] ) + ->getMock(); + + $controller->expects( $this->once() ) + ->method( 'renderHtml' ) + ->with( + $this->anything(), + $this->callback( function( $data ) { + return isset( $data['RegistrationEnabled'] ) && + $data['RegistrationEnabled'] === false; + } ), + 'index' + ) + ->willReturn( 'Home Page' ); + + $request = new Request(); + $result = $controller->index( $request ); + + $this->assertEquals( 'Home Page', $result ); + } + + + public function testIndexPassesCorrectDataToView(): void + { + $mockRegistrationService = $this->createMock( IRegistrationService::class ); + $mockRegistrationService->method( 'isRegistrationEnabled' )->willReturn( true ); + + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = $this->getMockBuilder( Home::class ) + ->setConstructorArgs( [ null, $mockRegistrationService, $this->_settingManager, $mockSessionManager ] ) + ->onlyMethods( [ 'renderHtml' ] ) + ->getMock(); + + $controller->expects( $this->once() ) + ->method( 'renderHtml' ) + ->with( + $this->anything(), + $this->callback( function( $data ) { + // Verify all required keys are present + return isset( $data['Title'] ) && + isset( $data['Name'] ) && + isset( $data['Description'] ) && + isset( $data['RegistrationEnabled'] ) && + $data['RegistrationEnabled'] === true; + } ), + 'index' + ) + ->willReturn( 'Home Page' ); + + $request = new Request(); + $controller->index( $request ); + } +} diff --git a/tests/Unit/Cms/Controllers/MediaIndexTest.php b/tests/Unit/Cms/Controllers/MediaIndexTest.php index f6ea0ce..44aa91b 100644 --- a/tests/Unit/Cms/Controllers/MediaIndexTest.php +++ b/tests/Unit/Cms/Controllers/MediaIndexTest.php @@ -63,26 +63,24 @@ public function testIndexReturnsSuccessWithResources(): void $user->method( 'getId' )->willReturn( 1 ); Registry::getInstance()->set( 'Auth.User', $user ); + // Create mocks for required dependencies + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $mockCloudinaryUploader = $this->createMock( CloudinaryUploader::class ); + $mockMediaValidator = $this->createMock( MediaValidator::class ); + // Create a partial mock that mocks renderHtml but allows view() to work $media = $this->getMockBuilder( Media::class ) + ->setConstructorArgs( [ null, $mockCloudinaryUploader, $mockMediaValidator, $mockSettingManager, $mockSessionManager ] ) ->onlyMethods( ['renderHtml'] ) ->getMock(); $media->method( 'renderHtml' )->willReturn( 'test' ); - // Create a mock session manager - $sessionManagerMock = $this->createMock( SessionManager::class ); - $sessionManagerMock->method( 'getFlash' )->willReturn( null ); - - // Inject mock session manager via reflection - $reflection = new \ReflectionClass( get_parent_class( Media::class ) ); - $sessionProperty = $reflection->getProperty( '_sessionManager' ); - $sessionProperty->setAccessible( true ); - $sessionProperty->setValue( $media, $sessionManagerMock ); + $mockSessionManager->method( 'getFlash' )->willReturn( null ); - // Create a mock uploader that returns resources - $uploaderMock = $this->createMock( CloudinaryUploader::class ); - $uploaderMock->method( 'listResources' )->willReturn( [ + // Configure the uploader mock to return resources + $mockCloudinaryUploader->method( 'listResources' )->willReturn( [ 'resources' => [ [ 'public_id' => 'test-folder/image1', @@ -101,12 +99,6 @@ public function testIndexReturnsSuccessWithResources(): void 'total_count' => 50 ] ); - // Inject mock uploader via reflection - $reflection = new \ReflectionClass( Media::class ); - $uploaderProperty = $reflection->getProperty( '_uploader' ); - $uploaderProperty->setAccessible( true ); - $uploaderProperty->setValue( $media, $uploaderMock ); - $request = $this->createMock( Request::class ); $request->method( 'get' )->with( 'cursor' )->willReturn( null ); @@ -123,26 +115,24 @@ public function testIndexHandlesCursorParameter(): void $user->method( 'getId' )->willReturn( 1 ); Registry::getInstance()->set( 'Auth.User', $user ); + // Create mocks for required dependencies + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $mockCloudinaryUploader = $this->createMock( CloudinaryUploader::class ); + $mockMediaValidator = $this->createMock( MediaValidator::class ); + // Create a partial mock that mocks renderHtml but allows view() to work $media = $this->getMockBuilder( Media::class ) + ->setConstructorArgs( [ null, $mockCloudinaryUploader, $mockMediaValidator, $mockSettingManager, $mockSessionManager ] ) ->onlyMethods( ['renderHtml'] ) ->getMock(); $media->method( 'renderHtml' )->willReturn( 'test' ); - // Create a mock session manager - $sessionManagerMock = $this->createMock( SessionManager::class ); - $sessionManagerMock->method( 'getFlash' )->willReturn( null ); - - // Inject mock session manager via reflection - $reflection = new \ReflectionClass( get_parent_class( Media::class ) ); - $sessionProperty = $reflection->getProperty( '_sessionManager' ); - $sessionProperty->setAccessible( true ); - $sessionProperty->setValue( $media, $sessionManagerMock ); + $mockSessionManager->method( 'getFlash' )->willReturn( null ); - // Create a mock uploader that expects cursor parameter - $uploaderMock = $this->createMock( CloudinaryUploader::class ); - $uploaderMock->expects( $this->once() ) + // Configure the uploader mock to expect cursor parameter + $mockCloudinaryUploader->expects( $this->once() ) ->method( 'listResources' ) ->with( $this->callback( function( $options ) { return $options['next_cursor'] === 'xyz789' @@ -154,12 +144,6 @@ public function testIndexHandlesCursorParameter(): void 'total_count' => 0 ] ); - // Inject mock uploader via reflection - $reflection = new \ReflectionClass( Media::class ); - $uploaderProperty = $reflection->getProperty( '_uploader' ); - $uploaderProperty->setAccessible( true ); - $uploaderProperty->setValue( $media, $uploaderMock ); - $request = $this->createMock( Request::class ); $request->method( 'get' )->with( 'cursor' )->willReturn( 'xyz789' ); @@ -175,34 +159,26 @@ public function testIndexHandlesListResourcesException(): void $user->method( 'getId' )->willReturn( 1 ); Registry::getInstance()->set( 'Auth.User', $user ); + // Create mocks for required dependencies + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $mockCloudinaryUploader = $this->createMock( CloudinaryUploader::class ); + $mockMediaValidator = $this->createMock( MediaValidator::class ); + // Create a partial mock that mocks renderHtml but allows view() to work $media = $this->getMockBuilder( Media::class ) + ->setConstructorArgs( [ null, $mockCloudinaryUploader, $mockMediaValidator, $mockSettingManager, $mockSessionManager ] ) ->onlyMethods( ['renderHtml'] ) ->getMock(); $media->method( 'renderHtml' )->willReturn( 'test' ); - // Create a mock session manager - $sessionManagerMock = $this->createMock( SessionManager::class ); - $sessionManagerMock->method( 'getFlash' )->willReturn( null ); + $mockSessionManager->method( 'getFlash' )->willReturn( null ); - // Inject mock session manager via reflection - $reflection = new \ReflectionClass( get_parent_class( Media::class ) ); - $sessionProperty = $reflection->getProperty( '_sessionManager' ); - $sessionProperty->setAccessible( true ); - $sessionProperty->setValue( $media, $sessionManagerMock ); - - // Create a mock uploader that throws exception - $uploaderMock = $this->createMock( CloudinaryUploader::class ); - $uploaderMock->method( 'listResources' ) + // Configure the uploader mock to throw exception + $mockCloudinaryUploader->method( 'listResources' ) ->willThrowException( new \Exception( 'Cloudinary API error' ) ); - // Inject mock uploader via reflection - $reflection = new \ReflectionClass( Media::class ); - $uploaderProperty = $reflection->getProperty( '_uploader' ); - $uploaderProperty->setAccessible( true ); - $uploaderProperty->setValue( $media, $uploaderMock ); - $request = $this->createMock( Request::class ); $request->method( 'get' )->with( 'cursor' )->willReturn( null ); diff --git a/tests/Unit/Cms/Controllers/MediaUploadTest.php b/tests/Unit/Cms/Controllers/MediaUploadTest.php index e3401b6..6a98687 100644 --- a/tests/Unit/Cms/Controllers/MediaUploadTest.php +++ b/tests/Unit/Cms/Controllers/MediaUploadTest.php @@ -65,7 +65,12 @@ public function testUploadImageReturnsErrorWhenNoFileUploaded(): void // Ensure $_FILES is empty $_FILES = []; - $media = new Media(); + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + $mockCloudinaryUploader = $this->createMock( CloudinaryUploader::class ); + $mockMediaValidator = $this->createMock( MediaValidator::class ); + + $media = new Media( null, $mockCloudinaryUploader, $mockMediaValidator, $mockSettingManager, $mockSessionManager ); $request = $this->createMock( Request::class ); $result = $media->uploadImage( $request ); @@ -94,8 +99,13 @@ public function testUploadImageReturnsErrorWhenValidationFails(): void 'size' => 1000 ]; + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + $mockCloudinaryUploader = $this->createMock( CloudinaryUploader::class ); + $mockMediaValidator = $this->createMock( MediaValidator::class ); + // Create Media controller - $media = new Media(); + $media = new Media( null, $mockCloudinaryUploader, $mockMediaValidator, $mockSettingManager, $mockSessionManager ); // Create a mock validator that fails $validatorMock = $this->createMock( MediaValidator::class ); @@ -127,7 +137,12 @@ public function testUploadFeaturedImageReturnsErrorWhenNoFileUploaded(): void // Ensure $_FILES is empty $_FILES = []; - $media = new Media(); + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + $mockCloudinaryUploader = $this->createMock( CloudinaryUploader::class ); + $mockMediaValidator = $this->createMock( MediaValidator::class ); + + $media = new Media( null, $mockCloudinaryUploader, $mockMediaValidator, $mockSettingManager, $mockSessionManager ); $request = $this->createMock( Request::class ); $result = $media->uploadFeaturedImage( $request ); @@ -156,8 +171,13 @@ public function testUploadFeaturedImageReturnsErrorWhenValidationFails(): void 'size' => 1000 ]; + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + $mockCloudinaryUploader = $this->createMock( CloudinaryUploader::class ); + $mockMediaValidator = $this->createMock( MediaValidator::class ); + // Create Media controller - $media = new Media(); + $media = new Media( null, $mockCloudinaryUploader, $mockMediaValidator, $mockSettingManager, $mockSessionManager ); // Create a mock validator that fails $validatorMock = $this->createMock( MediaValidator::class ); @@ -196,8 +216,13 @@ public function testUploadImageSuccessfulUpload(): void 'size' => 1000 ]; + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + $mockCloudinaryUploader = $this->createMock( CloudinaryUploader::class ); + $mockMediaValidator = $this->createMock( MediaValidator::class ); + // Create Media controller - $media = new Media(); + $media = new Media( null, $mockCloudinaryUploader, $mockMediaValidator, $mockSettingManager, $mockSessionManager ); // Create a mock validator that passes $validatorMock = $this->createMock( MediaValidator::class ); @@ -254,8 +279,13 @@ public function testUploadFeaturedImageSuccessfulUpload(): void 'size' => 2000 ]; + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + $mockCloudinaryUploader = $this->createMock( CloudinaryUploader::class ); + $mockMediaValidator = $this->createMock( MediaValidator::class ); + // Create Media controller - $media = new Media(); + $media = new Media( null, $mockCloudinaryUploader, $mockMediaValidator, $mockSettingManager, $mockSessionManager ); // Create a mock validator that passes $validatorMock = $this->createMock( MediaValidator::class ); @@ -312,8 +342,13 @@ public function testUploadImageHandlesUploadException(): void 'size' => 1000 ]; + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + $mockCloudinaryUploader = $this->createMock( CloudinaryUploader::class ); + $mockMediaValidator = $this->createMock( MediaValidator::class ); + // Create Media controller - $media = new Media(); + $media = new Media( null, $mockCloudinaryUploader, $mockMediaValidator, $mockSettingManager, $mockSessionManager ); // Create a mock validator that passes $validatorMock = $this->createMock( MediaValidator::class ); @@ -360,8 +395,13 @@ public function testUploadFeaturedImageHandlesUploadException(): void 'size' => 1000 ]; + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + $mockCloudinaryUploader = $this->createMock( CloudinaryUploader::class ); + $mockMediaValidator = $this->createMock( MediaValidator::class ); + // Create Media controller - $media = new Media(); + $media = new Media( null, $mockCloudinaryUploader, $mockMediaValidator, $mockSettingManager, $mockSessionManager ); // Create a mock validator that passes $validatorMock = $this->createMock( MediaValidator::class ); diff --git a/tests/Unit/Cms/Controllers/Member/DashboardTest.php b/tests/Unit/Cms/Controllers/Member/DashboardTest.php new file mode 100644 index 0000000..808f61e --- /dev/null +++ b/tests/Unit/Cms/Controllers/Member/DashboardTest.php @@ -0,0 +1,111 @@ +_versionFilePath = sys_get_temp_dir() . '/neuron-test-version-' . uniqid() . '.json'; + $versionContent = json_encode([ 'major' => 1, 'minor' => 0, 'patch' => 0 ]); + file_put_contents( $this->_versionFilePath, $versionContent ); + + // Create mock settings + $settings = new Memory(); + $settings->set( 'site', 'name', 'Test Site' ); + $settings->set( 'site', 'title', 'Test Title' ); + $settings->set( 'site', 'description', 'Test Description' ); + $settings->set( 'site', 'url', 'http://test.com' ); + $settings->set( 'paths', 'version_file', $this->_versionFilePath ); + + $this->_settingManager = new SettingManager( $settings ); + Registry::getInstance()->set( 'Settings', $this->_settingManager ); + } + + protected function tearDown(): void + { + Registry::getInstance()->set( 'Settings', null ); + Registry::getInstance()->set( 'version', null ); + Registry::getInstance()->set( 'name', null ); + Registry::getInstance()->set( 'rss_url', null ); + Registry::getInstance()->set( 'DtoFactoryService', null ); + Registry::getInstance()->set( 'CsrfToken', null ); + + // Clean up temp version file + if( isset( $this->_versionFilePath ) && file_exists( $this->_versionFilePath ) ) + { + unlink( $this->_versionFilePath ); + } + + parent::tearDown(); + } + + public function testConstructor(): void + { + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = new Dashboard( null, $mockSettingManager, $mockSessionManager ); + $this->assertInstanceOf( Dashboard::class, $controller ); + } + + public function testConstructorWithApplication(): void + { + $mockApp = $this->createMock( Application::class ); + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = new Dashboard( $mockApp, $mockSettingManager, $mockSessionManager ); + $this->assertInstanceOf( Dashboard::class, $controller ); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testIndexRendersView(): void + { + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + // Mock the controller to test view() method chain + $controller = $this->getMockBuilder( Dashboard::class ) + ->setConstructorArgs( [ null, $mockSettingManager, $mockSessionManager ] ) + ->onlyMethods( [ 'view' ] ) + ->getMock(); + + // Create a mock ViewContext that supports the fluent interface + $mockViewContext = $this->getMockBuilder( ViewContext::class ) + ->disableOriginalConstructor() + ->onlyMethods( [ 'title', 'description', 'withCurrentUser', 'withCsrfToken', 'render' ] ) + ->getMock(); + + $mockViewContext->method( 'title' )->willReturn( $mockViewContext ); + $mockViewContext->method( 'description' )->willReturn( $mockViewContext ); + $mockViewContext->method( 'withCurrentUser' )->willReturn( $mockViewContext ); + $mockViewContext->method( 'withCsrfToken' )->willReturn( $mockViewContext ); + $mockViewContext->method( 'render' )->willReturn( 'Member Dashboard' ); + + $controller->method( 'view' )->willReturn( $mockViewContext ); + + $request = new Request(); + $result = $controller->index( $request ); + + $this->assertEquals( 'Member Dashboard', $result ); + } +} diff --git a/tests/Unit/Cms/Controllers/Member/ProfileTest.php b/tests/Unit/Cms/Controllers/Member/ProfileTest.php new file mode 100644 index 0000000..6e30dd8 --- /dev/null +++ b/tests/Unit/Cms/Controllers/Member/ProfileTest.php @@ -0,0 +1,84 @@ +_versionFilePath = sys_get_temp_dir() . '/neuron-test-version-' . uniqid() . '.json'; + $versionContent = json_encode([ 'major' => 1, 'minor' => 0, 'patch' => 0 ]); + file_put_contents( $this->_versionFilePath, $versionContent ); + + $settings = new Memory(); + $settings->set( 'site', 'name', 'Test Site' ); + $settings->set( 'site', 'title', 'Test Title' ); + $settings->set( 'site', 'description', 'Test Description' ); + $settings->set( 'site', 'url', 'http://test.com' ); + $settings->set( 'paths', 'version_file', $this->_versionFilePath ); + + $this->_settingManager = new SettingManager( $settings ); + Registry::getInstance()->set( 'Settings', $this->_settingManager ); + } + + protected function tearDown(): void + { + Registry::getInstance()->set( 'Settings', null ); + Registry::getInstance()->set( 'version', null ); + Registry::getInstance()->set( 'name', null ); + Registry::getInstance()->set( 'rss_url', null ); + Registry::getInstance()->set( 'DtoFactoryService', null ); + Registry::getInstance()->set( 'CsrfToken', null ); + Registry::getInstance()->set( 'User', null ); + + // Clean up temp version file + if( isset( $this->_versionFilePath ) && file_exists( $this->_versionFilePath ) ) + { + unlink( $this->_versionFilePath ); + } + + parent::tearDown(); + } + + /** + * Note: Member\Profile controller requires integration testing due to: + * - Global auth() function dependency + * - Global group_timezones_for_select() function dependency + * - Complex DTO handling with YAML configuration + * - Redirect mechanisms that terminate execution + * + * Unit testing these methods would require extensive mocking infrastructure. + * Integration tests are more appropriate for this controller. + */ + public function testConstructorWithDependencies(): void + { + $mockRepository = $this->createMock( IUserRepository::class ); + $mockHasher = $this->createMock( PasswordHasher::class ); + $mockUpdater = $this->createMock( IUserUpdater::class ); + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = new Profile( null, $mockRepository, $mockHasher, $mockUpdater, $mockSettingManager, $mockSessionManager ); + + $this->assertInstanceOf( Profile::class, $controller ); + } +} diff --git a/tests/Unit/Cms/Controllers/Member/RegistrationTest.php b/tests/Unit/Cms/Controllers/Member/RegistrationTest.php index d9c7489..0289c6e 100644 --- a/tests/Unit/Cms/Controllers/Member/RegistrationTest.php +++ b/tests/Unit/Cms/Controllers/Member/RegistrationTest.php @@ -5,7 +5,7 @@ use Neuron\Cms\Controllers\Member\Registration; use Neuron\Cms\Services\Member\IRegistrationService; use Neuron\Cms\Services\Auth\IEmailVerifier; -use Neuron\Cms\Auth\ResendVerificationThrottle; +use Neuron\Cms\Services\Security\ResendVerificationThrottle; use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Services\Dto\DtoFactoryService; use Neuron\Data\Settings\SettingManager; @@ -124,20 +124,68 @@ public function testConstructorWithAllDependencies(): void $this->assertInstanceOf( Registration::class, $controller ); } - public function testConstructorResolvesFromContainer(): void + public function testConstructorThrowsExceptionWithoutRegistrationService(): void { - $controller = new Registration( $this->mockApp ); - $this->assertInstanceOf( Registration::class, $controller ); + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'IRegistrationService must be injected' ); + + new Registration( + $this->mockApp, + null, + $this->mockEmailVerifier, + $this->mockSettings, + $this->mockSession, + $this->mockResendThrottle, + $this->mockIpResolver + ); } - public function testConstructorWithPartialDependencies(): void + public function testConstructorThrowsExceptionWithoutEmailVerifier(): void { - $controller = new Registration( + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'IEmailVerifier must be injected' ); + + new Registration( $this->mockApp, - $this->mockRegistrationService + $this->mockRegistrationService, + null, + $this->mockSettings, + $this->mockSession, + $this->mockResendThrottle, + $this->mockIpResolver ); + } - $this->assertInstanceOf( Registration::class, $controller ); + public function testConstructorThrowsExceptionWithoutResendThrottle(): void + { + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'ResendVerificationThrottle must be injected' ); + + new Registration( + $this->mockApp, + $this->mockRegistrationService, + $this->mockEmailVerifier, + $this->mockSettings, + $this->mockSession, + null, + $this->mockIpResolver + ); + } + + public function testConstructorThrowsExceptionWithoutIpResolver(): void + { + $this->expectException( \InvalidArgumentException::class ); + $this->expectExceptionMessage( 'IIpResolver must be injected' ); + + new Registration( + $this->mockApp, + $this->mockRegistrationService, + $this->mockEmailVerifier, + $this->mockSettings, + $this->mockSession, + $this->mockResendThrottle, + null + ); } public function testShowRegistrationFormWhenEnabled(): void diff --git a/tests/Unit/Cms/Controllers/PagesControllerTest.php b/tests/Unit/Cms/Controllers/PagesControllerTest.php index 33c0616..32451e5 100644 --- a/tests/Unit/Cms/Controllers/PagesControllerTest.php +++ b/tests/Unit/Cms/Controllers/PagesControllerTest.php @@ -84,16 +84,19 @@ public function testIndexReturnsAllPagesForAdmin(): void $creator = $this->createMock( Creator::class ); $updater = $this->createMock( Updater::class ); - $deleter = $this->createMock( Deleter::class ); // Create controller with mocked dependencies and mocked view method + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $controller = $this->getMockBuilder( Pages::class ) ->setConstructorArgs([ null, $pageRepository, $creator, $updater, - $deleter + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); @@ -148,16 +151,19 @@ public function testIndexFiltersPagesForNonAdmin(): void $creator = $this->createMock( Creator::class ); $updater = $this->createMock( Updater::class ); - $deleter = $this->createMock( Deleter::class ); // Create controller with mocked dependencies + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $controller = $this->getMockBuilder( Pages::class ) ->setConstructorArgs([ null, $pageRepository, $creator, $updater, - $deleter + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); @@ -199,7 +205,9 @@ public function testCreateReturnsFormForAuthenticatedUser(): void $pageRepository = $this->createMock( DatabasePageRepository::class ); $creator = $this->createMock( Creator::class ); $updater = $this->createMock( Updater::class ); - $deleter = $this->createMock( Deleter::class ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); $controller = $this->getMockBuilder( Pages::class ) ->setConstructorArgs([ @@ -207,7 +215,8 @@ public function testCreateReturnsFormForAuthenticatedUser(): void $pageRepository, $creator, $updater, - $deleter + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); @@ -222,14 +231,6 @@ public function testCreateReturnsFormForAuthenticatedUser(): void $controller->method( 'view' )->willReturn( $viewBuilder ); - // Mock session manager - $reflection = new \ReflectionClass( get_parent_class( Pages::class ) ); - $sessionProperty = $reflection->getProperty( '_sessionManager' ); - $sessionProperty->setAccessible( true ); - - $sessionManager = $this->createMock( SessionManager::class ); - $sessionProperty->setValue( $controller, $sessionManager ); - $request = $this->createMock( Request::class ); $result = $controller->create( $request ); diff --git a/tests/Unit/Cms/Controllers/PagesTest.php b/tests/Unit/Cms/Controllers/PagesTest.php new file mode 100644 index 0000000..c39c3d7 --- /dev/null +++ b/tests/Unit/Cms/Controllers/PagesTest.php @@ -0,0 +1,258 @@ +_versionFilePath = sys_get_temp_dir() . '/neuron-test-version-' . uniqid() . '.json'; + $versionContent = json_encode([ + 'major' => 1, + 'minor' => 0, + 'patch' => 0 + ]); + file_put_contents( $this->_versionFilePath, $versionContent ); + + // Create mock settings + $settings = new Memory(); + $settings->set( 'site', 'name', 'Test Site' ); + $settings->set( 'site', 'title', 'Test Title' ); + $settings->set( 'site', 'description', 'Test Description' ); + $settings->set( 'site', 'url', 'http://test.com' ); + $settings->set( 'paths', 'version_file', $this->_versionFilePath ); + + // Wrap in SettingManager + $this->_settingManager = new SettingManager( $settings ); + + // Store settings in registry + Registry::getInstance()->set( 'Settings', $this->_settingManager ); + } + + protected function tearDown(): void + { + // Clear registry + Registry::getInstance()->set( 'Settings', null ); + Registry::getInstance()->set( 'version', null ); + Registry::getInstance()->set( 'name', null ); + Registry::getInstance()->set( 'rss_url', null ); + Registry::getInstance()->set( 'DtoFactoryService', null ); + + // Clean up temp version file + if( isset( $this->_versionFilePath ) && file_exists( $this->_versionFilePath ) ) + { + unlink( $this->_versionFilePath ); + } + + parent::tearDown(); + } + + public function testConstructorWithDependencies(): void + { + $mockPageRepository = $this->createMock( IPageRepository::class ); + $mockRenderer = $this->createMock( EditorJsRenderer::class ); + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = new Pages( null, $mockPageRepository, $mockRenderer, $mockSettingManager, $mockSessionManager ); + + $this->assertInstanceOf( Pages::class, $controller ); + } + + public function testShowRendersPublishedPage(): void + { + $mockPage = $this->createMock( Page::class ); + $mockPage->method( 'getId' )->willReturn( 1 ); + $mockPage->method( 'getTitle' )->willReturn( 'Test Page' ); + $mockPage->method( 'isPublished' )->willReturn( true ); + $mockPage->method( 'getContent' )->willReturn( [ 'blocks' => [] ] ); + $mockPage->method( 'getMetaTitle' )->willReturn( 'Test Meta Title' ); + $mockPage->method( 'getMetaDescription' )->willReturn( 'Test meta description' ); + $mockPage->method( 'getMetaKeywords' )->willReturn( 'test, keywords' ); + + $mockPageRepository = $this->createMock( IPageRepository::class ); + $mockPageRepository->method( 'findBySlug' )->with( 'test-page' )->willReturn( $mockPage ); + $mockPageRepository->expects( $this->once() ) + ->method( 'incrementViewCount' ) + ->with( 1 ); + + $mockRenderer = $this->createMock( EditorJsRenderer::class ); + $mockRenderer->method( 'render' )->willReturn( '

Rendered content

' ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = $this->getMockBuilder( Pages::class ) + ->setConstructorArgs( [ null, $mockPageRepository, $mockRenderer, $mockSettingManager, $mockSessionManager ] ) + ->onlyMethods( [ 'renderHtml' ] ) + ->getMock(); + + $controller->expects( $this->once() ) + ->method( 'renderHtml' ) + ->with( + $this->anything(), + $this->callback( function( $data ) use ( $mockPage ) { + return $data['Page'] === $mockPage && + $data['ContentHtml'] === '

Rendered content

' && + isset( $data['Title'] ) && + isset( $data['Description'] ) && + isset( $data['MetaKeywords'] ); + } ), + 'show' + ) + ->willReturn( 'Page content' ); + + $request = new Request(); + $request->setRouteParameters( [ 'slug' => 'test-page' ] ); + $result = $controller->show( $request ); + + $this->assertEquals( 'Page content', $result ); + } + + public function testShowThrowsNotFoundForNonexistentPage(): void + { + $mockPageRepository = $this->createMock( IPageRepository::class ); + $mockPageRepository->method( 'findBySlug' )->with( 'nonexistent' )->willReturn( null ); + + $mockRenderer = $this->createMock( EditorJsRenderer::class ); + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = new Pages( null, $mockPageRepository, $mockRenderer, $mockSettingManager, $mockSessionManager ); + + $this->expectException( NotFound::class ); + $this->expectExceptionMessage( 'Page not found' ); + + $request = new Request(); + $request->setRouteParameters( [ 'slug' => 'nonexistent' ] ); + $controller->show( $request ); + } + + public function testShowThrowsNotFoundForUnpublishedPage(): void + { + $mockPage = $this->createMock( Page::class ); + $mockPage->method( 'isPublished' )->willReturn( false ); + + $mockPageRepository = $this->createMock( IPageRepository::class ); + $mockPageRepository->method( 'findBySlug' )->with( 'unpublished' )->willReturn( $mockPage ); + + $mockRenderer = $this->createMock( EditorJsRenderer::class ); + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = new Pages( null, $mockPageRepository, $mockRenderer, $mockSettingManager, $mockSessionManager ); + + $this->expectException( NotFound::class ); + $this->expectExceptionMessage( 'Page not found' ); + + $request = new Request(); + $request->setRouteParameters( [ 'slug' => 'unpublished' ] ); + $controller->show( $request ); + } + + public function testShowUsesPageTitleWhenMetaTitleNotSet(): void + { + $mockPage = $this->createMock( Page::class ); + $mockPage->method( 'getId' )->willReturn( 1 ); + $mockPage->method( 'getTitle' )->willReturn( 'Test Page' ); + $mockPage->method( 'isPublished' )->willReturn( true ); + $mockPage->method( 'getContent' )->willReturn( [ 'blocks' => [] ] ); + $mockPage->method( 'getMetaTitle' )->willReturn( '' ); // Empty meta title + $mockPage->method( 'getMetaDescription' )->willReturn( '' ); + $mockPage->method( 'getMetaKeywords' )->willReturn( '' ); + + $mockPageRepository = $this->createMock( IPageRepository::class ); + $mockPageRepository->method( 'findBySlug' )->willReturn( $mockPage ); + $mockPageRepository->method( 'incrementViewCount' ); + + $mockRenderer = $this->createMock( EditorJsRenderer::class ); + $mockRenderer->method( 'render' )->willReturn( '

Content

' ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = $this->getMockBuilder( Pages::class ) + ->setConstructorArgs( [ null, $mockPageRepository, $mockRenderer, $mockSettingManager, $mockSessionManager ] ) + ->onlyMethods( [ 'renderHtml' ] ) + ->getMock(); + + $controller->expects( $this->once() ) + ->method( 'renderHtml' ) + ->with( + $this->anything(), + $this->callback( function( $data ) { + // Title should use page title when meta title is empty + return str_contains( $data['Title'], 'Test Page' ); + } ), + 'show' + ) + ->willReturn( 'Page' ); + + $request = new Request(); + $request->setRouteParameters( [ 'slug' => 'test-page' ] ); + $controller->show( $request ); + } + + public function testShowUsesDefaultDescriptionWhenMetaDescriptionNotSet(): void + { + $mockPage = $this->createMock( Page::class ); + $mockPage->method( 'getId' )->willReturn( 1 ); + $mockPage->method( 'getTitle' )->willReturn( 'Test Page' ); + $mockPage->method( 'isPublished' )->willReturn( true ); + $mockPage->method( 'getContent' )->willReturn( [ 'blocks' => [] ] ); + $mockPage->method( 'getMetaTitle' )->willReturn( 'Meta Title' ); + $mockPage->method( 'getMetaDescription' )->willReturn( '' ); // Empty meta description + $mockPage->method( 'getMetaKeywords' )->willReturn( '' ); + + $mockPageRepository = $this->createMock( IPageRepository::class ); + $mockPageRepository->method( 'findBySlug' )->willReturn( $mockPage ); + $mockPageRepository->method( 'incrementViewCount' ); + + $mockRenderer = $this->createMock( EditorJsRenderer::class ); + $mockRenderer->method( 'render' )->willReturn( '

Content

' ); + + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); + + $controller = $this->getMockBuilder( Pages::class ) + ->setConstructorArgs( [ null, $mockPageRepository, $mockRenderer, $mockSettingManager, $mockSessionManager ] ) + ->onlyMethods( [ 'renderHtml', 'getDescription' ] ) + ->getMock(); + + $controller->method( 'getDescription' )->willReturn( 'Default Description' ); + + $controller->expects( $this->once() ) + ->method( 'renderHtml' ) + ->with( + $this->anything(), + $this->callback( function( $data ) { + // Description should use default when meta description is empty + return $data['Description'] === 'Default Description'; + } ), + 'show' + ) + ->willReturn( 'Page' ); + + $request = new Request(); + $request->setRouteParameters( [ 'slug' => 'test-page' ] ); + $controller->show( $request ); + } +} diff --git a/tests/Unit/Cms/Controllers/PostsControllerTest.php b/tests/Unit/Cms/Controllers/PostsControllerTest.php index 9e7748c..7655dc9 100644 --- a/tests/Unit/Cms/Controllers/PostsControllerTest.php +++ b/tests/Unit/Cms/Controllers/PostsControllerTest.php @@ -92,6 +92,9 @@ public function testIndexReturnsAllPostsForAdmin(): void $deleter = $this->createMock( Deleter::class ); // Create controller with mocked dependencies and mocked view method + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $controller = $this->getMockBuilder( Posts::class ) ->setConstructorArgs([ null, @@ -100,7 +103,9 @@ public function testIndexReturnsAllPostsForAdmin(): void $tagRepository, $creator, $updater, - $deleter + $deleter, + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); @@ -160,6 +165,9 @@ public function testIndexFiltersPostsByAuthorForNonAdmin(): void $deleter = $this->createMock( Deleter::class ); // Create controller with mocked dependencies + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $controller = $this->getMockBuilder( Posts::class ) ->setConstructorArgs([ null, @@ -168,7 +176,9 @@ public function testIndexFiltersPostsByAuthorForNonAdmin(): void $tagRepository, $creator, $updater, - $deleter + $deleter, + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); @@ -218,6 +228,9 @@ public function testCreateReturnsFormForAuthenticatedUser(): void $updater = $this->createMock( Updater::class ); $deleter = $this->createMock( Deleter::class ); + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $controller = $this->getMockBuilder( Posts::class ) ->setConstructorArgs([ null, @@ -226,7 +239,9 @@ public function testCreateReturnsFormForAuthenticatedUser(): void $tagRepository, $creator, $updater, - $deleter + $deleter, + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); @@ -242,14 +257,6 @@ public function testCreateReturnsFormForAuthenticatedUser(): void $controller->method( 'view' )->willReturn( $viewBuilder ); - // Mock session manager - $reflection = new \ReflectionClass( get_parent_class( Posts::class ) ); - $sessionProperty = $reflection->getProperty( '_sessionManager' ); - $sessionProperty->setAccessible( true ); - - $sessionManager = $this->createMock( SessionManager::class ); - $sessionProperty->setValue( $controller, $sessionManager ); - $request = $this->createMock( Request::class ); $result = $controller->create( $request ); @@ -278,6 +285,9 @@ public function testEditThrowsExceptionWhenUserUnauthorized(): void $updater = $this->createMock( Updater::class ); $deleter = $this->createMock( Deleter::class ); + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $controller = new Posts( null, $postRepository, @@ -285,7 +295,9 @@ public function testEditThrowsExceptionWhenUserUnauthorized(): void $tagRepository, $creator, $updater, - $deleter + $deleter, + $mockSettingManager, + $mockSessionManager ); $request = $this->createMock( Request::class ); diff --git a/tests/Unit/Cms/Controllers/TagsControllerTest.php b/tests/Unit/Cms/Controllers/TagsControllerTest.php index 8c8bcf7..aa38638 100644 --- a/tests/Unit/Cms/Controllers/TagsControllerTest.php +++ b/tests/Unit/Cms/Controllers/TagsControllerTest.php @@ -76,10 +76,17 @@ public function testIndexReturnsAllTags(): void ->method( 'allWithPostCount' ) ->willReturn( $tags ); + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $mockSlugGenerator = $this->createMock( \Neuron\Cms\Services\SlugGenerator::class ); + $controller = $this->getMockBuilder( Tags::class ) ->setConstructorArgs([ null, - $repository + $repository, + $mockSlugGenerator, + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); @@ -95,14 +102,6 @@ public function testIndexReturnsAllTags(): void $controller->method( 'view' )->willReturn( $viewBuilder ); - // Mock session manager - $reflection = new \ReflectionClass( get_parent_class( Tags::class ) ); - $sessionProperty = $reflection->getProperty( '_sessionManager' ); - $sessionProperty->setAccessible( true ); - - $sessionManager = $this->createMock( SessionManager::class ); - $sessionProperty->setValue( $controller, $sessionManager ); - $request = $this->createMock( Request::class ); $result = $controller->index( $request ); @@ -120,10 +119,17 @@ public function testCreateReturnsForm(): void $repository = $this->createMock( DatabaseTagRepository::class ); + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $mockSlugGenerator = $this->createMock( \Neuron\Cms\Services\SlugGenerator::class ); + $controller = $this->getMockBuilder( Tags::class ) ->setConstructorArgs([ null, - $repository + $repository, + $mockSlugGenerator, + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); @@ -138,14 +144,6 @@ public function testCreateReturnsForm(): void $controller->method( 'view' )->willReturn( $viewBuilder ); - // Mock session manager - $reflection = new \ReflectionClass( get_parent_class( Tags::class ) ); - $sessionProperty = $reflection->getProperty( '_sessionManager' ); - $sessionProperty->setAccessible( true ); - - $sessionManager = $this->createMock( SessionManager::class ); - $sessionProperty->setValue( $controller, $sessionManager ); - $request = $this->createMock( Request::class ); $result = $controller->create( $request ); @@ -164,9 +162,16 @@ public function testEditThrowsExceptionWhenTagNotFound(): void $repository = $this->createMock( DatabaseTagRepository::class ); $repository->method( 'findById' )->with( 999 )->willReturn( null ); + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $mockSlugGenerator = $this->createMock( \Neuron\Cms\Services\SlugGenerator::class ); + $controller = new Tags( null, - $repository + $repository, + $mockSlugGenerator, + $mockSettingManager, + $mockSessionManager ); $request = $this->createMock( Request::class ); diff --git a/tests/Unit/Cms/Controllers/UsersControllerTest.php b/tests/Unit/Cms/Controllers/UsersControllerTest.php index fad37fe..d2a59c6 100644 --- a/tests/Unit/Cms/Controllers/UsersControllerTest.php +++ b/tests/Unit/Cms/Controllers/UsersControllerTest.php @@ -87,13 +87,19 @@ public function testIndexReturnsAllUsers(): void $deleter = $this->createMock( IUserDeleter::class ); // Create controller with mocked dependencies + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $mockSessionManager->method( 'getFlash' )->willReturn( null ); + $controller = $this->getMockBuilder( Users::class ) ->setConstructorArgs([ $app, $repository, $creator, $updater, - $deleter + $deleter, + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); @@ -109,15 +115,6 @@ public function testIndexReturnsAllUsers(): void $controller->method( 'view' )->willReturn( $viewBuilder ); - // Mock session manager - $reflection = new \ReflectionClass( get_parent_class( Users::class ) ); - $sessionProperty = $reflection->getProperty( '_sessionManager' ); - $sessionProperty->setAccessible( true ); - - $sessionManager = $this->createMock( SessionManager::class ); - $sessionManager->method( 'getFlash' )->willReturn( null ); - $sessionProperty->setValue( $controller, $sessionManager ); - $request = $this->createMock( Request::class ); $result = $controller->index( $request ); @@ -141,13 +138,18 @@ public function testCreateReturnsForm(): void $updater = $this->createMock( IUserUpdater::class ); $deleter = $this->createMock( IUserDeleter::class ); + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $controller = $this->getMockBuilder( Users::class ) ->setConstructorArgs([ $app, $repository, $creator, $updater, - $deleter + $deleter, + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); @@ -163,14 +165,6 @@ public function testCreateReturnsForm(): void $controller->method( 'view' )->willReturn( $viewBuilder ); - // Mock session manager - $reflection = new \ReflectionClass( get_parent_class( Users::class ) ); - $sessionProperty = $reflection->getProperty( '_sessionManager' ); - $sessionProperty->setAccessible( true ); - - $sessionManager = $this->createMock( SessionManager::class ); - $sessionProperty->setValue( $controller, $sessionManager ); - $request = $this->createMock( Request::class ); $result = $controller->create( $request ); @@ -200,13 +194,18 @@ public function testEditReturnsFormForValidUser(): void $updater = $this->createMock( IUserUpdater::class ); $deleter = $this->createMock( IUserDeleter::class ); + $mockSettingManager = Registry::getInstance()->get( 'Settings' ); + $mockSessionManager = $this->createMock( SessionManager::class ); + $controller = $this->getMockBuilder( Users::class ) ->setConstructorArgs([ $app, $repository, $creator, $updater, - $deleter + $deleter, + $mockSettingManager, + $mockSessionManager ]) ->onlyMethods( ['view'] ) ->getMock(); @@ -222,14 +221,6 @@ public function testEditReturnsFormForValidUser(): void $controller->method( 'view' )->willReturn( $viewBuilder ); - // Mock session manager - $reflection = new \ReflectionClass( get_parent_class( Users::class ) ); - $sessionProperty = $reflection->getProperty( '_sessionManager' ); - $sessionProperty->setAccessible( true ); - - $sessionManager = $this->createMock( SessionManager::class ); - $sessionProperty->setValue( $controller, $sessionManager ); - $request = $this->createMock( Request::class ); $request->method( 'getRouteParameter' )->with( 'id' )->willReturn( '2' ); diff --git a/tests/Unit/Cms/Services/Category/CreatorTest.php b/tests/Unit/Cms/Services/Category/CreatorTest.php index 48df159..825bb71 100644 --- a/tests/Unit/Cms/Services/Category/CreatorTest.php +++ b/tests/Unit/Cms/Services/Category/CreatorTest.php @@ -155,4 +155,38 @@ public function testAllowsEmptyDescription(): void $this->assertEquals( '', $result->getDescription() ); } + + public function testConstructorSetsPropertiesCorrectly(): void + { + $categoryRepository = $this->createMock( ICategoryRepository::class ); + + $creator = new Creator( $categoryRepository ); + + $this->assertInstanceOf( Creator::class, $creator ); + } + + public function testConstructorWithEventEmitter(): void + { + $categoryRepository = $this->createMock( ICategoryRepository::class ); + $eventEmitter = $this->createMock( \Neuron\Events\Emitter::class ); + + $categoryRepository + ->method( 'create' ) + ->willReturnArgument( 0 ); + + // Event emitter should emit CategoryCreatedEvent + $eventEmitter + ->expects( $this->once() ) + ->method( 'emit' ) + ->with( $this->isInstanceOf( \Neuron\Cms\Events\CategoryCreatedEvent::class ) ); + + $creator = new Creator( $categoryRepository, null, $eventEmitter ); + + $dto = $this->createDto( + name: 'Technology', + slug: 'technology' + ); + + $creator->create( $dto ); + } } diff --git a/tests/Unit/Cms/Services/Category/DeleterTest.php b/tests/Unit/Cms/Services/Category/DeleterTest.php index 0f20732..6d6c685 100644 --- a/tests/Unit/Cms/Services/Category/DeleterTest.php +++ b/tests/Unit/Cms/Services/Category/DeleterTest.php @@ -80,4 +80,40 @@ public function testThrowsExceptionWhenCategoryNotFound(): void $this->_deleter->delete( 99 ); } + + public function testConstructorSetsPropertiesCorrectly(): void + { + $categoryRepository = $this->createMock( ICategoryRepository::class ); + + $deleter = new Deleter( $categoryRepository ); + + $this->assertInstanceOf( Deleter::class, $deleter ); + } + + public function testConstructorWithEventEmitter(): void + { + $categoryRepository = $this->createMock( ICategoryRepository::class ); + $eventEmitter = $this->createMock( \Neuron\Events\Emitter::class ); + + $category = new Category(); + $category->setId( 1 ); + + $categoryRepository + ->method( 'findById' ) + ->willReturn( $category ); + + $categoryRepository + ->method( 'delete' ) + ->willReturn( true ); + + // Event emitter should emit CategoryDeletedEvent + $eventEmitter + ->expects( $this->once() ) + ->method( 'emit' ) + ->with( $this->isInstanceOf( \Neuron\Cms\Events\CategoryDeletedEvent::class ) ); + + $deleter = new Deleter( $categoryRepository, $eventEmitter ); + + $deleter->delete( 1 ); + } } diff --git a/tests/Unit/Cms/Services/Category/UpdaterTest.php b/tests/Unit/Cms/Services/Category/UpdaterTest.php index c7815e0..4db37b5 100644 --- a/tests/Unit/Cms/Services/Category/UpdaterTest.php +++ b/tests/Unit/Cms/Services/Category/UpdaterTest.php @@ -204,4 +204,66 @@ public function testAllowsEmptyDescription(): void $this->assertEquals( '', $result->getDescription() ); } + + public function testConstructorSetsPropertiesCorrectly(): void + { + $categoryRepository = $this->createMock( ICategoryRepository::class ); + + $updater = new Updater( $categoryRepository ); + + $this->assertInstanceOf( Updater::class, $updater ); + } + + public function testConstructorWithEventEmitter(): void + { + $categoryRepository = $this->createMock( ICategoryRepository::class ); + $eventEmitter = $this->createMock( \Neuron\Events\Emitter::class ); + + $category = new Category(); + $category->setId( 1 ); + $category->setName( 'Test' ); + + $categoryRepository + ->method( 'findById' ) + ->willReturn( $category ); + + $categoryRepository + ->method( 'update' ); + + // Event emitter should emit CategoryUpdatedEvent + $eventEmitter + ->expects( $this->once() ) + ->method( 'emit' ) + ->with( $this->isInstanceOf( \Neuron\Cms\Events\CategoryUpdatedEvent::class ) ); + + $updater = new Updater( $categoryRepository, null, $eventEmitter ); + + $dto = $this->createDto( + id: 1, + name: 'Updated Name', + slug: 'updated-slug' + ); + + $updater->update( $dto ); + } + + public function testThrowsExceptionWhenCategoryNotFound(): void + { + $this->_mockCategoryRepository + ->expects( $this->once() ) + ->method( 'findById' ) + ->with( 999 ) + ->willReturn( null ); + + $this->expectException( \Exception::class ); + $this->expectExceptionMessage( 'Category with ID 999 not found' ); + + $dto = $this->createDto( + id: 999, + name: 'Test', + slug: 'test' + ); + + $this->_updater->update( $dto ); + } } diff --git a/tests/Unit/Cms/Services/EmailVerifierTest.php b/tests/Unit/Cms/Services/EmailVerifierTest.php index dacde4d..bc89de3 100644 --- a/tests/Unit/Cms/Services/EmailVerifierTest.php +++ b/tests/Unit/Cms/Services/EmailVerifierTest.php @@ -342,4 +342,70 @@ public function testCleanupExpiredTokens(): void $this->assertEquals( 5, $result ); } + + public function testVerifyEmailWhenUserNotFound(): void + { + $plainToken = bin2hex( random_bytes( 32 ) ); + $hashedToken = hash( 'sha256', $plainToken ); + $userId = 999; + + // Create mock token + $token = new EmailVerificationToken( $userId, $hashedToken, 60 ); + + // Repository finds token + $this->_tokenRepository + ->expects( $this->once() ) + ->method( 'findByToken' ) + ->with( $hashedToken ) + ->willReturn( $token ); + + // Repository doesn't find user (user was deleted) + $this->_userRepository + ->expects( $this->once() ) + ->method( 'findById' ) + ->with( $userId ) + ->willReturn( null ); + + $result = $this->_manager->verifyEmail( $plainToken ); + + $this->assertFalse( $result ); + } + + public function testConstructorWithCustomRandom(): void + { + $mockRandom = $this->createMock( \Neuron\Core\System\IRandom::class ); + + $mockRandom + ->expects( $this->once() ) + ->method( 'string' ) + ->with( 64, 'hex' ) + ->willReturn( str_repeat( 'a', 64 ) ); + + $user = new User(); + $user->setId( 1 ); + $user->setEmail( 'test@example.com' ); + $user->setUsername( 'testuser' ); + + $tokenRepository = $this->createMock( \Neuron\Cms\Repositories\IEmailVerificationTokenRepository::class ); + $userRepository = $this->createMock( \Neuron\Cms\Repositories\IUserRepository::class ); + + $tokenRepository + ->method( 'deleteByUserId' ); + + $tokenRepository + ->expects( $this->once() ) + ->method( 'create' ) + ->with( $this->isInstanceOf( EmailVerificationToken::class ) ); + + $manager = new EmailVerifier( + $tokenRepository, + $userRepository, + $this->_settings, + $this->_basePath, + $this->_verificationUrl, + $mockRandom + ); + + $manager->sendVerificationEmail( $user ); + } } diff --git a/tests/Unit/Cms/Services/Event/UpdaterTest.php b/tests/Unit/Cms/Services/Event/UpdaterTest.php index dfedfc6..ec6d569 100644 --- a/tests/Unit/Cms/Services/Event/UpdaterTest.php +++ b/tests/Unit/Cms/Services/Event/UpdaterTest.php @@ -412,4 +412,34 @@ public function test_update_changes_status_from_draft_to_published(): void $this->assertEquals( Event::STATUS_PUBLISHED, $event->getStatus() ); } + + public function test_constructor_sets_properties_correctly(): void + { + $eventRepository = $this->createMock( IEventRepository::class ); + $categoryRepository = $this->createMock( IEventCategoryRepository::class ); + + $updater = new Updater( $eventRepository, $categoryRepository ); + + $this->assertInstanceOf( Updater::class, $updater ); + } + + public function test_update_throws_exception_when_event_not_found(): void + { + $this->eventRepository->expects( $this->once() ) + ->method( 'findById' ) + ->with( 999 ) + ->willReturn( null ); + + $this->expectException( \RuntimeException::class ); + $this->expectExceptionMessage( 'Event with ID 999 not found' ); + + $dto = $this->createDto( + id: 999, + title: 'New Title', + startDate: '2025-07-01 14:00:00', + status: Event::STATUS_PUBLISHED + ); + + $this->updater->update( $dto ); + } } diff --git a/tests/Unit/Cms/Services/EventCategory/UpdaterTest.php b/tests/Unit/Cms/Services/EventCategory/UpdaterTest.php index f82b1b7..1e710fe 100644 --- a/tests/Unit/Cms/Services/EventCategory/UpdaterTest.php +++ b/tests/Unit/Cms/Services/EventCategory/UpdaterTest.php @@ -280,4 +280,33 @@ public function test_update_calls_repository_update(): void $this->updater->update( $dto ); } + + public function test_constructor_sets_properties_correctly(): void + { + $categoryRepository = $this->createMock( IEventCategoryRepository::class ); + + $updater = new Updater( $categoryRepository ); + + $this->assertInstanceOf( Updater::class, $updater ); + } + + public function test_update_throws_exception_when_category_not_found(): void + { + $this->categoryRepository->expects( $this->once() ) + ->method( 'findById' ) + ->with( 999 ) + ->willReturn( null ); + + $this->expectException( \RuntimeException::class ); + $this->expectExceptionMessage( 'Category with ID 999 not found' ); + + $dto = $this->createDto( + id: 999, + name: 'Test', + slug: 'test', + color: '#000000' + ); + + $this->updater->update( $dto ); + } } diff --git a/tests/Unit/Cms/Services/Media/CloudinaryUploaderTest.php b/tests/Unit/Cms/Services/Media/CloudinaryUploaderTest.php index 534b0e6..9e48848 100644 --- a/tests/Unit/Cms/Services/Media/CloudinaryUploaderTest.php +++ b/tests/Unit/Cms/Services/Media/CloudinaryUploaderTest.php @@ -396,4 +396,485 @@ public function testListResourcesWithPagination(): void $this->assertNull( $firstPage['next_cursor'] ); } } + + /** + * Unit tests using mocked Cloudinary SDK to test logic without real API calls + */ + + public function testUploadCallsCloudinaryApiCorrectly(): void + { + // Create a temporary test file + $testFile = tempnam( sys_get_temp_dir(), 'test_image_' ); + file_put_contents( $testFile, 'test image content' ); + + try + { + $uploader = new CloudinaryUploader( $this->_settings ); + + // Mock the Cloudinary instance + $mockCloudinary = $this->createMock( \Cloudinary\Cloudinary::class ); + $mockUploadApi = $this->createMock( \Cloudinary\Api\Upload\UploadApi::class ); + + // Set up the mock expectations + $mockCloudinary + ->expects( $this->once() ) + ->method( 'uploadApi' ) + ->willReturn( $mockUploadApi ); + + $mockUploadApi + ->expects( $this->once() ) + ->method( 'upload' ) + ->with( + $this->equalTo( $testFile ), + $this->callback( function( $options ) { + return isset( $options['folder'] ) && + $options['folder'] === 'test-folder' && + isset( $options['resource_type'] ) && + $options['resource_type'] === 'image'; + } ) + ) + ->willReturn( [ + 'secure_url' => 'https://res.cloudinary.com/test/image.jpg', + 'public_id' => 'test-folder/image', + 'width' => 800, + 'height' => 600, + 'format' => 'jpg', + 'bytes' => 12345, + 'resource_type' => 'image', + 'created_at' => '2024-01-01T00:00:00Z' + ] ); + + // Use reflection to inject the mock + $reflection = new \ReflectionClass( $uploader ); + $property = $reflection->getProperty( '_cloudinary' ); + $property->setAccessible( true ); + $property->setValue( $uploader, $mockCloudinary ); + + // Test the upload + $result = $uploader->upload( $testFile ); + + // Verify the result format + $this->assertIsArray( $result ); + $this->assertEquals( 'https://res.cloudinary.com/test/image.jpg', $result['url'] ); + $this->assertEquals( 'test-folder/image', $result['public_id'] ); + $this->assertEquals( 800, $result['width'] ); + $this->assertEquals( 600, $result['height'] ); + $this->assertEquals( 'jpg', $result['format'] ); + $this->assertEquals( 12345, $result['bytes'] ); + } + finally + { + // Clean up + if( file_exists( $testFile ) ) + { + unlink( $testFile ); + } + } + } + + public function testUploadWithCustomOptions(): void + { + // Create a temporary test file + $testFile = tempnam( sys_get_temp_dir(), 'test_image_' ); + file_put_contents( $testFile, 'test image content' ); + + try + { + $uploader = new CloudinaryUploader( $this->_settings ); + + // Mock the Cloudinary instance + $mockCloudinary = $this->createMock( \Cloudinary\Cloudinary::class ); + $mockUploadApi = $this->createMock( \Cloudinary\Api\Upload\UploadApi::class ); + + $mockCloudinary + ->method( 'uploadApi' ) + ->willReturn( $mockUploadApi ); + + $mockUploadApi + ->expects( $this->once() ) + ->method( 'upload' ) + ->with( + $this->equalTo( $testFile ), + $this->callback( function( $options ) { + return $options['folder'] === 'custom-folder' && + $options['public_id'] === 'my-image' && + isset( $options['tags'] ) && + in_array( 'test-tag', $options['tags'] ); + } ) + ) + ->willReturn( [ + 'secure_url' => 'https://res.cloudinary.com/test/custom-folder/my-image.jpg', + 'public_id' => 'custom-folder/my-image', + 'width' => 1024, + 'height' => 768, + 'format' => 'jpg' + ] ); + + // Use reflection to inject the mock + $reflection = new \ReflectionClass( $uploader ); + $property = $reflection->getProperty( '_cloudinary' ); + $property->setAccessible( true ); + $property->setValue( $uploader, $mockCloudinary ); + + // Test upload with custom options + $result = $uploader->upload( $testFile, [ + 'folder' => 'custom-folder', + 'public_id' => 'my-image', + 'tags' => [ 'test-tag', 'another-tag' ] + ] ); + + $this->assertEquals( 'custom-folder/my-image', $result['public_id'] ); + } + finally + { + if( file_exists( $testFile ) ) + { + unlink( $testFile ); + } + } + } + + public function testUploadFromUrlCallsCloudinaryApiCorrectly(): void + { + $uploader = new CloudinaryUploader( $this->_settings ); + + // Mock the Cloudinary instance + $mockCloudinary = $this->createMock( \Cloudinary\Cloudinary::class ); + $mockUploadApi = $this->createMock( \Cloudinary\Api\Upload\UploadApi::class ); + + $mockCloudinary + ->expects( $this->once() ) + ->method( 'uploadApi' ) + ->willReturn( $mockUploadApi ); + + $mockUploadApi + ->expects( $this->once() ) + ->method( 'upload' ) + ->with( + $this->equalTo( 'https://example.com/image.jpg' ), + $this->callback( function( $options ) { + return isset( $options['folder'] ) && + $options['folder'] === 'test-folder'; + } ) + ) + ->willReturn( [ + 'secure_url' => 'https://res.cloudinary.com/test/image.jpg', + 'public_id' => 'test-folder/image', + 'width' => 800, + 'height' => 600, + 'format' => 'jpg' + ] ); + + // Use reflection to inject the mock + $reflection = new \ReflectionClass( $uploader ); + $property = $reflection->getProperty( '_cloudinary' ); + $property->setAccessible( true ); + $property->setValue( $uploader, $mockCloudinary ); + + // Test uploadFromUrl + $result = $uploader->uploadFromUrl( 'https://example.com/image.jpg' ); + + $this->assertIsArray( $result ); + $this->assertEquals( 'https://res.cloudinary.com/test/image.jpg', $result['url'] ); + $this->assertEquals( 'test-folder/image', $result['public_id'] ); + } + + public function testDeleteCallsCloudinaryApiCorrectly(): void + { + $uploader = new CloudinaryUploader( $this->_settings ); + + // Mock the Cloudinary instance + $mockCloudinary = $this->createMock( \Cloudinary\Cloudinary::class ); + $mockUploadApi = $this->createMock( \Cloudinary\Api\Upload\UploadApi::class ); + + $mockCloudinary + ->expects( $this->once() ) + ->method( 'uploadApi' ) + ->willReturn( $mockUploadApi ); + + $mockUploadApi + ->expects( $this->once() ) + ->method( 'destroy' ) + ->with( $this->equalTo( 'test-folder/image-to-delete' ) ) + ->willReturn( [ 'result' => 'ok' ] ); + + // Use reflection to inject the mock + $reflection = new \ReflectionClass( $uploader ); + $property = $reflection->getProperty( '_cloudinary' ); + $property->setAccessible( true ); + $property->setValue( $uploader, $mockCloudinary ); + + // Test delete + $result = $uploader->delete( 'test-folder/image-to-delete' ); + + $this->assertTrue( $result ); + } + + public function testDeleteReturnsFalseWhenResultIsNotOk(): void + { + $uploader = new CloudinaryUploader( $this->_settings ); + + // Mock the Cloudinary instance + $mockCloudinary = $this->createMock( \Cloudinary\Cloudinary::class ); + $mockUploadApi = $this->createMock( \Cloudinary\Api\Upload\UploadApi::class ); + + $mockCloudinary + ->method( 'uploadApi' ) + ->willReturn( $mockUploadApi ); + + $mockUploadApi + ->method( 'destroy' ) + ->willReturn( [ 'result' => 'not found' ] ); + + // Use reflection to inject the mock + $reflection = new \ReflectionClass( $uploader ); + $property = $reflection->getProperty( '_cloudinary' ); + $property->setAccessible( true ); + $property->setValue( $uploader, $mockCloudinary ); + + // Test delete with non-ok result + $result = $uploader->delete( 'nonexistent-image' ); + + $this->assertFalse( $result ); + } + + public function testListResourcesCallsCloudinaryApiCorrectly(): void + { + $uploader = new CloudinaryUploader( $this->_settings ); + + // Mock the Cloudinary instance + $mockCloudinary = $this->createMock( \Cloudinary\Cloudinary::class ); + $mockAdminApi = $this->createMock( \Cloudinary\Api\Admin\AdminApi::class ); + + $mockCloudinary + ->expects( $this->once() ) + ->method( 'adminApi' ) + ->willReturn( $mockAdminApi ); + + $mockAdminApi + ->expects( $this->once() ) + ->method( 'assets' ) + ->with( + $this->callback( function( $options ) { + return $options['type'] === 'upload' && + $options['prefix'] === 'test-folder' && + $options['max_results'] === 30 && + $options['resource_type'] === 'image'; + } ) + ) + ->willReturn( [ + 'resources' => [ + [ + 'secure_url' => 'https://res.cloudinary.com/test/image1.jpg', + 'public_id' => 'test-folder/image1', + 'width' => 800, + 'height' => 600, + 'format' => 'jpg', + 'bytes' => 12345, + 'resource_type' => 'image', + 'created_at' => '2024-01-01T00:00:00Z' + ], + [ + 'secure_url' => 'https://res.cloudinary.com/test/image2.jpg', + 'public_id' => 'test-folder/image2', + 'width' => 1024, + 'height' => 768, + 'format' => 'jpg', + 'bytes' => 23456, + 'resource_type' => 'image', + 'created_at' => '2024-01-02T00:00:00Z' + ] + ], + 'next_cursor' => 'abc123', + 'total_count' => 100 + ] ); + + // Use reflection to inject the mock + $reflection = new \ReflectionClass( $uploader ); + $property = $reflection->getProperty( '_cloudinary' ); + $property->setAccessible( true ); + $property->setValue( $uploader, $mockCloudinary ); + + // Test listResources + $result = $uploader->listResources(); + + $this->assertIsArray( $result ); + $this->assertArrayHasKey( 'resources', $result ); + $this->assertArrayHasKey( 'next_cursor', $result ); + $this->assertArrayHasKey( 'total_count', $result ); + $this->assertCount( 2, $result['resources'] ); + $this->assertEquals( 'abc123', $result['next_cursor'] ); + $this->assertEquals( 100, $result['total_count'] ); + $this->assertEquals( 'test-folder/image1', $result['resources'][0]['public_id'] ); + } + + public function testListResourcesWithCustomOptionsAndPagination(): void + { + $uploader = new CloudinaryUploader( $this->_settings ); + + // Mock the Cloudinary instance + $mockCloudinary = $this->createMock( \Cloudinary\Cloudinary::class ); + $mockAdminApi = $this->createMock( \Cloudinary\Api\Admin\AdminApi::class ); + + $mockCloudinary + ->method( 'adminApi' ) + ->willReturn( $mockAdminApi ); + + $mockAdminApi + ->expects( $this->once() ) + ->method( 'assets' ) + ->with( + $this->callback( function( $options ) { + return $options['max_results'] === 10 && + $options['next_cursor'] === 'cursor123' && + $options['prefix'] === 'custom-folder'; + } ) + ) + ->willReturn( [ + 'resources' => [], + 'next_cursor' => null, + 'total_count' => 0 + ] ); + + // Use reflection to inject the mock + $reflection = new \ReflectionClass( $uploader ); + $property = $reflection->getProperty( '_cloudinary' ); + $property->setAccessible( true ); + $property->setValue( $uploader, $mockCloudinary ); + + // Test listResources with custom options + $result = $uploader->listResources( [ + 'max_results' => 10, + 'next_cursor' => 'cursor123', + 'folder' => 'custom-folder' + ] ); + + $this->assertIsArray( $result ); + $this->assertNull( $result['next_cursor'] ); + } + + public function testUploadThrowsExceptionOnCloudinaryError(): void + { + $testFile = tempnam( sys_get_temp_dir(), 'test_image_' ); + file_put_contents( $testFile, 'test content' ); + + try + { + $uploader = new CloudinaryUploader( $this->_settings ); + + // Mock the Cloudinary instance + $mockCloudinary = $this->createMock( \Cloudinary\Cloudinary::class ); + $mockUploadApi = $this->createMock( \Cloudinary\Api\Upload\UploadApi::class ); + + $mockCloudinary + ->method( 'uploadApi' ) + ->willReturn( $mockUploadApi ); + + $mockUploadApi + ->method( 'upload' ) + ->willThrowException( new \Exception( 'Cloudinary API error' ) ); + + // Use reflection to inject the mock + $reflection = new \ReflectionClass( $uploader ); + $property = $reflection->getProperty( '_cloudinary' ); + $property->setAccessible( true ); + $property->setValue( $uploader, $mockCloudinary ); + + $this->expectException( \Exception::class ); + $this->expectExceptionMessage( 'Cloudinary upload failed' ); + + $uploader->upload( $testFile ); + } + finally + { + if( file_exists( $testFile ) ) + { + unlink( $testFile ); + } + } + } + + public function testUploadFromUrlThrowsExceptionOnCloudinaryError(): void + { + $uploader = new CloudinaryUploader( $this->_settings ); + + // Mock the Cloudinary instance + $mockCloudinary = $this->createMock( \Cloudinary\Cloudinary::class ); + $mockUploadApi = $this->createMock( \Cloudinary\Api\Upload\UploadApi::class ); + + $mockCloudinary + ->method( 'uploadApi' ) + ->willReturn( $mockUploadApi ); + + $mockUploadApi + ->method( 'upload' ) + ->willThrowException( new \Exception( 'Invalid image format' ) ); + + // Use reflection to inject the mock + $reflection = new \ReflectionClass( $uploader ); + $property = $reflection->getProperty( '_cloudinary' ); + $property->setAccessible( true ); + $property->setValue( $uploader, $mockCloudinary ); + + $this->expectException( \Exception::class ); + $this->expectExceptionMessage( 'Cloudinary upload from URL failed' ); + + $uploader->uploadFromUrl( 'https://example.com/image.jpg' ); + } + + public function testDeleteThrowsExceptionOnCloudinaryError(): void + { + $uploader = new CloudinaryUploader( $this->_settings ); + + // Mock the Cloudinary instance + $mockCloudinary = $this->createMock( \Cloudinary\Cloudinary::class ); + $mockUploadApi = $this->createMock( \Cloudinary\Api\Upload\UploadApi::class ); + + $mockCloudinary + ->method( 'uploadApi' ) + ->willReturn( $mockUploadApi ); + + $mockUploadApi + ->method( 'destroy' ) + ->willThrowException( new \Exception( 'API error' ) ); + + // Use reflection to inject the mock + $reflection = new \ReflectionClass( $uploader ); + $property = $reflection->getProperty( '_cloudinary' ); + $property->setAccessible( true ); + $property->setValue( $uploader, $mockCloudinary ); + + $this->expectException( \Exception::class ); + $this->expectExceptionMessage( 'Cloudinary deletion failed' ); + + $uploader->delete( 'test-image' ); + } + + public function testListResourcesThrowsExceptionOnCloudinaryError(): void + { + $uploader = new CloudinaryUploader( $this->_settings ); + + // Mock the Cloudinary instance + $mockCloudinary = $this->createMock( \Cloudinary\Cloudinary::class ); + $mockAdminApi = $this->createMock( \Cloudinary\Api\Admin\AdminApi::class ); + + $mockCloudinary + ->method( 'adminApi' ) + ->willReturn( $mockAdminApi ); + + $mockAdminApi + ->method( 'assets' ) + ->willThrowException( new \Exception( 'Authentication failed' ) ); + + // Use reflection to inject the mock + $reflection = new \ReflectionClass( $uploader ); + $property = $reflection->getProperty( '_cloudinary' ); + $property->setAccessible( true ); + $property->setValue( $uploader, $mockCloudinary ); + + $this->expectException( \Exception::class ); + $this->expectExceptionMessage( 'Cloudinary list resources failed' ); + + $uploader->listResources(); + } } diff --git a/tests/Unit/Cms/Services/Page/UpdaterTest.php b/tests/Unit/Cms/Services/Page/UpdaterTest.php index f100958..97b1c21 100644 --- a/tests/Unit/Cms/Services/Page/UpdaterTest.php +++ b/tests/Unit/Cms/Services/Page/UpdaterTest.php @@ -264,4 +264,37 @@ public function testUpdateWhenRepositoryFails(): void $this->assertInstanceOf( Page::class, $result ); } + + public function testConstructorSetsPropertiesCorrectly(): void + { + $repository = $this->createMock( IPageRepository::class ); + + $updater = new Updater( $repository ); + + $this->assertInstanceOf( Updater::class, $updater ); + } + + public function testUpdateThrowsExceptionWhenPageNotFound(): void + { + $repository = $this->createMock( IPageRepository::class ); + $updater = new Updater( $repository ); + + $repository + ->expects( $this->once() ) + ->method( 'findById' ) + ->with( 999 ) + ->willReturn( null ); + + $this->expectException( \Exception::class ); + $this->expectExceptionMessage( 'Page with ID 999 not found' ); + + $dto = $this->createDto( + id: 999, + title: 'Title', + content: '{}', + status: Page::STATUS_DRAFT + ); + + $updater->update( $dto ); + } } diff --git a/tests/Unit/Cms/Services/PasswordResetterTest.php b/tests/Unit/Cms/Services/PasswordResetterTest.php index c29cee8..fb93135 100644 --- a/tests/Unit/Cms/Services/PasswordResetterTest.php +++ b/tests/Unit/Cms/Services/PasswordResetterTest.php @@ -273,4 +273,70 @@ public function testCleanupExpiredTokens(): void $this->assertEquals( 5, $result ); } + + public function testResetPasswordWhenUserNotFound(): void + { + $plainToken = bin2hex( random_bytes( 32 ) ); + $hashedToken = hash( 'sha256', $plainToken ); + $email = 'deleted@example.com'; + + // Create mock token + $token = new PasswordResetToken( $email, $hashedToken, 60 ); + + // Repository finds token + $this->_tokenRepository + ->expects( $this->once() ) + ->method( 'findByToken' ) + ->with( $hashedToken ) + ->willReturn( $token ); + + // User repository doesn't find user (user was deleted) + $this->_userRepository + ->expects( $this->once() ) + ->method( 'findByEmail' ) + ->with( $email ) + ->willReturn( null ); + + $result = $this->_manager->resetPassword( $plainToken, 'NewPassword123!' ); + + $this->assertFalse( $result ); + } + + public function testConstructorWithCustomRandom(): void + { + $mockRandom = $this->createMock( \Neuron\Core\System\IRandom::class ); + + $mockRandom + ->expects( $this->once() ) + ->method( 'string' ) + ->with( 64, 'hex' ) + ->willReturn( str_repeat( 'a', 64 ) ); + + $user = new User(); + $user->setEmail( 'test@example.com' ); + + $this->_userRepository + ->method( 'findByEmail' ) + ->willReturn( $user ); + + $this->_tokenRepository + ->method( 'deleteByEmail' ); + + $this->_tokenRepository + ->expects( $this->once() ) + ->method( 'create' ) + ->with( $this->isInstanceOf( PasswordResetToken::class ) ); + + $manager = new PasswordResetter( + $this->_tokenRepository, + $this->_userRepository, + $this->_passwordHasher, + $this->_settings, + $this->_basePath, + $this->_resetUrl, + $mockRandom + ); + + $manager->requestReset( 'test@example.com' ); + } } diff --git a/tests/Unit/Cms/Auth/ResendVerificationThrottleTest.php b/tests/Unit/Cms/Services/Security/ResendVerificationThrottleTest.php similarity index 98% rename from tests/Unit/Cms/Auth/ResendVerificationThrottleTest.php rename to tests/Unit/Cms/Services/Security/ResendVerificationThrottleTest.php index 5a04744..e717c6d 100644 --- a/tests/Unit/Cms/Auth/ResendVerificationThrottleTest.php +++ b/tests/Unit/Cms/Services/Security/ResendVerificationThrottleTest.php @@ -1,15 +1,15 @@ assertTrue( $result->isEmailVerified() ); } + + public function testConstructorSetsPropertiesCorrectly(): void + { + $userRepository = $this->createMock( IUserRepository::class ); + $passwordHasher = $this->createMock( PasswordHasher::class ); + + $creator = new Creator( $userRepository, $passwordHasher ); + + $this->assertInstanceOf( Creator::class, $creator ); + } + + public function testConstructorWithEventEmitter(): void + { + $userRepository = $this->createMock( IUserRepository::class ); + $passwordHasher = $this->createMock( PasswordHasher::class ); + $eventEmitter = $this->createMock( \Neuron\Events\Emitter::class ); + + $passwordHasher + ->method( 'meetsRequirements' ) + ->willReturn( true ); + + $passwordHasher + ->method( 'hash' ) + ->willReturn( 'hashed_password' ); + + $userRepository + ->method( 'create' ) + ->willReturnArgument( 0 ); + + // Event emitter should emit UserCreatedEvent + $eventEmitter + ->expects( $this->once() ) + ->method( 'emit' ) + ->with( $this->isInstanceOf( \Neuron\Cms\Events\UserCreatedEvent::class ) ); + + $creator = new Creator( $userRepository, $passwordHasher, $eventEmitter ); + + $dto = $this->createDto( + 'testuser', + 'test@example.com', + 'Password123!', + User::ROLE_SUBSCRIBER + ); + + $creator->create( $dto ); + } } diff --git a/tests/Unit/Cms/Services/User/DeleterTest.php b/tests/Unit/Cms/Services/User/DeleterTest.php index b339cb5..04cb3f3 100644 --- a/tests/Unit/Cms/Services/User/DeleterTest.php +++ b/tests/Unit/Cms/Services/User/DeleterTest.php @@ -80,4 +80,40 @@ public function testThrowsExceptionWhenUserNotFound(): void $this->_deleter->delete( 99 ); } + + public function testConstructorSetsPropertiesCorrectly(): void + { + $userRepository = $this->createMock( IUserRepository::class ); + + $deleter = new Deleter( $userRepository ); + + $this->assertInstanceOf( Deleter::class, $deleter ); + } + + public function testConstructorWithEventEmitter(): void + { + $userRepository = $this->createMock( IUserRepository::class ); + $eventEmitter = $this->createMock( \Neuron\Events\Emitter::class ); + + $user = new User(); + $user->setId( 1 ); + + $userRepository + ->method( 'findById' ) + ->willReturn( $user ); + + $userRepository + ->method( 'delete' ) + ->willReturn( true ); + + // Event emitter should emit UserDeletedEvent + $eventEmitter + ->expects( $this->once() ) + ->method( 'emit' ) + ->with( $this->isInstanceOf( \Neuron\Cms\Events\UserDeletedEvent::class ) ); + + $deleter = new Deleter( $userRepository, $eventEmitter ); + + $deleter->delete( 1 ); + } } diff --git a/tests/Unit/Cms/Services/User/UpdaterTest.php b/tests/Unit/Cms/Services/User/UpdaterTest.php index 787ef41..db1dff0 100644 --- a/tests/Unit/Cms/Services/User/UpdaterTest.php +++ b/tests/Unit/Cms/Services/User/UpdaterTest.php @@ -251,4 +251,73 @@ public function testUpdatesRole(): void $this->assertEquals( User::ROLE_ADMIN, $result->getRole() ); } + + public function testConstructorSetsPropertiesCorrectly(): void + { + $userRepository = $this->createMock( IUserRepository::class ); + $passwordHasher = $this->createMock( PasswordHasher::class ); + + $updater = new Updater( $userRepository, $passwordHasher ); + + $this->assertInstanceOf( Updater::class, $updater ); + } + + public function testConstructorWithEventEmitter(): void + { + $userRepository = $this->createMock( IUserRepository::class ); + $passwordHasher = $this->createMock( PasswordHasher::class ); + $eventEmitter = $this->createMock( \Neuron\Events\Emitter::class ); + + $user = new User(); + $user->setId( 1 ); + $user->setUsername( 'testuser' ); + $user->setEmail( 'test@example.com' ); + $user->setRole( User::ROLE_SUBSCRIBER ); + $user->setPasswordHash( 'existing_hash' ); + + $userRepository + ->method( 'findById' ) + ->willReturn( $user ); + + $userRepository + ->method( 'update' ); + + // Event emitter should emit UserUpdatedEvent + $eventEmitter + ->expects( $this->once() ) + ->method( 'emit' ) + ->with( $this->isInstanceOf( \Neuron\Cms\Events\UserUpdatedEvent::class ) ); + + $updater = new Updater( $userRepository, $passwordHasher, $eventEmitter ); + + $dto = $this->createDto( + 1, + 'testuser', + 'test@example.com', + User::ROLE_SUBSCRIBER + ); + + $updater->update( $dto ); + } + + public function testThrowsExceptionWhenUserNotFound(): void + { + $this->_mockUserRepository + ->expects( $this->once() ) + ->method( 'findById' ) + ->with( 999 ) + ->willReturn( null ); + + $this->expectException( \Exception::class ); + $this->expectExceptionMessage( 'User with ID 999 not found' ); + + $dto = $this->createDto( + 999, + 'testuser', + 'test@example.com', + User::ROLE_SUBSCRIBER + ); + + $this->_updater->update( $dto ); + } } diff --git a/tests/Unit/Cms/Services/Widget/CalendarWidgetTest.php b/tests/Unit/Cms/Services/Widget/CalendarWidgetTest.php index c5b0111..07cb137 100644 --- a/tests/Unit/Cms/Services/Widget/CalendarWidgetTest.php +++ b/tests/Unit/Cms/Services/Widget/CalendarWidgetTest.php @@ -113,4 +113,242 @@ public function test_limit_defaults_to_5(): void $this->widget->render( [] ); } + + public function testGetNameReturnsCalendar(): void + { + $this->assertEquals( 'calendar', $this->widget->getName() ); + } + + public function testGetDescriptionReturnsString(): void + { + $description = $this->widget->getDescription(); + $this->assertIsString( $description ); + $this->assertNotEmpty( $description ); + $this->assertEquals( 'Display a list of calendar events', $description ); + } + + public function testGetAttributesReturnsExpectedStructure(): void + { + $attributes = $this->widget->getAttributes(); + + $this->assertIsArray( $attributes ); + $this->assertArrayHasKey( 'category', $attributes ); + $this->assertArrayHasKey( 'limit', $attributes ); + $this->assertArrayHasKey( 'upcoming', $attributes ); + + $this->assertIsString( $attributes['category'] ); + $this->assertIsString( $attributes['limit'] ); + $this->assertIsString( $attributes['upcoming'] ); + } + + public function testRenderWithNoEventsReturnsNoEventsMessage(): void + { + $this->eventRepository->expects( $this->once() ) + ->method( 'getUpcoming' ) + ->willReturn( [] ); + + $result = $this->widget->render( [] ); + + $this->assertStringContainsString( 'No events found', $result ); + $this->assertStringContainsString( 'calendar-widget', $result ); + } + + public function testRenderWithCategoryNotFoundReturnsComment(): void + { + $this->categoryRepository->expects( $this->once() ) + ->method( 'findBySlug' ) + ->with( 'nonexistent' ) + ->willReturn( null ); + + $result = $this->widget->render( [ 'category' => 'nonexistent' ] ); + + $this->assertStringContainsString( '', $result ); + } + + public function testRenderWithValidCategoryFiltersEvents(): void + { + $category = $this->createMock( \Neuron\Cms\Models\EventCategory::class ); + $category->method( 'getId' )->willReturn( 5 ); + + $this->categoryRepository->expects( $this->once() ) + ->method( 'findBySlug' ) + ->with( 'tech-events' ) + ->willReturn( $category ); + + $this->eventRepository->expects( $this->once() ) + ->method( 'getByCategory' ) + ->with( 5, 'published' ) + ->willReturn( [] ); + + $this->widget->render( [ 'category' => 'tech-events' ] ); + } + + public function testRenderWithCategoryRespectsLimit(): void + { + $category = $this->createMock( \Neuron\Cms\Models\EventCategory::class ); + $category->method( 'getId' )->willReturn( 5 ); + + $this->categoryRepository->expects( $this->once() ) + ->method( 'findBySlug' ) + ->with( 'tech-events' ) + ->willReturn( $category ); + + // Create more events than the limit + $events = []; + for( $i = 1; $i <= 10; $i++ ) + { + $event = $this->createMock( \Neuron\Cms\Models\Event::class ); + $event->method( 'getTitle' )->willReturn( "Event $i" ); + $event->method( 'getSlug' )->willReturn( "event-$i" ); + $event->method( 'getStartDate' )->willReturn( new \DateTimeImmutable( '2024-01-01' ) ); + $event->method( 'isAllDay' )->willReturn( true ); + $event->method( 'getLocation' )->willReturn( null ); + $events[] = $event; + } + + $this->eventRepository->expects( $this->once() ) + ->method( 'getByCategory' ) + ->with( 5, 'published' ) + ->willReturn( $events ); + + $result = $this->widget->render( [ 'category' => 'tech-events', 'limit' => 3 ] ); + + // Should only show 3 events + $this->assertStringContainsString( 'Event 1', $result ); + $this->assertStringContainsString( 'Event 2', $result ); + $this->assertStringContainsString( 'Event 3', $result ); + $this->assertStringNotContainsString( 'Event 4', $result ); + } + + public function testRenderWithEventsGeneratesHtmlStructure(): void + { + $event = $this->createMock( \Neuron\Cms\Models\Event::class ); + $event->method( 'getTitle' )->willReturn( 'Test Event' ); + $event->method( 'getSlug' )->willReturn( 'test-event' ); + $event->method( 'getStartDate' )->willReturn( new \DateTimeImmutable( '2024-01-15 14:30:00' ) ); + $event->method( 'isAllDay' )->willReturn( false ); + $event->method( 'getLocation' )->willReturn( 'Conference Room A' ); + + $this->eventRepository->expects( $this->once() ) + ->method( 'getUpcoming' ) + ->willReturn( [ $event ] ); + + $result = $this->widget->render( [] ); + + // Check HTML structure + $this->assertStringContainsString( '
', $result ); + $this->assertStringContainsString( '', $result ); + $this->assertStringContainsString( '
', $result ); + } + + public function testRenderWithAllDayEventDoesNotShowTime(): void + { + $event = $this->createMock( \Neuron\Cms\Models\Event::class ); + $event->method( 'getTitle' )->willReturn( 'All Day Event' ); + $event->method( 'getSlug' )->willReturn( 'all-day' ); + $event->method( 'getStartDate' )->willReturn( new \DateTimeImmutable( '2024-01-15 00:00:00' ) ); + $event->method( 'isAllDay' )->willReturn( true ); + $event->method( 'getLocation' )->willReturn( null ); + + $this->eventRepository->expects( $this->once() ) + ->method( 'getUpcoming' ) + ->willReturn( [ $event ] ); + + $result = $this->widget->render( [] ); + + $this->assertStringContainsString( 'January 15, 2024', $result ); + $this->assertStringNotContainsString( ' at ', $result ); + $this->assertStringNotContainsString( 'AM', $result ); + $this->assertStringNotContainsString( 'PM', $result ); + } + + public function testRenderWithEventWithoutLocationDoesNotShowLocationSpan(): void + { + $event = $this->createMock( \Neuron\Cms\Models\Event::class ); + $event->method( 'getTitle' )->willReturn( 'Online Event' ); + $event->method( 'getSlug' )->willReturn( 'online-event' ); + $event->method( 'getStartDate' )->willReturn( new \DateTimeImmutable( '2024-01-15' ) ); + $event->method( 'isAllDay' )->willReturn( true ); + $event->method( 'getLocation' )->willReturn( null ); + + $this->eventRepository->expects( $this->once() ) + ->method( 'getUpcoming' ) + ->willReturn( [ $event ] ); + + $result = $this->widget->render( [] ); + + $this->assertStringNotContainsString( '', $result ); + } + + public function testRenderEscapesHtmlInEventData(): void + { + $event = $this->createMock( \Neuron\Cms\Models\Event::class ); + $event->method( 'getTitle' )->willReturn( '' ); + $event->method( 'getSlug' )->willReturn( 'safe-slug' ); + $event->method( 'getStartDate' )->willReturn( new \DateTimeImmutable( '2024-01-15' ) ); + $event->method( 'isAllDay' )->willReturn( true ); + $event->method( 'getLocation' )->willReturn( '' ); + + $this->eventRepository->expects( $this->once() ) + ->method( 'getUpcoming' ) + ->willReturn( [ $event ] ); + + $result = $this->widget->render( [] ); + + // HTML should be escaped + $this->assertStringNotContainsString( '