Thanks for the info Samsonasik.
For the past 24 hours I’ve been tweaking and testing different approaches and decided to refactor the code entirely.
I migrated the code from onBootstrap
method into a Listener, and then in my test I override that Listener service in the same way as your example.
Rather than wrapping production code in actual logic (if (testing)) which was always going to be iffy practice, I thought I might as well do it right. My goal is to mock a users login to test the controllers views.
At the moment it looks like:
IndexControllerTest.php:
<?php
namespace ApplicationTest\Controller;
use Laminas\Authentication\AuthenticationService;
use Laminas\Stdlib\ArrayUtils;
use Laminas\Test\PHPUnit\Controller\AbstractHttpControllerTestCase;
/**
* Class IndexControllerTest
* @package ApplicationTest\Controller
*/
class IndexControllerTest extends AbstractHttpControllerTestCase {
public function setUp(): void
{
$configOverrides = [];
$this->setApplicationConfig(ArrayUtils::merge(
include __DIR__ . '/../../../../config/application.config.php',
$configOverrides
));
parent::setUp();
}
public function mockAuth()
{
$appSrvLoc = $this->getApplicationServiceLocator();
$mockAuth = $this->getMockBuilder(AuthenticationService::class)
->disableOriginalConstructor()
->getMock();
$mockAuth->expects($this->any())
->method('hasIdentity')
->willReturn(true);
$mockAuth->expects($this->any())
->method('getIdentity')
->willReturn(['id' => 1]);
$appSrvLoc->setAllowOverride(true);
$appSrvLoc->setService(AuthenticationService::class, $mockAuth);
$appSrvLoc->setAllowOverride(false);
}
public function testHomeCanBeAccessed()
{
$this->mockAuth();
$this->dispatch('/', 'GET');
$this->assertResponseStatusCode(200);
$this->assertModuleName('application');
$this->assertControllerName(IndexController::class);
$this->assertControllerClass('IndexController');
$this->assertMatchedRouteName('home');
}
}
And then I pass through AuthenticationService
in the constructor of my Listener:
SessionListener.php
<?php
namespace Application\Listener;
use Laminas\Authentication\AuthenticationService;
use Laminas\EventManager\Event;
use Laminas\EventManager\EventManagerInterface;
use Laminas\EventManager\ListenerAggregateInterface;
use Laminas\Http\Response;
use Laminas\Mvc\MvcEvent;
class SessionListener implements ListenerAggregateInterface {
/** @var AuthenticationService */
protected AuthenticationService $authService;
/** @var array */
protected array $listeners = [];
/**
* SessionListener constructor.
* @param AuthenticationService $authenticationService
*/
public function __construct(AuthenticationService $authenticationService)
{
$this->authService = $authenticationService;
}
/**
* @return AuthenticationService
*/
public function getAuthService(): AuthenticationService
{
return $this->authService;
}
/**
* @return array
*/
public function getListeners(): array
{
return $this->listeners;
}
/**
* Attach event to listener
*
* @param EventManagerInterface $events
* @param int $priority
*/
public function attach(EventManagerInterface $events, $priority = 1)
{
$events->attach(MvcEvent::EVENT_DISPATCH,
[$this, 'confirmSession'], 1
);
}
/**
* Confirm user has session
*
* @param Event $event
* @return bool|Response
*/
public function confirmSession(Event $event)
{
$controller = $event->getTarget();
if (!$this->getAuthService()->hasIdentity()) {
// Invalid user - redirect to the Login page.
return $controller->redirect()->toRoute('login');
}
return true;
}
/**
* Detach listeners
*
* @param EventManagerInterface $events
*/
public function detach(EventManagerInterface $events)
{
foreach ($this->listeners as $index => $listener) {
$events->detach($listener);
unset($this->listeners[$index]);
}
}
}
I’ve got a little more validation to add in i.e. route permission access but I much prefer this code method for code separation.
I then attach the Listener in module.config.php
like so:
<?php
namespace Application;
use Laminas\Authentication\AuthenticationService;
use Laminas\Router\Http\Literal;
use Laminas\Log\Logger;
use Laminas\Router\Http\Segment;
use Laminas\ServiceManager\Factory\InvokableFactory;
return [
'controllers' => [
'factories' => [
Controller\IndexController::class => Controller\Factory\IndexControllerFactory::class
],
],
'listeners' => [
Listener\SessionListener::class
],
'service_manager' => [
'factories' => [
AuthenticationService::class => InvokableFactory::class,
Listener\SessionListener::class => Listener\Factory\SessionListenerFactory::class
]
]
// ...
];
Current Issue
Hijacking my own thread here but weirdly, this approach was working earlier, however I just ran my tests again and now it’s failing. It looks like my mock AuthenticationService is no longer being fired and hasIdentity() is false, forcing a redirect…
Failed asserting response code "200", actual status code is "302"
Is there any better practice for this situation that’s more consistent or am I building these mocks incorrectly? I’ve got a similar mock for a curl wrapper to fake an API response which is still working fine.