# Component Guide: Middlewares Middleware đứng giữa HTTP Request và ứng dụng, có nhiệm vụ kiểm tra header, xác thực token, kiểm tra role, hoặc biến đổi request trước khi chuyển tiếp cho `$next($request)`. --- ## 1. Bản Chất Kiểm Thử Middleware Một Middleware có hai kịch bản chính cần Unit Test: 1. **Pass-through (Cho phép đi tiếp)**: Điều kiện thỏa mãn -> gọi closure `$next($request)` và trả về response từ `$next`. 2. **Blocked (Chặn lại / Từ chối)**: Điều kiện không thỏa mãn -> trả về `JsonResponse` (401, 403) hoặc `RedirectResponse`, closure `$next` **tuyệt đối không được gọi**. --- ## 2. Mã Mẫu Chuẩn: Middleware Unit Test ### Middleware Nguồn: `App\Http\Middleware\EnsureUserIsAdmin` ```php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; class EnsureUserIsAdmin { public function handle(Request $request, Closure $next): Response { $user = $request->user(); if (!$user || !$user->is_admin) { return response()->json([ 'error' => 'Forbidden. Admin privileges required.' ], Response::HTTP_FORBIDDEN); } return $next($request); } } ``` ### Test Chuẩn: PHPUnit 10+ ```php namespace Tests\Unit\Http\Middleware; use Tests\TestCase; use PHPUnit\Framework\Attributes\Test; use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; use Symfony\Component\HttpFoundation\Response; use App\Http\Middleware\EnsureUserIsAdmin; use App\Models\User; class EnsureUserIsAdminTest extends TestCase { private EnsureUserIsAdmin $middleware; protected function setUp(): void { parent::setUp(); $this->middleware = new EnsureUserIsAdmin(); } #[Test] public function it_allows_request_to_proceed_when_user_is_admin(): void { // Arrange $adminUser = new User(['id' => 1, 'is_admin' => true]); $request = Request::create('/admin/dashboard', 'GET'); $request->setUserResolver(fn () => $adminUser); $nextCalled = false; $next = function ($req) use (&$nextCalled) { $nextCalled = true; return response('Next Passed', Response::HTTP_OK); }; // Act $response = $this->middleware->handle($request, $next); // Assert $this->assertTrue($nextCalled, 'Closure $next should have been called.'); $this->assertSame(Response::HTTP_OK, $response->getStatusCode()); } #[Test] public function it_returns_403_forbidden_when_user_is_not_admin(): void { // Arrange $normalUser = new User(['id' => 2, 'is_admin' => false]); $request = Request::create('/admin/dashboard', 'GET'); $request->setUserResolver(fn () => $normalUser); $nextCalled = false; $next = function () use (&$nextCalled) { $nextCalled = true; return response('Next Passed'); }; // Act $response = $this->middleware->handle($request, $next); // Assert $this->assertFalse($nextCalled, 'Closure $next must NOT be called.'); $this->assertInstanceOf(JsonResponse::class, $response); $this->assertSame(Response::HTTP_FORBIDDEN, $response->getStatusCode()); } #[Test] public function it_returns_403_forbidden_when_no_user_is_authenticated(): void { // Arrange $request = Request::create('/admin/dashboard', 'GET'); $request->setUserResolver(fn () => null); $nextCalled = false; $next = function () use (&$nextCalled) { $nextCalled = true; return response('Next Passed'); }; // Act $response = $this->middleware->handle($request, $next); // Assert $this->assertFalse($nextCalled); $this->assertSame(Response::HTTP_FORBIDDEN, $response->getStatusCode()); } } ```