Laravel Unit Testing - add cookie to request? - phpunit

I want to send a cookie with json POST:
public function testAccessCookie()
{
$response = $this->json('POST', route('publications'))->withCookie(Cookie::create('test'));
//some asserts
}
publications route has some middleware:
public function handle($request, Closure $next)
{
Log::debug('cookie', [$request->cookies]);
//cookie validation
return $next($request);
}
But while running testAccessCookie(), there is [null] inside log. No cookies attached.
What's wrong?
There is no such problem with real (in-browser) requests.

You can add cookies to calls in tests:
$cookies = ['test' => 'value'];
$response = $this->call('POST', route('publications'), [], $cookies);
See https://laravel.com/api/5.4/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.html#method_call
However you will run into a cookie encryption problem. You can temporarily disable cookies during testing with:
use Illuminate\Cookie\Middleware\EncryptCookies;
/**
* #param array|string $cookies
* #return $this
*/
protected function disableCookiesEncryption($name)
{
$this->app->resolving(EncryptCookies::class,
function ($object) use ($name)
{
$object->disableFor($name);
});
return $this;
}
Adding $this->disableCookiesEncryption('test'); at the start of the test.
You may need to add headers to specify a json response.

This should work in recent versions (Laravel 6):
Either:
$this->disableCookieEncryption();
or:
$cookies = ['test' => encrypt('value', false)];
$response = $this->call('POST', route('publications'), [], $cookies);

Since Laravel 5.2 you get the \App\Http\Middleware\EncryptCookies::class middleware defined by default in the web middleware group and it will set all unencrypted cookies to null.
Unfortunately all cookies you send with $request->call(), $request->get() and $request->post() in unit testing are usually unencrypted and nothing in the official documentation tells you they need to be encrypted.
If you don't want to call $request->disableCookieEncryption() everytime, as a permanent solution you can simply redefine the isDisabled() method in App\Http\Middleware\EncryptCookies.php to ignore cookies encryption during unit testing.
Here is the implementation I made for Laravel 6.x, it should work on earlier versions too.
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* #var array
*/
protected $except = [
//
];
public function isDisabled($name)
{
if (app()->runningUnitTests()) {
return true; // Disable cookies encryption/decryption during unit testing
}
return parent::isDisabled($name);
}
}

Related

Drupal 8 Class 'Drupal\Core\Session\AccountInterface' not found

I am trying to write a custom php script in my Drupal site root that checks if the user is logged in. To check this I import bootstrap.inc. However when I do this it throws me this error
This is the code of the php script in my site root:
<?php
require_once './core/includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
global $user;
var_dump($user->uid);
?>
Anyone has a solution to this?
To bootstrap Drupal 8, you need different code. Drupal 8 doesn't have any drupal_bootstrap() function, so the code you are using would throw a PHP error.
You can use authorize.php as guideline to write your own script.
use Drupal\Core\DrupalKernel;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$autoloader = (require_once 'autoload.php');
try {
$request = Request::createFromGlobals();
$kernel = DrupalKernel::createFromRequest($request, $autoloader, 'prod');
$kernel->prepareLegacyRequest($request);
} catch (HttpExceptionInterface $e) {
$response = new Response('', $e->getStatusCode());
$response
->prepare($request)
->send();
exit;
}
\Drupal::moduleHandler()
->addModule('system', 'core/modules/system');
\Drupal::moduleHandler()
->addModule('user', 'core/modules/user');
\Drupal::moduleHandler()
->load('system');
\Drupal::moduleHandler()
->load('user');
$account = \Drupal::service('authentication')
->authenticate($request);
if ($account) {
\Drupal::currentUser()
->setAccount($account);
if (\Drupal::currentUser()->isAuthenticated() {
// The user is logged-in.
}
}
I fixed this by using a complete different approach. I wrote a module which sets a cookie on the moment that the user logs in to drupal (I use the hook_user_login for this). When the user logs out I delete that cookie (I use the hook_user_logout for this). This is the code of my test.module:
/**
* #param $account
*/
function intersoc_content_user_login($account)
{
setcookie("user", "loggedin", time() + (86400 * 30),"/");
}
/**
* #param $account
*/
function intersoc_content_user_logout($account)
{
if (isset($_COOKIE['user']))
{
unset($_COOKIE['user']);
setcookie('user', '', time() - 3600, '/'); //Clearing cookie
}
}
Then in my custom script in the site root I check if the cookie is set. When the cookie exists => The user is logged in. If the cookie doesn't exist then the user isn't logged in. The isLoggedIn() function below:
/**
* #return bool which indicates if the user is logged in or not
*/
private function isLoggedIn()
{
if(isset($_COOKIE["user"]))
{
return TRUE;
}
else
{
return FALSE;
}
}
It isn't the most beautiful solution, but it works!!!

handle and personalize symfony no route found exception response

I have a controller that response a json data to another application , this is the controller code :
/**
*
* #Get("/getXXX/{id}")
*/
public function getDataAction($id,Request $request){
$ceService = $this->container->get('comptexpertcews.service');
$employeNumber= $request->get('employeNumber') ;
$url = $this->container->getParameter('serverUri') . $id;
$res = new Response();
$res->setContent($ceService->getCews($url, wsUrl::ws_Headers));
$res->headers->set('Content-TYpe','application/json; charset=UTF-8');
return $res;
}
The problem is by default , if you don't give id in the url , symfony rise exception : not route foundexception , what i want is to handle the exception and personalize with my owner response like sending
{"error" :" id undefined "}
instead of the long message expcetion of symfony
You have two simple options:
Don't use param converter, get you data from a repository and then you can wrap it in try catch and create your own exception/message
If this is something you want to do globally, you can implement an event listener that catches onKernelException event and work with it from there, e.g.:
public function onKernelException(GetResponseForExceptionEvent $event): void
{
$exception = $event->getException();
if ($exception instanceof NotFoundHttpException) {
$response = $this->resourceNotFoundResponse(json_encode($exception->getMessage()));
}
if (isset($response)) {
$event->setResponse($response);
}
}
You also need to register you listener as a service, see the documentation here http://symfony.com/doc/current/event_dispatcher.html

Preventing Update of LastUsed in MetadataBag for specific Controller

I wrote an event listener for kernel.request to make me able to logout user automatically when he is idle for more than an amount of time.
I use this to calculate idle time:
$idle = time() - $this->session->getMetadataBag()->getLastUsed()
But I have a periodic Ajax request in my pages (for notification counts in pages) and they constantly change the LastUsed field of MetadataBag so Idle limit never reaches.
Is it possible to prevent a specific Controller (that ajax controller) to update session LastUsed ?
If yes, How?
If no, what else can I do to handle this?
Thanks
I don't know how to prevent the update of MetadataBag's lastUsed, but you can manually set the time for the user's last request, in the session and use it.
You can create a listener like below and make it listen to the kernel.request event, and in your other listener, get the data you store using this listener in the session instead of $this->session->getMetadataBag()->getLastUsed().
public function listen(GetResponseEvent $event){
// in your listeners main function
$request = $event->getRequest();
$route = $request->attributes->get('_route');
if($route != 'your_ajax_check_route'){
// update the session and etc.
// retrieve what you store here in your other listener.
}
}
The feature you are talking about (prevent update session lastUsed) can't be done without some Symfony hacking which is unnecesary as you can simple create your own logic for this. For example you can create KernelRequest listener which will update last used session variable for all request except the one you will use to check how much time left too logout:
public function onKernelRequest(GetResponseEvent $event): void
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
//$this->sessionMaxIdleTime is number from config parameter
//representing max idle time in seconds
if ($this->sessionMaxIdleTime > 0) {
$this->session->start();
$time = time();
$route = $event->getRequest()->attributes->get('_route');
//for all routes but one - checking how much time is left
if ('session_check' !== $route) {
//manual set lastUsed time
$this->session->set('manualLastUsedTime', $time);
}
$idleTime = $time - $this->session->get('manualLastUsedTime');
if ($idleTime > $this->sessionMaxIdleTime) {
$this->session->invalidate();
$this->session->getFlashBag()->set('info', 'Logged out due to inactivity.');
if ($event->getRequest()->isXmlHttpRequest()) {
$event->setResponse(new Response('Logged out due to inactivity.', Response::HTTP_FORBIDDEN));
} else {
$event->setResponse(new RedirectResponse($this->router->generate('login')));
}
}
}
Then you can simple create method in some controller, which will be used by some ajax function to check how much time left to logout for example:
/**
* #Route("/session/check", name="session_check", methods={"GET"})
* #param Request $request
* #param SessionInterface $session
* #param int $sessionMaxIdleTime
* #return JsonResponse
*/
public function checkSecondsToExpire(Request $request, SessionInterface $session, $sessionMaxIdleTime = 0): JsonResponse
{
$idleTime = time() - $session->get('manualLastUsedTime');
$secToExp = $sessionMaxIdleTime - $idleTime;
return new JsonResponse(['secToExp' => $secToExp]);
}
Last piece is to make some check mechanism. It can be done as simple as starting some JS function in your base template. The parameter is in twig syntax and is from config (it is the same as sessionMaxIdleTime in kernelRequest listener):
<script>
$(document).ready(function () {
SessionIdler.start({{ session_max_idle_time }});
});
</script>
SessionIdler.start is just a function that runs some other function in specific interval (in this example it will run 1 minute before configured sessionmaxIdleTime):
function start(time) {
checkSessionCounter = setInterval(checkSession, (time - 60) * 1000);
isCheckSessionCounterRunning = true;
// console.debug("checkSession will START in: " + (time - 60) + "s");
}
checkSession function make ajax request to our session_check route and depends on result it shows modal with proper information about too long inactivity. Modal can have a button or action when hiding, that will make another request to session_extend route (which can do nothing - it just need to be captured by kernelRequest listener to overwrite manualLastUsedTime)
This three pieces together creates a mechanism for notify user about too long inactivity without any influence on session metadataBag.

fosuserbundle ldap configuration for strange use case

I'm trying to create a fosuserbundle for a quite strange use case, which is mandatory requirement, so no space to diplomacy.
Use case is as follow:
users in a mongo db table populated by jms messages -no registration form
users log in by ldap
user record not created by ldap, after a successful login username is checked against mongodb document
Considering that ldap could successfully log in people that exhist in ldap but cannot access site (but login is still successful), what could be the best way to perform such authentication chain?
I was thinking about some possible options:
listen on interactive login event, but imho there's no way to modify an onSuccess event
create a custom AuthenticationListener to do another check inside onSuccess method
chain authentication using scheb two-factor bundle
any hint?
I've used Fr3DLdapBundle which can be incorporate with FOSUserBundle quite easily (I'm using the 2.0.x version, I have no idea if the previous ones will do the same or be as easy to set up).
In the LdapManager (by default) it creates a new user if one is not already on the database which is not what I wanted (and doesn't seem to be what you want) so I have added my own manager that checks for the presence of the user in the database and then deals with the accordingly.
use FR3D\LdapBundle\Ldap\LdapManager as BaseLdapManager;
.. Other use stuff ..
class LdapManager extends BaseLdapManager
{
protected $userRepository;
protected $usernameCanonicalizer;
public function __construct(
LdapDriverInterface $driver,
$userManager,
array $params,
ObjectRepository $userRepository,
CanonicalizerInterface $usernameCanonicalizer
) {
parent::__construct($driver, $userManager, $params);
$this->userRepository = $userRepository;
$this->usernameCanonicalizer = $usernameCanonicalizer;
}
/**
* {#inheritDoc}
*/
public function findUserBy(array $criteria)
{
$filter = $this->buildFilter($criteria);
$entries = $this->driver->search(
$this->params['baseDn'], $filter, $this->ldapAttributes
);
if ($entries['count'] > 1) {
throw new \Exception('This search can only return a single user');
}
if ($entries['count'] == 0) {
return false;
}
$uid = $entries[0]['uid'][0];
$usernameCanonical = $this->usernameCanonicalizer->canonicalize($uid);
$user = $this->userRepository->findOneBy(
array('usernameCanonical' => $usernameCanonical)
);
if (null === $user) {
throw new \Exception('Your account has yet to be set up. See Admin.');
}
return $user;
}

Symfony2 - Redirect response from request EventListener in dev mode while ignoring built in request events

I am building my own user management system in Symfony2 (not using FOSUserBundle) and want to be able to force users to change their password.
I have setup an EventListener to listen to the kernal.request event, then I perform some logic in the listener to determine if the user needs to change their password; if they do, then they are redirected to a "Change Password" route.
I add the service to my config.yml to listen on the kernal.request:
password_change_listener:
class: Acme\AdminBundle\EventListener\PasswordChangeListener
arguments: [ #service_container ]
tags:
- { name: kernel.event_listener, event: kernel.request, method: onMustChangepasswordEvent }
And then the listener:
public function onMustChangepasswordEvent(GetResponseEvent $event) {
$securityContext = $this->container->get('security.context');
// if not logged in, no need to change password
if ( !$securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED') )
return;
// If already on the change_password page, no need to change password
$changePasswordRoute = 'change_password';
$_route = $event->getRequest()->get('_route');
if ($changePasswordRoute == $_route)
return;
// Check the user object to see if user needs to change password
$user = $this->getUser();
if (!$user->getMustChangePassword())
return;
// If still here, redirect to the change password page
$url = $this->container->get('router')->generate($changePasswordRoute);
$response = new RedirectResponse($url);
$event->setResponse($response);
}
The problem I am having is that in dev mode, my listener is also redirecting the profiler bar and assetic request events. It works when I dump assets and clear cache and view the site in production mode.
Is there a way I can ignore the events from assetic/profiler bar/any other internal controllers? Or a better way to redirect a user to the change_password page (not only on login success)?
Going crazy thinking up wild hack solutions, but surely there is a way to handle this elegantly in Symfony2?
This is the very hack solution I am using for now:
Determine if in dev environment
If so, get an array of all the routes
Filter the route array so that only the routes I have added remain
Compare the current route to the array of routes
If a match is found, this means that the event is not an in-built controller, but must be one that I have added, so perform the redirect.
And this is the madness that makes that work:
// determine if in dev environment
if (($this->container->getParameter('kernel.environment') == 'dev'))
{
// Get array of all routes that are not built in
// (i.e You have added them yourself in a routing.yml file).
// Then get the current route, and check if it exists in the array
$myAppName = 'Acme';
$routes = $this->getAllNonInternalRoutes($myAppName);
$currentRoute = $event->getRequest()->get('_route');
if(!in_array($currentRoute, $routes))
return;
}
// If still here, success, you have ignored the assetic and
// web profiler actions, and any other actions that you did not add
// yourself in a routing.yml file! Go ahead and redirect!
$url = $this->container->get('router')->generate('change_password_route');
$response = new RedirectResponse($url);
$event->setResponse($response);
And the crazy hack function getAllNonInternalRoutes() that makes it work (which is a modification of code I found here by Qoop:
private function getAllNonInternalRoutes($app_name) {
$router = $this->container->get('router');
$collection = $router->getRouteCollection();
$allRoutes = $collection->all();
$routes = array();
foreach ($allRoutes as $route => $params)
{
$defaults = $params->getDefaults();
if (isset($defaults['_controller']))
{
$controllerAction = explode(':', $defaults['_controller']);
$controller = $controllerAction[0];
if ((strpos($controller, $app_name) === 0))
$routes[]= $route;
}
}
return $routes;
}

Resources