Search code examples
laravelphpunitmiddlewarelaravel-testing

Laravel middleware sharing variable view does not work when writing HTTP test


We have the following middleware. We know the middleware works since testing the application in browser is working. Sadly when writing HTTP test case, blade is saying that the variable defined by the middleware is not there.

<?php

namespace App\Http\Middleware;

use App\Repository\UserContract;
use Closure;
use Illuminate\Contracts\View\Factory as ViewFactory;
use Illuminate\Support\Facades\View;

class Authenticate
{
    private $userRepository;
    private $view;

    public function __construct(UserContract $userRepository, ViewFactory $view)
    {
        $this->userRepository = $userRepository;
        $this->view = $view;
    }

    public function handle($request, Closure $next)
    {
        $userId = null;
        $username = null;
        if ($request->hasCookie('auth')) {
            $secret = $request->cookie('auth');
            $userId = $this->userRepository->getUserIdBySecret($secret);
            $username = $this->userRepository->getUsername($userId);
        }
        $this->view->share('username', $username);
        $request['_user_id'] = $userId;
        $request['_username'] = $username;
        return $next($request);
    }
}

We doing a PHPUnit test as following:

    /**
     * @test
     */
    public function it_shows_logged_in_username()
    {
        $app = $this->createApplication();
        $encrypter = $app->get(Encrypter::class);
        $userRepository = $app->make(UserContract::class);
        $userRepository->addUser('jane', 'secret');

        $secret = $encrypter->encrypt('secret', false);
        $response = $this->call('GET', '/', [], ['auth' => $secret], [], [], null);
        $response->assertSeeText('jane');
    }

error

ErrorException {#980                                                                                                                                           
  #message: "Undefined variable: username"                                                                                                                     
  #code: 0                                                                                                                                                     
  #file: "./storage/framework/views/db4b9232a3b0957f912084f26d9041e8a510bd6c.php"
  #line: 3                                                                     
  #severity: E_NOTICE                                                          
  trace: {                                                                     
    ./storage/framework/views/db4b9232a3b0957f912084f26d9041e8a510bd6c.php:3 {
      › <?php dump($comments); ?>                                              
      › <?php dump($username); ?>  

Any advice would be appreciated?

EDITS

I have to add that using the View facade and called the share methods on it make it works but I hit the same issue with the errors variable that should be set by the \Illuminate\View\Middleware\ShareErrorsFromSession stock laravel middleware.

Other remark, my middleware is called Authenticate but it has nothing to do with the normal password based authentication of Laravel.

Here the UserContract:

<?php
declare(strict_types=1);


namespace App\Repository;


use App\Repository\Exception\UserAlreadyRegisteredException;

interface UserContract
{
    public function isRegistered(string $username): bool;

    public function getUserID(string $username): int;

    public function getUserIdBySecret(string $secret): int;

    /**
     * @param int $userId
     *
     * @return string
     * @throws
     */
    public function getUsername(int $userId): string;

    /**
     * @param int $userId
     *
     * @return string
     * @throws AuthSecretNotSetException
     */
    public function getUserAuthSecret(int $userId): string;

    public function getNextId(): int;

    /**
     * @param string $username
     * @param string $authSecret
     *
     * @return int
     * @throws UserAlreadyRegisteredException
     */
    public function addUser(string $username, string $authSecret): int;

    public function fetchUpdatedBetween(?\DateTimeInterface $start, ?\DateTimeInterface $end): array;

    public function markAsSynced(int $userId): void;

    public function isSynced(int $userId): bool;

    public function delete(int $userId): void;

    public function markAsDeleted(int $userId): void;

    public function fetchDeletedIds(): array;

    public function removeDeletedFlag(int $userId): void;

    public function fetchStalledIds(\DateTimeInterface $dateTime): array;

    public function purge(int $userId): void;

    /**
     * @param UserFetcherContract $fetcher the closure would be passed userId and it should return data or null
     */
    public function setFallbackFetcher(UserFetcherContract $fetcher): void;
}

Solution

  • I finally took the time to do a deep dive on this one.

    It turns out the issue is happening because I am creating multiple instance of the booted app and somehow it mess with the instance of the view factory registered in the the application.

    In the test case removing this line make it works:

    /**
         * @test
         */
        public function it_shows_logged_in_username()
        {
            // THIS IS WRONG
            // $app = $this->createApplication();
            $app = $this->app; // Use application instantiated in setUp method of test case
            $encrypter = $app->get(Encrypter::class);
            $userRepository = $app->make(UserContract::class);
            $userRepository->addUser('jane', 'secret');
    
            $secret = $encrypter->encrypt('secret', false);
            $response = $this->call('GET', '/', [], ['auth' => $secret], [], [], null);
            $response->assertSeeText('jane');
        }
    

    I am not 100% sure why booting multiple instance of the app would create issues but my gut feeling is: some shared data somewhere...

    For any of you interested here the commit