alberto98's avatar

Error with Laravel Policies and Laravel Github Action

Hi everybody,

I'm currently working on a OpenSource Laravel API that will support the communication between personal trainers and their athletes.

For the first time, I'm trying using the Laravel GitHub action with the default configuration.

This is how the yml file look like:

name: Laravel

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  laravel-tests:

    runs-on: ubuntu-latest

    steps:
    - uses: shivammathur/setup-php@15c43e89cdef867065b0213be354c2841860869e
      with:
        php-version: '8.0'
    - uses: actions/checkout@v3
    - name: Copy .env
      run: php -r "file_exists('.env') || copy('.env.example', '.env');"
    - name: Install Dependencies
      run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
    - name: Generate key
      run: php artisan key:generate
    - name: Directory Permissions
      run: chmod -R 777 storage bootstrap/cache
    - name: Create Database
      run: |
        mkdir -p database
        touch database/database.sqlite
    - name: Execute tests (Unit and Feature tests) via PHPUnit
      env:
        DB_CONNECTION: sqlite
        DB_DATABASE: database/database.sqlite
      run: vendor/bin/phpunit

I'll share you one of my migration, test, controller and policy:

create_category_table:

public function up()
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained();
            $table->string('name');
            $table->string('description');
            $table->timestamps();
        });
    }

CategoryTest:

    public function test_get_category(): void
    {
        $user = User::factory()->create();
        Sanctum::actingAs($user);
        $category = Category::factory()->create(['user_id' => $user->id]);
        $response = $this->get('/api/category/'.$category->id);

        $response->assertStatus(200);
        $response->assertJson(['status' => 'success','category' => []]);
    }

CategoryController:

public function getCategory(\Illuminate\Http\Request $request): JsonResponse {
        $category = Category::find($request->id);
        if($category) {
            if($request->user()->cannot('view', $category)) {
                return response()->json(['error' => 'Unauthorized'], 401);
            }
            return response()->json(['status' => 'success', 'category' => $category]);
        }
        return response()->json(['status' => 'error', 'report' => 'Non è stata trovata nessuna categoria']);
    }

CategoryPolicy:

    public function view(User $user, Category $category)
    {
        return  $user->id === $category->user_id;
    }

GitHub Action:

1) Tests\Feature\CategoryTest::test_get_category
Expected response status code [200] but received 401.
Failed asserting that 200 is identical to 401.

/home/runner/work/gym-diary/gym-diary/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:177
/home/runner/work/gym-diary/gym-diary/tests/Feature/CategoryTest.php:44

This seems to happen to every method that has a Policy.

Can anyone help me?

P.S: Of course, it works when I run the test locally

EDIT: CategoryFactory:

public function definition()
    {
        return [
            'name' => $this->faker->sentence(2),
            'description' => $this->faker->sentence(),
            'user_id' => User::factory()
        ];
    }
0 likes
6 replies
Nakov's avatar

The issue is not from the policy, then you will get a 403 error code, 401 means that something is wrong with the authentication. So something is not working with your Sanctum::actingAs($user);.

Maybe try this approach:

$this
    ->actingAs($user, 'sanctum')
    ->getJson('/api/category/'.$category->id);

not I showed getJson instead of get() usage since the response you provide is always json

1 like
alberto98's avatar

@Nakov I'll try but it seems strange, because if I call a method that doesn't have a check to the Policy it works as expected.

Another thing is that if the Policy check fails, my controller returns a 401, that is exactly what the action says:

if($request->user()->cannot('view', $category)) {
	return response()->json(['error' => 'Unauthorized'], 401);
}
Nakov's avatar

@alberto98 oh, sorry.. I missed that in your controller. You should return 403 - Forbidden, 401 is misleading in this case, because it should be returned if the credentials are wrong: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401

In your Category factory do you have 'user_id' => User::factory() or User::factory()->create() if you have the latter, then you should use the first one, since the second one will create another user in which case the error is correct. And just User::factory() if another ID is provided as you do from the tests it will overwrite it.

alberto98's avatar

@Nakov Thanks again for your reply.

I've updated the question with my factory and as you can see I use User::factory().

Locally all the tests works as expected and I don't know why the action returns all those errors.

For testing this error locally, I've just cloned the repo in my local machine and run the tests on a different. They just worked...

If you want to test it or just want to look at the GitHub Action errors and all the code you can find my repo here: Repo Link

Nakov's avatar
Nakov
Best Answer
Level 73

@alberto98 Okay, I found what the issue is. It is related to this: https://github.com/laravel/framework/issues/3548

Basically if you do this:

public function view(User $user, Category $category)
{
    return  $user->id === (int) $category->user_id;
}

or if you add:

public $casts = [ 'user_id' => 'integer'];

in your Category model, the test will work.

Btw, I use this to test Github actions locally : https://mauricius.dev/run-and-debug-github-actions-locally/

and I had to add this :

with:
    php-version: '8.0'
    extensions: php-sqlite3

in the action in order to not throw the driver error.

alberto98's avatar

@Nakov The moment before you answered me I changed my local db to Sqlite and I've finally found the error that was being returned from the CI.

The real error is exactly what you just pointed out, I need to cast user_id as integer and it will work as expected!

Thank you so much for your time!!!

Please or to participate in this conversation.