laracastsluvr's avatar

User not authenticated when testing a globalscope on a model

The title is a bit confusing, I know.

I have a simple polymorphic "following" system for the User::class, where Users follow other Users or some other models.

I wrote tests for the follow unfollow actions and everything runs fine.

I have a problem testing an attribute that is set in a global scope.

The global scope does the following. In order to avoid the N+1 issue when rendering an index page of users, where every user has a follow/unfollow button. In order to check if the current user has followed the users in the list, I used a subselect query with addSelect() and placed that in a GlobalScope on the User model

class User extends Authenticatable
{
	// stuff

	protected static function booted()
	{
		// I need the authenticated  user here
		$user = auth()->user(); // is this stupid? is there a better way?

		static::addGlobalScope('current_user_follower', function(Builder $builder) use ($user) {
            $builder->addSelect([
                'current_user_follower' => Follow::
                    select('user_id')
                    ->whereColumn('followables.followable_id', 'users.id')
                    ->where('followables.followable_type', User::class)
                    ->where('followables.user_id', ($user) ? $user->id : 0) // there will never be a user with ID of 0
                    ->limit(1)
            ]);
        });
	}

	// there's also this method because the scope returns Null if user isnt a current follower or not logged in
	public function getCurrentUserFollowerAttribute($value): bool
    {
        return (is_null($value)) ? false : true;
    }

}

The above actually works when I request an index page of all users, and if a user is already followed the button displays "unfollow". All in a single query thus avoiding the N+1 problem.

I had to write out the idea before I wrote the unit test that is checking the attribute set by that global scope.

Now I set out to cover it in an actual test but when I request a user list the auth()->user() is awlays null, even tho I tried $this->actingAs(), $this->be(), or Auth::guard()->setUser().

/** @test */
public function user_model_have_initial_follow_condition()
{
$currentUser = $this->createUser();
        $followableUsers = User::factory()->count(100)->create();

        $this->actingAs($currentUser->fresh());

        $followableUsers
            ->random(25)
            ->each(fn($followableUser) => (new FollowUserAction)->execute($currentUser, $followableUser));

        $this->assertCount(25, $currentUser->followsUsers); // this is OK

		// this is not passing, also I know it isnt asserting anything and it isnt an actual test
		User::
            where('id', '!=', $currentUser->id)
            ->each(function($user) {
                dump($user->current_user_follower); // every user reports false and I dont get the 25 true where I set the follow relationshpis
            });
}

When I dd() the auth()->user() inside the global scope it returns null, so my best guess is that requesting the User list in the test is done "outside" the normal flow of a an actual request. Is there a way to "encapsulate" if that's the correct terminology, the User query inside the test so that the authenticated user is set in the global scope??

0 likes
6 replies
CorvS's avatar

Fetch the current user's ID inside the (anonymous) global scope and it should work.

protected static function booted()
{
	static::addGlobalScope('current_user_follower', function(Builder $builder) {
        $builder->addSelect(['current_user_follower' => Follow::select('user_id')
            ->whereColumn('followables.followable_id', 'users.id')
            ->where('followables.followable_type', User::class)
            ->where('followables.user_id', Auth::id() ?? 0)
            ->limit(1)
        ]);
    });
}
1 like
CorvS's avatar

Because your model has already been booted before you set the authenticated user using actingAs. booted() is not called for every instance of the model if that's what you were thinking.

1 like
laracastsluvr's avatar

It passes the test it crashes PHP when trying to view it on page.

"PHP message: PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes)
martinbean's avatar
Level 80

Don’t try to interact with authenticated users in model global scopes. Create a local scope that you pass a user to instead.

Models should not be reaching out and grabbing things from requests, sessions, etc.

Please or to participate in this conversation.