Default Laravel comes with the "users" table, and we need to add roles and permissions on top of that one. And what I want to do here is describe my way of thinking about this, so you would understand the alternatives and be prepared to make the decisions for your applications.
Many tutorials start with roles and then go to permissions, but I will start from the other side: let's reverse-engineer what we need roles/permissions FOR.
Goals of This Lesson
- Create our simple DB structure for roles/permissions
- Create first API endpoints and PHPUnit Tests for registration
- Simulate API endpoints for logged-in users and write tests to check permissions
- Alternative: look at the spatie/laravel-permission package
As a result of this lesson, we will launch the automated tests and see this result:
Simple Permissions with Gates
In our application, we will have features like:
- Manage properties
- Make booking
- Change the user's password
- etc.
Those are all permissions. Every user may or may not have permission for some action.
In Laravel's language, permissions are almost the same as Gates.
So, technically speaking, simple applications may be created with just Users and Gates, without separate Roles/Permissions DB tables.
In a simple app, if you have just a users.is_admin
DB column with 0/1 values, you can define:
app/Providers/AppServiceProvider.php:
Gate::define('manage-users', function (User $user) { return $user->is_admin == 1;});
And then, wherever we have the permission check, instead of checking the role, you should check the permission every time:
Blade files:
@can('manage-users') Link to edit user's password@endcan
UserController.php:
public function edit(User $user){ $this->authorize('manage-users'); // Return the form to edit the user's password}
But in our case, it's not just the admin. We will have three roles, at least:
- Administrator
- Property owner
- Simple User
So, in our case, such Provider would look like this:
app/Providers/AppServiceProvider.php:
Gate::define('manage-properties', function (User $user) { return $user->is_owner == 1;});Gate::define('book-property', function (User $user) { return $user->is_user == 1;});Gate::define('manage-users', function (User $user) { return $user->is_admin == 1;}); // ... more permissions
Those $user->is_xxxxx
are not that pretty, are they? With dozens of permissions, the provider file would look huge and ugly. So, we DO need to have roles and permissions separately in the database.
Roles and Permissions in the DB
Instead of those is_owner
and is_admin
potential columns in the DB, let's create Roles with a relationship.
php artisan make:model Role -ms
Migration:
Schema::create('roles', function (Blueprint $table) { $table->id(); $table->string('name'); $table->timestamps();});
Make the name
fillable:
app/Models/Role.php:
class Role extends Model{ protected $fillable = ['name'];}
And then immediately I would suggest adding a Seeder to seed the main roles for the beginning. You noticed that I generated make:model
with -s
at the end. So yeah, it generated the Seeder file, which we will in like this:
database/seeders/RoleSeeder.php:
use App\Models\Role; class RoleSeeder extends Seeder{ public function run() { Role::create(['name' => 'Administrator']); Role::create(['name' => 'Property Owner']); Role::create(['name' => 'Simple User']); }}
Next, each role will have multiple Permissions. So let's store them in the database, too. The DB structure is identical to the Role:
php artisan make:model Permission -m
Migration:
Schema::create('permissions', function (Blueprint $table) { $table->id(); $table->string('name'); $table->timestamps();});
app/Models/Permission.php:
class Permission extends Model{ protected $fillable = ['name'];}
Now, the relationship. It should be a many-to-many, because both each role may have many permissions, and also each permission may belong to many roles.
php artisan make:migration create_permission_role_table
Migration:
Schema::create('permission_role', function (Blueprint $table) { $table->foreignId('permission_id')->constrained(); $table->foreignId('role_id')->constrained();});
And we add the methods for relationships, in both Models:
app/Models/Role.php:
class Role extends Model{ // ... public function permissions() { return $this->belongsToMany(Permission::class); }}
app/Models/Permission.php:
class Permission extends Model{ // ... public function roles() { return $this->belongsToMany(Role::class); }}
Ok, great, we have the relationship between roles and permissions. Now, how do we assign roles or permissions to Users?
User: One Role or Multiple Roles?
Typically, there are two layers of managing permissions:
- Admin adds the permissions and then specifies which roles have certain permissions
- For users, the admin/system assigns the ROLES to them, which in itself includes the permissions
In other words, we don't assign permissions to the users, we assign only the roles.
So, we need to create the relationship from User to Role. And here's where we have a million-dollar question:
Should User-Role be a belongsTo or a belongsToMany?
This, of course, depends on your application needs. To rephrase, "can a user have multiple roles"?
I've seen applications that allow multiple roles, like the same person can be an editor and an administrator, but it makes the system so much more complex, and even confusing.
So, I would vote that, in most cases, you would first choose a more simple approach of one role per user.
I've checked Booking.com itself, and they require separate registrations as a user and as a property owner, so you can be either one or another.
That's why we will go for just a belongsTo
relationship here.
php artisan make:migration add_role_id_to_users_table
Migration:
Schema::table('users', function (Blueprint $table) { $table->foreignId('role_id')->constrained();});
Then we add that role_id
to the $fillable
and define the relationship:
app/Models/User.php:
class User extends Authenticatable{ // ... protected $fillable = [ 'name', 'email', 'password', 'role_id', ]; public function role() { return $this->belongsTo(Role::class); }}
So, at the moment of the registration, our new users will choose whether they are looking for a property to rent, or want to publish their own property for rent.
As for the Administrator, I suggest creating a special Seeder to already have one Administrator in the DB after the project install:
php artisan make:seeder AdminUserSeeder
database/seeders/AdminUserSeeder.php:
use App\Models\User; class AdminUserSeeder extends Seeder{ public function run() { User::create([ 'name' => 'Administrator', 'email' => 'superadmin@booking.com', 'password' => bcrypt('SuperSecretPassword'), 'email_verified_at' => now(), 'role_id' => 1, // Administrator ]); }}
Then, we add both seeders to the main DatabaseSeeder
.
database/seeders/DatabaseSeeder.php:
class DatabaseSeeder extends Seeder{ public function run() { $this->call(RoleSeeder::class); $this->call(AdminUserSeeder::class); }}
And, finally, we can launch the migrations with seeds:
php artisan migrate --seed
Our next goal is to implement the registration API and test that users get their roles correctly.
Registration API: Assign Permissions and Test Them
Let's say that we will have two registration forms: one for a simple user, and another for the property owner. So, let's simulate both of them and write automated tests that would check if users get the correct role/permissions.
For now, let's group them into one endpoint POST /api/auth/register
, because the user registration would be almost identical. In future lessons, we will talk about differences in the profile fields, but both users and property owners are Users with the same fields of name/email/password.
php artisan make:controller Auth/RegisterController
routes/api.php:
Route::post('auth/register', App\Http\Controllers\Auth\RegisterController::class);
We will create only one method and use the Controller as Invokable Single-Action Controller.
app/Http/Controllers/Auth/RegisterController.php:
namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller;use App\Models\User;use Illuminate\Http\Request;use Illuminate\Support\Facades\Hash;use Illuminate\Validation\Rules\Password; class RegisterController extends Controller{ public function __invoke(Request $request) { $request->validate([ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'password' => ['required', 'confirmed', Password::defaults()], 'role_id' => ['required', Rule::in(2, 3)], ]); $user = User::create([ 'name' => $request->name, 'email' => $request->email, 'password' => Hash::make($request->password), 'role_id' => $request->role_id, ]); return response()->json([ 'access_token' => $user->createToken('client')->plainTextToken, ]); }}
Here I'm assuming we use Laravel Sanctum for the Auth, so we just return the access token, as for mobile application, and all the subsequent requests would happen with that as a Bearer Token, for the authentication. If you don't know how that works, check out my course Build Laravel API for Car Parking App: Step-By-Step.
So, after the validation, we can create the User with role_id => 3
, which is the Simple User, according to our Seeder above.
In the validation, we have that Rule::in(2, 3)
hardcoded. You would probably agree that this 2, 3
isn't readable or understandable at the first glance to a new developer, as it's hard to remember which role is ID 2 or 3. Let's introduce a few constants inside a Role model.
class Role extends Model{ const ROLE_ADMINISTRATOR = 1; const ROLE_OWNER = 2; const ROLE_USER = 3; // ...}
Then, in Controller, we can do this:
use App\Models\Role; class RegisterController extends Controller{ public function __invoke(Request $request) { $request->validate([ // ... 'role_id' => ['required', Rule::in(Role::ROLE_OWNER, Role::ROLE_USER)], ]);
More readable, isn't it? You could also create a separate Enum Class for this, but for simplicity, I decided to create those constants in the Model. It's a totally personal preference.
Ok, so we have the user's registration endpoint. Here's how it works in the Postman API client:
Let's test how it works, by building automated test.
Let's prepare our test suite first.
In the default Laravel project, I typically delete two example test files
- tests/Feature/ExampleTest.php
- tests/Unit/ExampleTest.php
Also, I edit the default phpunit.xml
file to uncomment these two lines:
<env name="DB_CONNECTION" value="sqlite"/><env name="DB_DATABASE" value=":memory:"/>
Notice: For demo projects like this one, it's typically fine to use an in-memory database, but in the real-world scenario I often set up a separate testing MySQL database to run tests on.
Ok, and now, let's generate our first test that would check if the registration works.
php artisan make:test AuthTest
tests/Feature/AuthTest.php:
namespace Tests\Feature; use App\Models\Role;use Illuminate\Foundation\Testing\RefreshDatabase;use Tests\TestCase; class AuthTest extends TestCase{ use RefreshDatabase; public function test_registration_fails_with_admin_role() { $response = $this->postJson('/api/auth/register', [ 'name' => 'Valid name', 'email' => 'valid@email.com', 'password' => 'ValidPassword', 'password_confirmation' => 'ValidPassword', 'role_id' => Role::ROLE_ADMINISTRATOR ]); $response->assertStatus(422); } public function test_registration_succeeds_with_owner_role() { $response = $this->postJson('/api/auth/register', [ 'name' => 'Valid name', 'email' => 'valid@email.com', 'password' => 'ValidPassword', 'password_confirmation' => 'ValidPassword', 'role_id' => Role::ROLE_OWNER ]); $response->assertStatus(200)->assertJsonStructure([ 'access_token', ]); } public function test_registration_succeeds_with_user_role() { $response = $this->postJson('/api/auth/register', [ 'name' => 'Valid name', 'email' => 'valid@email.com', 'password' => 'ValidPassword', 'password_confirmation' => 'ValidPassword', 'role_id' => Role::ROLE_USER ]); $response->assertStatus(200)->assertJsonStructure([ 'access_token', ]); }}
As you can see, we have three methods, each of which tests the registration with each of the roles. By the way, see how constants like Role::ROLE_OWNER
paid off here as well, the test code is really readable.
The result: our registration works!
Finally, in this lesson, let's create a few endpoints for the logged-in users and test if we have the correct access to them.
Let's say that our property owner will have access to the list of their properties, and the regular user will have access to their bookings.
So we will create those endpoints, without any functionality for now, just with the Gates
to check the permissions.
Let's add those permissions as a seeder, and group them for more permissions in the future.
php artisan make:seeder PermissionSeeder
database/seeders/PermissionSeeder.php:
namespace Database\Seeders; use App\Models\Permission;use App\Models\Role;use Illuminate\Database\Console\Seeds\WithoutModelEvents;use Illuminate\Database\Seeder; class PermissionSeeder extends Seeder{ /** * Run the database seeds. * * @return void */ public function run() { $allRoles = Role::all()->keyBy('id'); $permissions = [ 'properties-manage' => [Role::ROLE_OWNER], 'bookings-manage' => [Role::ROLE_USER], ]; foreach ($permissions as $key => $roles) { $permission = Permission::create(['name' => $key]); foreach ($roles as $role) { $allRoles[$role]->permissions()->attach($permission->id); } } }}
Now, as we have all the other data in the DB already, we can run this single seeder:
php artisan db:seed --class=PermissionSeeder
And now let's use those permissions on the actual routes.
php artisan make:controller Owner/PropertyControllerphp artisan make:controller User/BookingController
routes/api.php:
Route::post('auth/register', \App\Http\Controllers\Auth\RegisterController::class); Route::middleware('auth:sanctum')->group(function() { // No owner/user grouping, for now, will do it later with more routes Route::get('owner/properties', [\App\Http\Controllers\Owner\PropertyController::class, 'index']); Route::get('user/bookings', [\App\Http\Controllers\User\BookingController::class, 'index']);});
And then let's use our Gates in the Controllers:
app/Http/Controllers/Owner/PropertyController.php:
namespace App\Http\Controllers\Owner; use App\Http\Controllers\Controller; class PropertyController extends Controller{ public function index() { $this->authorize('properties-manage'); // Will implement property management later return response()->json(['success' => true]); }}
app/Http/Controllers/User/BookingController.php:
namespace App\Http\Controllers\User; use App\Http\Controllers\Controller;use Illuminate\Http\Request; class BookingController extends Controller{ public function index() { $this->authorize('bookings-manage'); // Will implement booking management later return response()->json(['success' => true]); }}
Now, for $this->authorize()
to work, we need to actually define the Gates for the application. Currently, the permissions are in our database, but depending on the current user's role, we need to set every Gate to a true/false value for the user.
We will generate a Middleware specifically for that and enable that Middleware to run on every API request.
php artisan make:middleware GateDefineMiddleware
app/Http/Middleware/GateDefineMiddleware.php:
namespace App\Http\Middleware; use App\Models\Permission;use Closure;use Illuminate\Http\Request;use Illuminate\Support\Facades\Gate; class GateDefineMiddleware{ public function handle(Request $request, Closure $next) { if (auth()->check()) { $permissions = Permission::whereHas('roles', function($query) { $query->where('roles.id', auth()->user()->role_id); })->get(); foreach ($permissions as $permission) { Gate::define($permission->name, fn() => true); } } return $next($request); }}
As you can see, we launch Gate::define()
only on those permissions that are assigned to the role_id
of the logged-in user. For other permissions, the Gate will not be defined, so it will return false
when checking for that permission.
We register our middleware to run in the api
group.
app/Http/Kernel.php:
use App\Http\Middleware\GateDefineMiddleware; class Kernel extends HttpKernel{ // ... protected $middlewareGroups = [ 'web' => [ // ... ], 'api' => [ // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 'throttle:api', \Illuminate\Routing\Middleware\SubstituteBindings::class, GateDefineMiddleware::class, ], ]; // ...}
Now, all we need to do to check if the middleware is working is to launch the API endpoints for the user and the property owner separately, with the Bearer Token that we received from the registration endpoint.
Here's how it looks in Postman:
And when access is denied:
Now, automated tests should cover those cases, too. So let's generate one.
php artisan make:test PropertiesTest
tests/Feature/PropertiesTest.php:
namespace Tests\Feature; use App\Models\Role;use App\Models\User;use Illuminate\Foundation\Testing\RefreshDatabase;use Tests\TestCase; class PropertiesTest extends TestCase{ use RefreshDatabase; public function test_property_owner_has_access_to_properties_feature() { $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]); $response = $this->actingAs($owner)->getJson('/api/owner/properties'); $response->assertStatus(200); } public function test_user_does_not_have_access_to_properties_feature() { $owner = User::factory()->create(['role_id' => Role::ROLE_USER]); $response = $this->actingAs($owner)->getJson('/api/owner/properties'); $response->assertStatus(403); }}
At this point, we don't care what that /api/owner/properties
returns inside, we care if it's successful (code 200) or forbidden (code 403).
To make this work, we need to make a change in the base TestCase to run the seeds for the roles/permissions, for our tests. Cause the default RefreshDatabase
will just run the migrations, but not the data seeds.
tests/TestCase.php:
abstract class TestCase extends BaseTestCase{ use CreatesApplication; public function setUp(): void { parent::setUp(); $this->seed(); // THIS IS THE LINE WE NEED }}
Also, in the same fashion, let's generate the identical test to check for the bookings list permissions as a simple user.
php artisan make:test BookingsTest
tests/Feature/BookingsTest.php:
namespace Tests\Feature; use App\Models\Role;use App\Models\User;use Illuminate\Foundation\Testing\RefreshDatabase;use Tests\TestCase; class BookingsTest extends TestCase{ use RefreshDatabase; public function test_user_has_access_to_bookings_feature() { $owner = User::factory()->create(['role_id' => Role::ROLE_USER]); $response = $this->actingAs($owner)->getJson('/api/user/bookings'); $response->assertStatus(200); } public function test_property_owner_does_not_have_access_to_bookings_feature() { $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]); $response = $this->actingAs($owner)->getJson('/api/user/bookings'); $response->assertStatus(403); }}
Now, we launch php artisan test
, and...
We successfully simulated the registration for two user roles and their permissions for specific API endpoints!
Alternative: Spatie Permission Package
There's another popular approach to adding roles/permissions to Laravel projects: package spatie/laravel-permission.
It would replace our own DB structure, and instead of manually attaching/detaching permissions, we will work with the package functions like $user->assignRole('Administrator')
and similar ones.
I've tried it on the same project and committed the code to a separate branch in the repository, let's review the differences and changes I've made.
First, we install the package and publish its assets.
composer require spatie/laravel-permissionphp artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
Then, before we run the migrations, we need to remove our own ones. We don't need our own DB structure for roles/permissions, as the package will have their own migrations for this.
So we delete the migrations for:
- migration for
roles
- migration for
permissions
- migration for pivot
permission_role
table - column
role_id
fromusers
migrations
Then we run php artisan migrate
to get the package tables into our DB.
Next, the Models. Again, we don't need our own models, because the package has its own Role
and Permission
models, with the same field "name" in the structure.
So, we delete the App\Models\Permission.php
file.
With the Role model, it's a bit more tricky, because we've added an extra feature: the constants like Role::ROLE_OWNER
which we use all over our project.
So we leave the Role model inside our project, just instead of extending the Model from Eloquent, we extend the Spatie package Role model.
app/Models/Role.php:
namespace App\Models; class Role extends \Spatie\Permission\Models\Role{ use HasFactory; const ROLE_ADMINISTRATOR = 1; const ROLE_OWNER = 2; const ROLE_USER = 3;}
Finally, we need to add a Trait in the User model:
app/Models/User.php:
use Spatie\Permission\Traits\HasRoles; class User extends Authenticatable{ use HasApiTokens, HasFactory, Notifiable, HasRoles; // ...
Next, the Seeds. They will look a bit different, cause we need to switch from our own DB structure to use the package's features. Only the RoleSeeder
doesn't change at all, the other ones will look like this.
database/seeders/AdminUserSeeder.php:
public function run() { $user = User::create([ 'name' => 'Administrator', 'email' => 'superadmin@booking.com', 'password' => bcrypt('SuperSecretPassword'), 'email_verified_at' => now(), // no more role_id here ]); $user->assignRole('Administrator'); }}
database/seeders/PermissionSeeder.php:
use Spatie\Permission\Models\Permission; // We change only two lines:// - Permission model from the package// - attach() to givePermissionTo() class PermissionSeeder extends Seeder{ public function run() { $allRoles = Role::all()->keyBy('id'); $permissions = [ 'properties-manage' => [Role::ROLE_OWNER], 'bookings-manage' => [Role::ROLE_USER], ]; foreach ($permissions as $key => $roles) { $permission = Permission::create(['name' => $key]); foreach ($roles as $role) { $allRoles[$role]->givePermissionTo($permission); } } }}
Similarly, in the Registration Controller, we need to change assigning role_id
to the $user->assignRole()
.
app/Http/Controllers/Auth/RegisterController.php:
class RegisterController extends Controller{ public function __invoke(Request $request) { // ... $user = User::create([ 'name' => $request->name, 'email' => $request->email, 'password' => Hash::make($request->password), ]); $user->assignRole($request->role_id); // ... }}
Next, we don't need our own Middleware, the Gate::define()
things will be taken over by the package.
- Delete the file
GateDefineMiddleware
- Remove its mention from
app/Http/Kernel.php
Finally, we change our Tests to use the package feature of assignRole()
.
tests/Feature/BookingsTest.php:
class BookingsTest extends TestCase{ public function test_user_has_access_to_bookings_feature() { $user = User::factory()->create()->assignRole(Role::ROLE_USER); $response = $this->actingAs($user)->getJson('/api/user/bookings'); $response->assertStatus(200); } public function test_property_owner_does_not_have_access_to_bookings_feature() { $owner = User::factory()->create()->assignRole(Role::ROLE_OWNER); $response = $this->actingAs($owner)->getJson('/api/user/bookings'); $response->assertStatus(403); }}
tests/Feature/PropertiesTest.php:
class PropertiesTest extends TestCase{ public function test_property_owner_has_access_to_properties_feature() { $owner = User::factory()->create()->assignRole(Role::ROLE_OWNER); $response = $this->actingAs($owner)->getJson('/api/owner/properties'); $response->assertStatus(200); } public function test_user_does_not_have_access_to_properties_feature() { $user = User::factory()->create()->assignRole(Role::ROLE_USER); $response = $this->actingAs($user)->getJson('/api/owner/properties'); $response->assertStatus(403); }}
And if we relaunch php artisan test
, the test should all pass!
Notice that we haven't changed anything in the Owner/User Controllers. It's all still the same: $this->authorize('properties-manage');
and $this->authorize('bookings-manage');
.
That's because Spatie uses the same Gate mechanism as we did before using the package, it just adds its own DB layer on top.
The main difference, however, is the database structure and the queries executed. In the Spatie package case, it's trying to be very flexible, using polymorphic relations and allowing users to have multiple roles (which we don't need, as discussed earlier).
Also, the package is running queries in "live mode" when checking the permissions. In our case, we're running the DB query only once in the Middleware, and then comparing the Gates whenever we need to check them.
Typical DB query without Spatie package - get all permissions by role:
select *from "permissions"where exists ( select * from "roles" inner join "permission_role" on "roles"."id" = "permission_role"."role_id" where "permissions"."id" = "permission_role"."permission_id" and "id" = 3 )
A typical query of the Spatie package - get all permissions by user:
select "permissions".*, "model_has_permissions"."model_id" as "pivot_model_id", "model_has_permissions"."permission_id" as "pivot_permission_id", "model_has_permissions"."model_type" as "pivot_model_type"from "permissions" inner join "model_has_permissions" on "permissions"."id" = "model_has_permissions"."permission_id"where "model_has_permissions"."model_id" = 2 and "model_has_permissions"."model_type" = 'App\Models\User'
So, it's your personal preference whether to use the package or not. I've just shown it to you as an alternative.
I would vote for it in the case when you do need users to have many roles, as it is supported in the package by default.
Again, the code is in the repository branch.
Hi, Regarding the second approach, we need to add HasRoles trait (from spatie package) to User model, otherwise, almost all the tests will be failed
Of course, well spotted! I did it in the repository but forgot to write it in the course lesson. Fixed, thanks!
If I wanted to add specific data in the register method for both the property owner and simple user, I would create two additional reference tables (e.g. property_owners and simple_users) and add the fields userable_id and userable_type to the users table (via morph). Additionally, would it be appropriate to adjust the role_id?
It would work probably but not my favorite approach.
Hi. I stuck on the testing part... These 2 tests returns Expected response status code [200] but received 403: test_user_has_access_to_bookings_feature, test_property_owner_has_access_to_properties_feature.
I did $this->withoutExceptionHandling() and got this message: This action is unauthorized.
But in the PostMan everithing is ok and I got this: { "success": true } and status: 200
Could you explain me how it possible and how to fix this issue? Thank you
Hard to say what you did wrong without seeing the code. If you put your code on GitHub and invite me (username PovilasKorop), I may take a look.
invited
Spent 30 minutes debugging, finally found it. In DatabaseSeeder, you seed users but you don't seed the permissions, PermissionSeeder is missing from there.
Yes, I missed that... Thanks for your help!
Just in case anyone is having
id is ambigous
problem in theGateDefineMiddleware
, you should make sure that the query part looks like so:$query->where('roles.id', auth()->user()->role_id);
So that the where clause is more specific.Probably you should use $query->where('id', auth()->user()->role_id) instead $query->where('roles.id', auth()->user()->role_id)
I did that as the tutorial but i had the ambiguous error as mentioned
What Bless Darah suggested worked like a charm to me.
Thanks everyone for flagging this, I fixed in the article.
don't forget to clear the cache of the configuration sometime you thtink everthing is right and is not workin "php artisan config:clear" can help you if is a cache problem
Hi, I have some issue while using on hitting registation API.
$request->validate([ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'password' => ['required', 'confirmed', Password::defaults()], 'role_id' => ['required', Rule::in(Role::ROLE_OWNER, Role::ROLE_USER)], ]);
I changed it to
$validator = Validator::make($request->all(), [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'password' => ['required', 'confirmed', Password::defaults()], 'role_id' => ['required', Rule::in(Role::ROLE_OWNER, Role::ROLE_USER)], ]);
Now working fine. What do you suggest?
Not sure what "some issue" you had so can't comment on that. But, when returning the error in the API, you MUST return the status code not 200.
In case of validation error, the error should be 422.
If you return 200, API client would understand it as a successful response, without any errors.
Hi I got this error on test. FAIL Tests\Feature\AuthTest ⨯ registration fails with admin role 0.38s ✓ registration succeeds with owner role 0.06s ✓ registration succeeds with user role 0.05s ───────────────────────────────────────────────────────────────────────────────────────────────────────────────── FAILED Tests\Feature\AuthTest > registration fails with admin role Expected response status code [422] but received 200. Failed asserting that 422 is identical to 200.
Please explain this concept.
I can't explain this concept without your code, but it seems like the same error you had in another case - that you return 200 status code in case of error, and you should return a proper validation error with the code something like 422.
hi, please help me, i try run test? but i have a lot of mistakes,
Tests\Feature\PropertiesTest > property owner has access to properties feature QueryException
SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (
testing
.users
, CONSTRAINTusers_role_id _foreign
FOREIGN KEY (role_id
) REFERENCESroles
(id
)) (Connection: mysql, SQL: insert intousers
(name
,email
,email_verified_at
,password
,remember _token
,role_id
,updated_at
,created_at
) values (Sabina Tremblay, brown.alphonso@example.net, 2023-06-28 06:56:17, $2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.where i send scrin?
My guess is that you haven't run the seeders for the Roles.
i run all seed seed RoleSeeder, AdminUserSeeder, PermissionSeeder, but when i test it increment in tables roles increases and id != 1,2,3 id increases with each test 5,6,7,
and we get error
You need to run those seeds as part of the tests, not separately. It should be a part of your RefreshDatabase, please compare your code to my code in the repository where I run seeders in the tests.
thanks, but i take your code and it don't working, problem with test User::factory()->create(['role_id' => Role::ROLE_USER]);
You're saying that you take the code directly from the repository, don't change anything, run tests and they fail? Then I would need to debug it, but no one else complained, and it was working last time I checked.
so it is, i clone your repository and install packages composer, add data base and install migration successfully, but run tests with error, sorry
Sorry, i will be find error, thanks for any help!
Hi, i have got weird problem.
I use "php artisan make:test AuthTest" command. Filled up this file, and when i want use "php artisan test" console return "No tests found".
When i try use "make" command again i recieve "Test already exists".
"No tests found" may mean two things:
Thank you for your help.
I found a solution, the first test function was without the word "test_".
How can i put an image in comment? I have problems and I would like to share it. I do not know how do you send a request to http://booking.test/api/auth/register? Default laravel project runs on localhost and this is the domain where I try to send a request to. When I senda a request to http://127.0.0.1:8000/api/auth/register I get some html code but not an access_token in json format.
When making the API request, you need to add a Header
Accept: application/json
. This tells the API endpoint that the client prefers to get data in JSON format and not HTML.Here's the tutorial from Laravel Daily that may help you: Laravel: API Error Returns HTML and not JSON - How To Fix
Povilas I know this is a long shot but can you do the test in "Pest.php" since this is the up and coming testing tool these days? Also do you have a class using Pest?
I cover PEST at the end of this course: https://laraveldaily.com/course/laravel-testing
wheres complete step at
I was using mysql for testing and noted that in the AuthTest tthe first one ran successfully but the other two failed. Did some disgging and found out that the roles tabes get truncated after the fist test. I did some little tweaking and added $this->artisan('migrate:fresh --seed'); at the beginning of every function now it works fine
For context: From
To
You can open TestCase file and add:
use Illuminate\Foundation\Testing\RefreshDatabase;
and
use RefreshDatabase;
This will reset the database automatically after each test. Running the artisan command manually is not the best practice (that's why the RefreshDatabase exists) :) Hope that helps!
The test I got this, any idea why?! I did 100% same as tutorial in AuthTest
Yeah, your'e getting a server side error when you are supposed to be asserting the 422 client error(for the first case). The server indeed does understand your request, but it cannot process it due to (most of the time) two scenarios:
public function testHomePage() { $response = $this->get('/');
Some of the ways to assert are different if you are using Lumen's Laravel\Lumen\Testing\TestCase vs Laravel's default Illuminate\Foundation\Testing\TestCase.
but im pretty sure if that was the issue you can get the response you need to be able to assert that status
Njuguna Mwangi post is an interesting solution i didnt think about. That also somewhat relates with what I posted earlier.. Sometimes when you run DB tests especially when you're first learning, it can unfortunately have an unitended impact such as your DB data changing. I think theres a trait if im not mistaken that without your intention can make changes to your table and its data.
class SomeTestn use RefreshDatabase;
that one in particular. Sorry for the long post but i felt compelled to answer my theories on this issue cause i went through the same frustrating process when i followed the tutorial to a T
Since that is a 500 error, you need to take a look at
laravel.log
file - it will tell you what the error is.Since you have asked the same question on Discord (I assume, because it's the same error, but different username) - the issue is with missing namespace use in your
RegisterController
. It is in an intermediate step where it was introduced, but you missed it (Double checked that this is in a tutorial too!). Add the namespace there and you should be good to go!ps. A thing that can be learned here is - if your test fails with 500 error, you can go into
laravel.log
and find the stacktrace of the issue itself :)Thank you guys, I think I missed out put the public function role() belongs to in User Model, because I didn't see in Role model to put hasMany users in it, usually it's a pair action (in both Models), so I forgot that part, I guess the error number from there, oh, I forget to say, I git reverted, and follow the tutorial step by step again, I found out that, but this time, it works