spatie/laravel-permission

The given role or permission should use guard `sanctum` instead of `api` when using teams mode in Laravel tests.

sts-ryan-holton opened this issue · 6 comments

Describe the bug

I'm using the teams feature of this package. In my application a "team" is a Company and users are assigned to companies. There is one global role called super_admin. My default auth guard is api since my Laravel 10 project is being used as an api to server a Nuxt front-end - all of this is working fine right now and I'd like to implement some tests.

When trying to create a reusable funtion to seed my roles and permissions, an error is thrown:

The given role or permission should use guard sanctum instead of api.

Versions

  • spatie/laravel-permission package version: 5.10.2
  • illuminate/framework package: 10.19.0

PHP version: 8.1

Database version: MariaDB

Additional context

My TestCase file is what all my tests extend, here's it's contents along with my createRoleWithPermissions public function:

<?php

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Illuminate\Support\Facades\Log;
use Spatie\Permission\PermissionRegistrar;
use Database\Seeders\Production\Roles\GlobalRoleTableSeeder;
use Laravel\Sanctum\Sanctum;
use App\Models\Company;
use App\Models\User;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication, RefreshDatabase;

    /**
     * Define a global user for each test
     */
    public Company $company;

    /**
     * Define a company for each test that user can be attributed to
     */
    public User $user;

    /**
     * Runs before each test
     */
    protected function setUp(): void
    {
        parent::setUp();

        // now re-register all the roles and permissions (clears cache and reloads relations)
        $this->app->make(PermissionRegistrar::class)->registerPermissions();

        // globally set common headers for HTTP tests
        $this->withHeaders([
            'Accept' => 'application/json'
        ]);

        // create global role
        $this->seed(GlobalRoleTableSeeder::class);

        // create user each time
        $this->user = User::factory()->create();

        // create the company each time
        $this->company = Company::factory()->create([
            'user_id' => $this->user->id
        ]);

        // assign user to company
        $sessionCompanyId = getPermissionsTeamId();
        setPermissionsTeamId($this->company->id);
        $this->user->assignRole('super_admin');
        setPermissionsTeamId($sessionCompanyId);
    }

    /**
     * Login as a user
     * docs: https://laravel.com/docs/10.x/sanctum#testing
     */
    public function loginAs(User $user = null): void
    {
        Sanctum::actingAs(
            $user,
            ['*']
        );
    }

    /**
     * Create roles with permissions
     * inspiration: https://github.com/drbyte/spatie-permissions-demo/blob/master/tests/Feature/PostsTest.php#L36
     */
    public function createRoleWithPermissions(int $companyId, array $roles): void
    {
        foreach ($roles as $key => $permissions) {
            $role = Role::query();
            $role = $role->where('name', $key);

            if ($key != 'super_admin') {
                $role = $role->where('company_id', $companyId);
            }

            $role = $role->first();

            foreach ($permissions as $permission) {
                $discoveredPermission = Permission::where('name', $permission)->first();

                if ($discoveredPermission) {
                    $discoveredPermission->assignRole($role);
                    continue;
                }

                $permissionCreated = Permission::create([
                    'name' => $permission,
                    'guard_name' => 'sanctum',
                ]);

                $permissionCreated->assignRole($role);
            }
        }
    }
}

Note that the $this->seed(GlobalRoleTableSeeder::class); simply seeds my global role that associated everywhere:

if (! Role::where('name', 'super_admin')->whereNull('company_id')->first()) {
    Role::create([
        'name' => 'super_admin',
        'company_id' => null,
        'guard_name' => config('auth.defaults.guard'),
    ]);
}

Then, in a simple test file...

<?php

namespace Tests\Feature\System\Company\Affiliates;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\TestResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;

class AffiliateTest extends TestCase
{
    /**
     * Ensure that affiliates index returns successful response
     */
    public function test_affiliates_index_returns_successful_response(): void
    {
        $companyId = $this->company->id;

        setPermissionsTeamId(1);

        $this->createRoleWithPermissions($companyId, [
            'super_admin' => [
                'affiliate_index'
            ]
        ]);

        $this->loginAs($this->user);

        $response = $this->get("/api/company/$companyId/affiliates");

        $this->assertThat(
            $response->getStatusCode(),
            $this->logicalOr(
                $this->equalTo(200),
                $this->equalTo(404)
            )
        );
    }
}

It's here where the errors occur. I need to apply the super admin's permission of affiliate_index to the role so that it passes my authorize method.

Why am I getting this error? I've tried forcing sanctum with no luck.

Environment (please complete the following information, because it helps us investigate better):

  • OS: Mac OS
  • Version Latest

Make an example app: https://spatie.be/docs/laravel-permission/v5/basic-usage/example-app

Are you trying to pass authorization only with the super_admin role?

In GlobalRoleTableSeeder is 'guard_name' => config('auth.defaults.guard'), where it's picking up api?

In test_affiliates_index_returns_successful_response should setPermissionsTeamId(1) be setPermissionsTeamId($companyId)?

I have a similar issue, its only actually happening when I run tests though. Also it's only when the gate / model policy returns false. I can actually check for the roles and if it's successful no issue. If it's false the issue is raised.

I think it's something to do with the type of response it wants to do.

For example if you run:

$this->authorize('view',$model); // if the return is false the test will fail.
Gate::authorize('view',$model); // this works the same

However

Gate::check('view',$model); // this will return a true or false.

It seems like when we have an AuthorizationException being thrown that is when the issue comes up. It's also pointing to my seeder though, which is weird.

If you already have a loaded user, after change team_id, you always must reload relations, look at the tests

setPermissionsTeamId(1);
$this->testUser->load('roles', 'permissions');
$this->assertEquals(
collect(['edit-articles', 'edit-news']),
$this->testUser->getAllPermissions()->pluck('name')->sort()->values()
);
setPermissionsTeamId(2);
$this->testUser->load('roles', 'permissions');

And i think "/api/company/$companyId/affiliates" is using api auth, check that

That part is fine and it works. When running tests though, it runs into the following issue and throws an error:

if (! $this->getGuardNames()->contains($roleOrPermission->guard_name)) { throw GuardDoesNotMatch::create($roleOrPermission->guard_name, $this->getGuardNames()); }

The permissions all have the 'web' guard. But the user we are logged in with has a 'sanctum' guard, this happens whether I use any of the following:

   $this->actingAs($user2);

        Sanctum::actingAs(
            $user2,
            ['*'],
            
        );
        Sanctum::actingAs(
            $user2,
            ['*'],
            'web'
        );

If you go to the same controller function via via web.php instead of api.php then it works fine. When going through api.php, we hit the "auth:sanctum" middleware and that causes an issue somewhere.

@muhzak that is not an issue, it's the expected behaivor, read other issues/discussions, you could set a default guard_name on user for avoid that