Files
agent-unit-testing/docs/components/08-middlewares.md
T
Pham-Tan-Thien 2045c856e9 feat: complete Smartgift AI Laravel unit testing toolkit
- Add 10 core testing guides (setup, AAA principles, PHPUnit/Pest, mocking, anti-patterns, PHP 8.4+, debugging, data factories, advanced edge cases)
- Add 13 component testing guides covering all Laravel layers
- Add standard templates for PHPUnit 10/11 and Pest v2/v3 including ArchitectureTest
- Add Docker smart test runner, dependency installer, and CI/CD templates (GitHub Actions, GitLab CI)
- Add 4 Claude Code skills (laravel-test-setup, laravel-unit-test with --all mode, laravel-test-review, laravel-mock-helper)
- Add single unified installer script (sg-install-unit-testing.sh) with global support, CLI runners, and 1-click updater (sg-test-update)
- Add comprehensive documentation (README.md, USAGE-GUIDE.md)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-07 16:41:37 +07:00

3.9 KiB

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

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+

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());
    }
}