Mock user login for phpunit testing

I’m having problems trying to mock a logged in user to run my PhpUnit testing.

For background, users are authenticated uses a custom extension of the Ldap identifier, this performs a bind request to active directory and then if successful looks the user up in the users using their username, the users table contains their username and role_id, which is linked to roles table. There are two roles super users and normal users. Authorization is handled using a request policy.

In my testing I have created a UserLoginTrait, in my fixtures I create a user with superuser role, in the login trait I look the user up and then assign them to the session variable Auth i.e.

 $this->session(['Auth' => $user]);

I then call the UserLoginTrait in the setup of each test, But when I run my tests I get a fatal a PHP fatal error

"PHP Fatal error: Cannot declare class User, because the name is already in use… "

I have no idea how to fix this? Can anyone help.

// TestUserLoginTrait 

$this->addFixture('app.Users');
$this->addFixture('app.Roles');

$users = FactoryLocator::get('Table')->get('Users');
       $user = $users->find()->contain(['Roles'])->where(['username' => $username])->first();

then in my test setup

public function setUp(): void
    {
        parent::setUp();
        $this->login('test');
		
    }

Thanks

P

None of the code you’ve shown here is declaring a User class. Where (as in exactly what line of code) is the error coming from?

I’m running a single controller test.

<?php
declare(strict_types=1);

namespace App\Test\TestCase\Controller;

use Cake\TestSuite\IntegrationTestTrait;
use Cake\TestSuite\TestCase;
use Cake\Datasource\FactoryLocator;


/**
 * App\Controller\DemandsController Test Case
 *
 * @uses \App\Controller\DemandsController
 */
class DemandsControllerTest extends TestCase
{
    use IntegrationTestTrait;
	use TestUserLoginTrait;

    /**
     * Fixtures
     *
     * @var array<string>
     */
    protected $fixtures = [
//relevant fixtures
    ];
	
	public function setUp(): void
    {
        parent::setUp();
		
		$this->login();
		
    }
	public function testAdd(): void
    {
		$this->enableCsrfToken();
		$this->enableSecurityToken();
        $this->enableRetainFlashMessages();
		$data = [
			//example data
		];
		
		$this->post('/controller/action', $data);
		$this->assertResponseSuccess();
		$this->assertRedirect();
		$this->logout();
    }

/**
     * Test edit method
     *
     * @return void
     * @uses \App\Controller\DemandsController::add()
     */
    public function testEdit(): void
    {

		$data = [
			//example data
		];
		
		$this->post('/controller/action', $data);
		$this->assertResponseSuccess();
		$this->assertRedirect();
		$this->logout();
    }


No where does it declare a User class this is why im confused

The error only references where the User class is already in use. When i run the tests using the debug flag it appears half way through the testEdit test, testAdd seems to run fine but testEdit … this makes me think something is being left behind from the first test and then redeclared in the second test? But I have no idea where

You still have not answered this question. You should have more error details than you’ve shared. “PHP Fatal error: Cannot declare class User, because the name is already in use…” It’s the part after the “…” that I’m interested in seeing, and maybe the last couple lines of the stack trace.

It’s just references the User class “because the class is already in use at line 21 of the User class”

And by “the User class”, you mean \App\Model\Entity\User, the implementation of which is found in src/Model/Entity/User.php? Details matter, I don’t know how many different ways I can tell you this…

Yes the user class in src/Model/Entity/User. I have not changed the User class this is the same as what would be automatically generated via a $ cake bake command

Okay, that eliminates some possibilities. Seems most likely at this point that it’s something in TestUserLoginTrait or UsersFixture causing the issue.

When running in debug mode the first test seems to run fine, i.e. in the log there is a testAdd started and testAdd ended. Then next test, testEdit, fails once the SQL code to get the user is ran i.e.

2023-10-09 10:10:54 debug: connection=test duration=0 rows=0 SELECT Users.id AS Users__id, Users.username AS Users__username, Users.name AS Users__name, Users.email AS Users__email, Users.role_id AS Users__role_id, Roles.id AS Roles__id, Roles.role AS Roles__role FROM users Users INNER JOIN roles Roles ON Roles.id = Users.role_id WHERE (username = 'test' AND) ORDER BY name ASC LIMIT 1
PHP Fatal error:  Cannot declare class User, because the name is already in use in /src/Model/Entity/User.php on line 20

in the test that ran successfully the line that runs after this one where it fails is…


2023-10-09 10:10:51 debug: connection=test duration=0 rows=0 SELECT Users.id AS Users__id, Users.username AS Users__username, Users.name AS Users__name, Users.email AS Users__email, Users.role_id AS Users__role_id, Roles.id AS Roles__id, Roles.role AS Roles__role FROM users Users INNER JOIN roles Roles ON Roles.id = Users.role_id WHERE (username = 'test' AND) ORDER BY name ASC LIMIT 1
2023-10-09 10:10:52 debug: connection=test duration=0 rows=0 SET foreign_key_checks = 0

Hi @pavia20 ,

this is some recommendation to write your tests. Tehy might not solve your issue, but by organizing your tests in a cleaner way, the issue might appear:

  1. Do you use the FactoryLocator on purpose? As far as I know using TableRegistry::getTableLocator()->get('Users') is the recommanded approach
  2. How about creating a DemandsControllerAddTest class with all the test scenarios for the DemandsController::add() method, and the same with a DemandsControllerEditTest class? This will help keep your tests clean
  3. I would not log the user in the setUp(), since you will also want to cover the case where the user is not logged in, but rather in the test methods where this is required
  4. Eventually, if after all this little refactoring you have not found the issue, or even if you have!, I would recommend you to use the fixture factories plugin. The fact that you are willing to test your code makes you now a professional, and you surely want to explore the tools needed for that.

Hi @pabloelcolombiano thanks for your help. I have now restructured my TestUserLoginTrait as the following

<?php
declare(strict_types=1);

namespace App\Test\TestCase\Controller;

use Cake\ORM\TableRegistry;

trait TestUserLoginTrait
{	
    protected function login(string   $username = 'test'):void
    {

          $users = TableRegistry::getTableLocator()->get('Users');
        $user = $users->find()->where(['username' => $username])->first();           
        $this->session(['Auth'=>$user]);
        $this->post('users/login');
    }

    protected function logout():void
    {
        $this->post('users/logout');
    }
}

In my DemandsControllerTest I have

public function testAdd(): void
    {
		//test unauthenticated user
		$this->get('/demands/add/2023/2');
		//custom redirect handler to user login page
		$this->assertRedirect();
		//warn user to login
		$this->assertFlashMessage('Please login before trying to access this application.', 'flash');
		$this->login();	
		$this->assertSession('test', 'Auth.username');
		//check user logged in 
		//now try to access
		$this->get('/demands/add/2023/2');
		$this->assertResponseOk();
		$data = [
			'demand'=>100
		];
		
		//post data
		$this->post(/demands/add/2023/2', $data);
		//redirect
		$this->assertResponseSuccess();
		$this->assertRedirect();
    }
	/**
     * Test edit method
     *
     * @return void
     * @uses \App\Controller\DemandsController::add()
     */
    public function testEdit(): void
    {
		$this->login();	
		$data = [
			'demand'=>100
		];
               //Fails if user not logged in but login causes PHP Fatal Error
                $this->get('/demands/add/2023/1?demandId=1');
		$this->post(/demands/add/2023/1?demandId=1', $data);
    }

The first test, testAdd() runs fine. However, testEdit fails if I include the $this->login() line with same error message from my previous replies. If I don’t include this then any assertions will fail as the user is not authenticated. I will look into your 4th suggestion, but I would like to work out why this isnt working if possible

Is there any chance you’re somewhere referencing the users table through something like TableRegistry::getTableLocator()->get('users') (note that I’ve used lower case users here, not Users like in your code above)? I’m not sure, but it seems like that might work okay in many scenarios, but fail in this sort of way.

Another way to try tracking this down would be to create a constructor for your User entity, which just dumps the stack trace of how it got there, then run your tests. This should help you see how it’s hitting it twice.

Hi @pabloelcolombiano. I’m starting to look into using the fixture factories plugin, however with my project already quite comprehensive I’m finding the documentation quite a large jump from toy examples to actually in practice.

I have a users table, users can have one of two roles admin or user, the users table links to the roles table via belongsTo association. Users are also in user teams, teams contain many users and users can be part of many teams i.e. users table has a belongsToMany relationship with the teams table, joined via the teams_users join table. I have installed the fixture plugin and baked all models for with -m flag.

/**
     * Defines the factory's default values. This is useful for
     * not nullable fields. You may use methods of the present factory here too.
     *
     * @return void
     */
    protected function setDefaultTemplate(): void
    {
        $this->setDefaultData(function (Generator $faker) {
            return [
                'username' => $faker->userName(),
                'name' => $faker->name(),
                'email' => $faker->userName() . '@example.com'
            ];
        });

    }

    /**
     * @param array|callable|null|int|\Cake\Datasource\EntityInterface|string $parameter
     * @return UserFactory
     */
    public function withRoles($parameter = null): UserFactory
    {
        return $this->with(
            'Roles',
            \App\Test\Factory\RoleFactory::make($parameter)
        );
    }

    /**
     * @param string $name
     * @return $this
     */
    public function withRole(string $name)
    {
        return $this->with('Roles', compact('name'));
    }

    /**
     * @return $this
     */
    public function admin()
    {
        return $this->withRole('admin');
    }

    /**
     * @param array|callable|null|int|\Cake\Datasource\EntityInterface|string $parameter
     * @param int $n
     * @return UserFactory
     */
    public function withTeams($parameter = null, int $n = 1): UserFactory
    {
        return $this->with(
            'Services',
            \App\Test\Factory\TeamFactory::make($parameter, $n)->without('Users')
        );
    }

I have added the admin() method, however I seem to getting some syntax errors when trying to create a admin or not using

$user = UserFactory::make()->admin()->getEntity();

I’m not quite sure how to use this to mock an authenticated user for my testing. Would you be able to offer some advice? Cant find anything in the documentation about this

  1. What are the syntax errors you mentioned?

  2. The package does not adress the login issue. However you could take example on how we mock the session login in the passbolt_api here using that.