Back to Course |
Testing in Laravel 9 For Beginners: PHPUnit, Pest, TDD

Testing roles: only Admin can access creating products

In the previous lessons, we protected the products page from unauthenticated users. Now, in this lesson, let's talk about authorization, whether the user is an admin or not.


The Laravel Code

First, we create a migration to add a new field, is_admin to the User table.

php artisan make:migration "add is admin to users table"

database/migrations/xxx_add_is_admin_to_users_table.php:

public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_admin')->default(false);
});
}

app/Models/User.php:

class User extends Authenticatable
{
// ...
 
protected $fillable = [
'name',
'email',
'password',
'is_admin',
];
 
// ...
}

And then, in the products list, we restrict the create button.

resources/views/products/index.blade.php:

// ...
<div class="min-w-full align-middle">
@if (auth()->user()->is_admin)
<a href="{{ route('products.create') }}" class="mb-4 inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 active:bg-gray-900 focus:outline-none focus:border-gray-900 focus:ring ring-gray-300 disabled:opacity-25 transition ease-in-out duration-150">
Add new product
</a>
@endif
 
<table class="min-w-full divide-y divide-gray-200 border">
// ...

This is a protection on the frontend, but of course, we must protect it on the backend. For this, let's create a middleware.

php artisan make:middleware IsAdminMiddleware

app/Http/Middleware/IsAdminMiddleware.php:

class IsAdminMiddleware
{
public function handle(Request $request, Closure $next): Response
{
if (! auth()->user()->is_admin) {
abort(403);
}
 
return $next($request);
}
}

And we need to register it.

app/Http/Kernel.php:

<?php
 
namespace App\Http;
 
use Illuminate\Foundation\Http\Kernel as HttpKernel;
 
class Kernel extends HttpKernel
{
 
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'is_admin' => \App\Http\Middleware\IsAdminMiddleware::class,
];
}

Now, we can use this Middleware in the routes. We will separate routes from the resource. One Route group is for the auth user, and the second is for is_admin.

routes/web.php:

Route::resource('products', ProductController::class)->middleware('auth');
 
Route::middleware('auth')->group(function () {
Route::get('products', [ProductController::class, 'index'])->name('products.index');
 
Route::middleware('is_admin')->group(function () {
Route::get('products/create', [ProductController::class, 'create'])->name('products.create');
Route::post('products', [ProductController::class, 'store'])->name('products.store');
});
});

The new product button is only visible to the user if is_admin is true.

create product button visible for admin

We also need to add methods in the Controller.

app/Http/Controllers/ProductController.php:

class ProductController extends Controller
{
// ...
 
public function create()
{
//
}
 
public function store()
{
//
}
}

The Tests

We will add four tests, two for visual and two for backend restriction.

First, let's add two visual tests. The admin user should see the Add new product button, while others shouldn't.

feature/Tests/ProductsTest.php:

class ProductsTest extends TestCase
{
use RefreshDatabase;
 
// ...
 
public function test_admin_can_see_products_create_button()
{
$admin = User::factory()->create(['is_admin' => true]);
$response = $this->actingAs($admin)->get('/products');
 
$response->assertStatus(200);
$response->assertSee('Add new product');
}
 
public function test_non_admin_cannot_see_products_create_button()
{
$response = $this->actingAs($this->user)->get('/products');
 
$response->assertStatus(200);
$response->assertDontSee('Add new product');
}
 
private function createUser(): User
{
return User::factory()->create();
}
}

We create an admin user manually and overwrite the is_admin value.

products create button visual tests

Next, let's add the tests for accessing the page. If someone knows the URL he might try to access it.

tests/Feature/ProductsTest.php:

class ProductsTest extends TestCase
{
use RefreshDatabase;
 
// ...
 
public function test_admin_can_access_product_create_page()
{
$admin = User::factory()->create(['is_admin' => true]);
$response = $this->actingAs($admin)->get('/products/create');
 
$response->assertStatus(200);
}
 
public function test_non_admin_cannot_access_product_create_page()
{
$response = $this->actingAs($this->user)->get('/products/create');
 
$response->assertStatus(403);
}
 
private function createUser(): User
{
return User::factory()->create();
}
}

The last thing is to refactor the admin user creation. In the createUser method, let's add a parameter isAdmin, which by default will be false.

tests/Feature/ProductsTest.php:

class ProductsTest extends TestCase
{
// ...
 
private function createUser(bool $isAdmin = false): User
{
return User::factory()->create([
'is_admin' => $isAdmin,
]);
}
}

Now, we can have a second private property, $admin, and create the admin user the same way we did with a regular user.

tests/Feature/ProductsTest.php:

class ProductsTest extends TestCase
{
use RefreshDatabase;
 
private User $user;
private User $admin;
 
protected function setUp(): void
{
parent::setUp();
 
$this->user = $this->createUser();
$this->admin = $this->createUser(isAdmin: true);
}
 
// ...
 
public function test_admin_can_see_products_create_button()
{
$admin = User::factory()->create(['is_admin' => true]);
$response = $this->actingAs($admin)->get('/products');
$response = $this->actingAs($this->admin)->get('/products');
 
$response->assertStatus(200);
$response->assertSee('Add new product');
}
 
public function test_non_admin_cannot_see_products_create_button()
{
$response = $this->actingAs($this->user)->get('/products');
 
$response->assertStatus(200);
$response->assertDontSee('Add new product');
}
 
public function test_admin_can_access_product_create_page()
{
$admin = User::factory()->create(['is_admin' => true]);
$response = $this->actingAs($admin)->get('/products/create');
$response = $this->actingAs($this->admin)->get('/products/create');
 
$response->assertStatus(200);
}
 
public function test_non_admin_cannot_access_product_create_page()
{
$response = $this->actingAs($this->user)->get('/products/create');
 
$response->assertStatus(403);
}
 
private function createUser(bool $isAdmin = false): User
{
return User::factory()->create([
'is_admin' => $isAdmin,
]);
}
}

For creating admin, I have added a named parameter isAdmin: for better clearance of what that true means.

products create backend tests


Commit for this lesson