From b3000cffe8b1a67f0a0afa925891b5eb75d97657 Mon Sep 17 00:00:00 2001 From: vedavith Date: Sun, 14 Jun 2026 11:35:30 +0530 Subject: [PATCH] Add Codecov/CI badges to readme and auth middleware pipeline integration tests --- readme.md | 3 + tests/Http/RouteMiddlewarePipelineTest.php | 153 +++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 tests/Http/RouteMiddlewarePipelineTest.php diff --git a/readme.md b/readme.md index ae52d48..7a9621d 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,8 @@ # EntityForge +[![CI](https://github.com/vedavith/Entity-Forge/actions/workflows/php.yml/badge.svg)](https://github.com/vedavith/Entity-Forge/actions/workflows/php.yml) +[![codecov](https://codecov.io/gh/vedavith/Entity-Forge/graph/badge.svg)](https://codecov.io/gh/vedavith/Entity-Forge) + **EntityForge** is a configuration-driven, multi-tenant SaaS framework built in PHP 8.4. It provides everything needed to build a scalable SaaS backend: JSON-driven code generation, two tenant isolation strategies, automated migrations, an HTTP routing layer with middleware pipeline, and a dependency injection container — all wired together through a single boot cycle. diff --git a/tests/Http/RouteMiddlewarePipelineTest.php b/tests/Http/RouteMiddlewarePipelineTest.php new file mode 100644 index 0000000..a04b69f --- /dev/null +++ b/tests/Http/RouteMiddlewarePipelineTest.php @@ -0,0 +1,153 @@ +router = new Router(); + $this->router->get('/me', function (Request $req): Response { + $user = $req->getAttribute('user'); + return (new Response())->withJson(['email' => $user['email'] ?? null]); + }); + $this->router->get('/open', function (Request $req): Response { + return (new Response())->withJson(['public' => true]); + }); + } + + public function test_auth_middleware_attaches_user_and_handler_reads_it(): void + { + $auth = new class implements AuthMiddlewareInterface { + public function handle(Request $request, callable $next): Response + { + $token = $request->header('Authorization'); + if ($token !== 'Bearer valid-token') { + return (new Response())->withJson(['error' => 'Unauthorized'], 401); + } + return $next($request->withAttribute('user', ['email' => 'alice@example.com'])); + } + }; + + $pipeline = (new Pipeline())->pipe($auth); + $request = new Request( + method: 'GET', + path: '/me', + headers: ['Authorization' => 'Bearer valid-token'] + ); + + $response = $pipeline->run($request, fn(Request $req): Response => $this->router->dispatch($req)); + + $this->assertSame(200, $response->getStatus()); + $this->assertStringContainsString('alice@example.com', $response->getBody()); + } + + public function test_auth_middleware_short_circuits_with_401_on_bad_token(): void + { + $auth = new class implements AuthMiddlewareInterface { + public function handle(Request $request, callable $next): Response + { + $token = $request->header('Authorization'); + if ($token !== 'Bearer valid-token') { + return (new Response())->withJson(['error' => 'Unauthorized'], 401); + } + return $next($request->withAttribute('user', ['email' => 'alice@example.com'])); + } + }; + + $pipeline = (new Pipeline())->pipe($auth); + $request = new Request( + method: 'GET', + path: '/me', + headers: ['Authorization' => 'Bearer wrong'] + ); + + $response = $pipeline->run($request, fn(Request $req): Response => $this->router->dispatch($req)); + + $this->assertSame(401, $response->getStatus()); + $this->assertStringContainsString('Unauthorized', $response->getBody()); + } + + public function test_auth_middleware_does_not_block_routes_it_allows(): void + { + $auth = new class implements AuthMiddlewareInterface { + public function handle(Request $request, callable $next): Response + { + if ($request->path() === '/open') { + return $next($request); + } + $token = $request->header('Authorization'); + if (!$token) { + return (new Response())->withJson(['error' => 'Unauthorized'], 401); + } + return $next($request->withAttribute('user', ['email' => 'user@example.com'])); + } + }; + + $pipeline = (new Pipeline())->pipe($auth); + $request = new Request(method: 'GET', path: '/open'); + + $response = $pipeline->run($request, fn(Request $req): Response => $this->router->dispatch($req)); + + $this->assertSame(200, $response->getStatus()); + $this->assertStringContainsString('public', $response->getBody()); + } + + public function test_multiple_middleware_run_in_order_before_route_handler(): void + { + $log = []; + + $auth = new class ($log) implements AuthMiddlewareInterface { + public function __construct(private array &$log) {} + public function handle(Request $request, callable $next): Response + { + $this->log[] = 'auth'; + return $next($request->withAttribute('user', ['email' => 'bob@example.com'])); + } + }; + + $logger = new class ($log) implements \EntityForge\Http\Middleware\MiddlewareInterface { + public function __construct(private array &$log) {} + public function handle(Request $request, callable $next): Response + { + $this->log[] = 'logger'; + return $next($request); + } + }; + + $pipeline = (new Pipeline())->pipe($auth)->pipe($logger); + $request = new Request(method: 'GET', path: '/me'); + + $response = $pipeline->run($request, fn(Request $req): Response => $this->router->dispatch($req)); + + $this->assertSame(['auth', 'logger'], $log); + $this->assertSame(200, $response->getStatus()); + $this->assertStringContainsString('bob@example.com', $response->getBody()); + } + + public function test_router_returns_404_when_route_not_registered(): void + { + $auth = new class implements AuthMiddlewareInterface { + public function handle(Request $request, callable $next): Response + { + return $next($request->withAttribute('user', ['email' => 'x@example.com'])); + } + }; + + $pipeline = (new Pipeline())->pipe($auth); + $request = new Request(method: 'GET', path: '/nonexistent'); + + $response = $pipeline->run($request, fn(Request $req): Response => $this->router->dispatch($req)); + + $this->assertSame(404, $response->getStatus()); + } +}