You've already forked Php-Template
more tests #19
@@ -2,7 +2,7 @@
|
|||||||
<configuration default="false" name="Main" type="PHPUnitRunConfigurationType" factoryName="PHPUnit">
|
<configuration default="false" name="Main" type="PHPUnitRunConfigurationType" factoryName="PHPUnit">
|
||||||
<CommandLine>
|
<CommandLine>
|
||||||
<PhpTestInterpreterSettings>
|
<PhpTestInterpreterSettings>
|
||||||
<option name="interpreterName" value="composer-runtime" />
|
<option name="interpreterName" value="dev-runtime" />
|
||||||
</PhpTestInterpreterSettings>
|
</PhpTestInterpreterSettings>
|
||||||
</CommandLine>
|
</CommandLine>
|
||||||
<TestRunner configuration_file="$PROJECT_DIR$/phpunit.xml" scope="XML" use_alternative_configuration_file="true" />
|
<TestRunner configuration_file="$PROJECT_DIR$/phpunit.xml" scope="XML" use_alternative_configuration_file="true" />
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"lcobucci/jwt": "^5.6",
|
"lcobucci/jwt": "^5.6",
|
||||||
"adhocore/cli": "^1.9",
|
"adhocore/cli": "^1.9",
|
||||||
"robinvdvleuten/ulid": "^5.0",
|
"robinvdvleuten/ulid": "^5.0",
|
||||||
"monolog/monolog": "^3.9"
|
"monolog/monolog": "^3.9",
|
||||||
|
"react/promise": "^3",
|
||||||
|
"react/async": "^4"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^12.4",
|
"phpunit/phpunit": "^12.4",
|
||||||
|
|||||||
222
composer.lock
generated
222
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "7c2d40400d6f4d0469324dc1645eba3c",
|
"content-hash": "3554ee67a6c8a798d673b42dc9de3093",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "adhocore/cli",
|
"name": "adhocore/cli",
|
||||||
@@ -1798,6 +1798,226 @@
|
|||||||
},
|
},
|
||||||
"time": "2021-10-29T13:26:27+00:00"
|
"time": "2021-10-29T13:26:27+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "react/async",
|
||||||
|
"version": "v4.3.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/reactphp/async.git",
|
||||||
|
"reference": "635d50e30844a484495713e8cb8d9e079c0008a5"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/reactphp/async/zipball/635d50e30844a484495713e8cb8d9e079c0008a5",
|
||||||
|
"reference": "635d50e30844a484495713e8cb8d9e079c0008a5",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1",
|
||||||
|
"react/event-loop": "^1.2",
|
||||||
|
"react/promise": "^3.2 || ^2.8 || ^1.2.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "1.10.39",
|
||||||
|
"phpunit/phpunit": "^9.6"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/functions_include.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"React\\Async\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Christian Lück",
|
||||||
|
"email": "christian@clue.engineering",
|
||||||
|
"homepage": "https://clue.engineering/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cees-Jan Kiewiet",
|
||||||
|
"email": "reactphp@ceesjankiewiet.nl",
|
||||||
|
"homepage": "https://wyrihaximus.net/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jan Sorgalla",
|
||||||
|
"email": "jsorgalla@gmail.com",
|
||||||
|
"homepage": "https://sorgalla.com/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chris Boden",
|
||||||
|
"email": "cboden@gmail.com",
|
||||||
|
"homepage": "https://cboden.dev/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Async utilities and fibers for ReactPHP",
|
||||||
|
"keywords": [
|
||||||
|
"async",
|
||||||
|
"reactphp"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/reactphp/async/issues",
|
||||||
|
"source": "https://github.com/reactphp/async/tree/v4.3.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://opencollective.com/reactphp",
|
||||||
|
"type": "open_collective"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2024-06-04T14:40:02+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "react/event-loop",
|
||||||
|
"version": "v1.5.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/reactphp/event-loop.git",
|
||||||
|
"reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354",
|
||||||
|
"reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=5.3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-pcntl": "For signal handling support when using the StreamSelectLoop"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"React\\EventLoop\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Christian Lück",
|
||||||
|
"email": "christian@clue.engineering",
|
||||||
|
"homepage": "https://clue.engineering/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cees-Jan Kiewiet",
|
||||||
|
"email": "reactphp@ceesjankiewiet.nl",
|
||||||
|
"homepage": "https://wyrihaximus.net/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jan Sorgalla",
|
||||||
|
"email": "jsorgalla@gmail.com",
|
||||||
|
"homepage": "https://sorgalla.com/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chris Boden",
|
||||||
|
"email": "cboden@gmail.com",
|
||||||
|
"homepage": "https://cboden.dev/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.",
|
||||||
|
"keywords": [
|
||||||
|
"asynchronous",
|
||||||
|
"event-loop"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/reactphp/event-loop/issues",
|
||||||
|
"source": "https://github.com/reactphp/event-loop/tree/v1.5.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://opencollective.com/reactphp",
|
||||||
|
"type": "open_collective"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2023-11-13T13:48:05+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "react/promise",
|
||||||
|
"version": "v3.3.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/reactphp/promise.git",
|
||||||
|
"reference": "23444f53a813a3296c1368bb104793ce8d88f04a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a",
|
||||||
|
"reference": "23444f53a813a3296c1368bb104793ce8d88f04a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.1.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "1.12.28 || 1.4.10",
|
||||||
|
"phpunit/phpunit": "^9.6 || ^7.5"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/functions_include.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"React\\Promise\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jan Sorgalla",
|
||||||
|
"email": "jsorgalla@gmail.com",
|
||||||
|
"homepage": "https://sorgalla.com/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Christian Lück",
|
||||||
|
"email": "christian@clue.engineering",
|
||||||
|
"homepage": "https://clue.engineering/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cees-Jan Kiewiet",
|
||||||
|
"email": "reactphp@ceesjankiewiet.nl",
|
||||||
|
"homepage": "https://wyrihaximus.net/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chris Boden",
|
||||||
|
"email": "cboden@gmail.com",
|
||||||
|
"homepage": "https://cboden.dev/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A lightweight implementation of CommonJS Promises/A for PHP",
|
||||||
|
"keywords": [
|
||||||
|
"promise",
|
||||||
|
"promises"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/reactphp/promise/issues",
|
||||||
|
"source": "https://github.com/reactphp/promise/tree/v3.3.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://opencollective.com/reactphp",
|
||||||
|
"type": "open_collective"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-08-19T18:57:03+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "roadrunner-php/app-logger",
|
"name": "roadrunner-php/app-logger",
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ use Illuminate\Contracts\Support\Arrayable;
|
|||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Siteworxpro\App\Attributes\Events\ListensFor;
|
use Siteworxpro\App\Attributes\Events\ListensFor;
|
||||||
|
|
||||||
|
use function React\Async\await;
|
||||||
|
use function React\Async\coroutine;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Dispatcher
|
* Class Dispatcher
|
||||||
*
|
*
|
||||||
@@ -29,6 +32,8 @@ class Dispatcher implements DispatcherContract, Arrayable
|
|||||||
*/
|
*/
|
||||||
private Collection $pushed;
|
private Collection $pushed;
|
||||||
|
|
||||||
|
private array $subscribers = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string LISTENERS_NAMESPACE The namespace where listeners are located
|
* @var string LISTENERS_NAMESPACE The namespace where listeners are located
|
||||||
*/
|
*/
|
||||||
@@ -99,7 +104,7 @@ class Dispatcher implements DispatcherContract, Arrayable
|
|||||||
*/
|
*/
|
||||||
public function subscribe($subscriber): void
|
public function subscribe($subscriber): void
|
||||||
{
|
{
|
||||||
$this->listeners = array_merge($this->listeners, (array) $subscriber);
|
$this->subscribers[] = $subscriber;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,6 +113,7 @@ class Dispatcher implements DispatcherContract, Arrayable
|
|||||||
* @param $event
|
* @param $event
|
||||||
* @param array $payload
|
* @param array $payload
|
||||||
* @return array|null
|
* @return array|null
|
||||||
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function until($event, $payload = []): array|null
|
public function until($event, $payload = []): array|null
|
||||||
{
|
{
|
||||||
@@ -121,6 +127,7 @@ class Dispatcher implements DispatcherContract, Arrayable
|
|||||||
* @param array $payload
|
* @param array $payload
|
||||||
* @param bool $halt
|
* @param bool $halt
|
||||||
* @return array|null
|
* @return array|null
|
||||||
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function dispatch($event, $payload = [], $halt = false): array|null
|
public function dispatch($event, $payload = [], $halt = false): array|null
|
||||||
{
|
{
|
||||||
@@ -130,23 +137,46 @@ class Dispatcher implements DispatcherContract, Arrayable
|
|||||||
$eventClass = $event;
|
$eventClass = $event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle subscribers as a coroutine
|
||||||
|
$promise = coroutine(function () use ($event, $payload, $halt, $eventClass, &$responses) {
|
||||||
|
foreach ($this->subscribers as $subscriber) {
|
||||||
|
if (method_exists($subscriber, 'handle')) {
|
||||||
|
$response = $subscriber->handle($event, $payload);
|
||||||
|
$responses[$eventClass] = $response;
|
||||||
|
|
||||||
|
if ($halt && $response !== null) {
|
||||||
|
return $responses;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
$listeners = $this->listeners[$eventClass] ?? null;
|
$listeners = $this->listeners[$eventClass] ?? null;
|
||||||
|
|
||||||
|
// If no listeners, just await the subscriber promise
|
||||||
if ($listeners === null) {
|
if ($listeners === null) {
|
||||||
return null;
|
return await($promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
$responses = [];
|
$responses = [];
|
||||||
|
|
||||||
foreach ($listeners as $listener) {
|
foreach ($listeners as $listener) {
|
||||||
$response = $listener($event, $payload);
|
$response = $listener($event, $payload);
|
||||||
$responses[] = $response;
|
$responses[$eventClass] = $response;
|
||||||
|
|
||||||
if ($halt && $response !== null) {
|
if ($halt && $response !== null) {
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Await the subscriber promise and merge responses
|
||||||
|
$promiseResponses = await($promise);
|
||||||
|
|
||||||
|
if (is_array($promiseResponses)) {
|
||||||
|
$responses = array_merge($responses, $promiseResponses);
|
||||||
|
}
|
||||||
|
|
||||||
return $responses;
|
return $responses;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +197,7 @@ class Dispatcher implements DispatcherContract, Arrayable
|
|||||||
*
|
*
|
||||||
* @param $event
|
* @param $event
|
||||||
* @return void
|
* @return void
|
||||||
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function flush($event): void
|
public function flush($event): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,12 +18,15 @@ use Siteworxpro\App\Services\Facades\Logger;
|
|||||||
class Connected extends Listener
|
class Connected extends Listener
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param ConnectionEvent $event
|
* @param mixed $event
|
||||||
* @param array $payload
|
* @param array $payload
|
||||||
* @return null
|
* @return null
|
||||||
*/
|
*/
|
||||||
public function __invoke($event, array $payload = []): null
|
public function __invoke(mixed $event, array $payload = []): null
|
||||||
{
|
{
|
||||||
|
if (!($event instanceof ConnectionEvent)) {
|
||||||
|
throw new \TypeError("Invalid event type passed to listener " . static::class);
|
||||||
|
}
|
||||||
|
|
||||||
Logger::info("Database connection event", [get_class($event), $event->connectionName]);
|
Logger::info("Database connection event", [get_class($event), $event->connectionName]);
|
||||||
|
|
||||||
|
|||||||
@@ -22,4 +22,19 @@ class IndexControllerTest extends AbstractController
|
|||||||
$this->assertEquals(200, $response->getStatusCode());
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
$this->assertEquals('{"status_code":200,"message":"Server is running"}', (string)$response->getBody());
|
$this->assertEquals('{"status_code":200,"message":"Server is running"}', (string)$response->getBody());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testPost(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(true);
|
||||||
|
|
||||||
|
$controller = new IndexController();
|
||||||
|
|
||||||
|
$response = $controller->post($this->getMockRequest());
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$this->assertEquals('{"status_code":200,"message":"Server is running"}', (string)$response->getBody());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
tests/Events/Listeners/ConnectedTest.php
Normal file
30
tests/Events/Listeners/ConnectedTest.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests\Events\Listeners;
|
||||||
|
|
||||||
|
use Illuminate\Database\Events\ConnectionEstablished;
|
||||||
|
use Illuminate\Database\Events\ConnectionEvent;
|
||||||
|
use Siteworxpro\App\Events\Listeners\Database\Connected;
|
||||||
|
use Siteworxpro\Tests\Unit;
|
||||||
|
|
||||||
|
class ConnectedTest extends Unit
|
||||||
|
{
|
||||||
|
public function testHandlesEvent()
|
||||||
|
{
|
||||||
|
$this->expectNotToPerformAssertions();
|
||||||
|
|
||||||
|
$connectedEvent = $this->createMock(ConnectionEstablished::class);
|
||||||
|
$listener = new Connected();
|
||||||
|
|
||||||
|
$listener->__invoke($connectedEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsException()
|
||||||
|
{
|
||||||
|
$this->expectException(\TypeError::class);
|
||||||
|
$listener = new Connected();
|
||||||
|
$listener->__invoke(new \stdClass());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ use Siteworxpro\App\Http\Middleware\CorsMiddleware;
|
|||||||
use Siteworxpro\App\Services\Facades\Config;
|
use Siteworxpro\App\Services\Facades\Config;
|
||||||
use Siteworxpro\Tests\Unit;
|
use Siteworxpro\Tests\Unit;
|
||||||
|
|
||||||
class CorsMiddlewareTest extends Unit
|
class CorsMiddlewareTest extends Middleware
|
||||||
{
|
{
|
||||||
public function testAllowsConfiguredOrigin(): void
|
public function testAllowsConfiguredOrigin(): void
|
||||||
{
|
{
|
||||||
@@ -80,22 +80,4 @@ class CorsMiddlewareTest extends Unit
|
|||||||
|
|
||||||
$this->assertEquals('true', $response->getHeaderLine('Access-Control-Allow-Credentials'));
|
$this->assertEquals('true', $response->getHeaderLine('Access-Control-Allow-Credentials'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function mockHandler(Response $response): RequestHandlerInterface
|
|
||||||
{
|
|
||||||
return new class ($response) implements RequestHandlerInterface {
|
|
||||||
private Response $response;
|
|
||||||
|
|
||||||
public function __construct(Response $response)
|
|
||||||
{
|
|
||||||
$this->response = $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle(
|
|
||||||
\Psr\Http\Message\ServerRequestInterface $request
|
|
||||||
): \Psr\Http\Message\ResponseInterface {
|
|
||||||
return $this->response;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
255
tests/Http/Middleware/JwtMiddlewareTest.php
Normal file
255
tests/Http/Middleware/JwtMiddlewareTest.php
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests\Http\Middleware;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Lcobucci\JWT\JwtFacade;
|
||||||
|
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||||
|
use Lcobucci\JWT\Signer\Key\InMemory;
|
||||||
|
use Lcobucci\JWT\Token\Builder;
|
||||||
|
use League\Route\Dispatcher;
|
||||||
|
use Nyholm\Psr7\Response;
|
||||||
|
use Nyholm\Psr7\ServerRequest;
|
||||||
|
use Siteworxpro\App\Attributes\Guards\Jwt;
|
||||||
|
use Siteworxpro\App\Http\Middleware\JwtMiddleware;
|
||||||
|
use Siteworxpro\App\Services\Facades\Config;
|
||||||
|
use Siteworxpro\HttpStatus\CodesEnum;
|
||||||
|
|
||||||
|
class JwtMiddlewareTest extends Middleware
|
||||||
|
{
|
||||||
|
private const string TEST_SIGNING_KEY = 'test_signing_key_123456444478901234';
|
||||||
|
|
||||||
|
public function getClass(): object
|
||||||
|
{
|
||||||
|
return new class {
|
||||||
|
public function getCallable(): array
|
||||||
|
{
|
||||||
|
return [$this, 'index'];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Jwt]
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Dummy method for testing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Config::set('jwt.signing_key', self::TEST_SIGNING_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testIgnoresNoJwtAttribute()
|
||||||
|
{
|
||||||
|
$class = new class {
|
||||||
|
public function getCallable(): array
|
||||||
|
{
|
||||||
|
return [ $this, 'index' ];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Dummy method for testing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$handler
|
||||||
|
->shouldReceive('handle')
|
||||||
|
->once()
|
||||||
|
->andReturn(new Response(200));
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/');
|
||||||
|
$middleware = new JwtMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(CodesEnum::OK->value, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testIgnoresJwtAttributeButNoToken()
|
||||||
|
{
|
||||||
|
$class = $this->getClass();
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$handler
|
||||||
|
->shouldReceive('handle')
|
||||||
|
->once()
|
||||||
|
->andReturn(new Response(200));
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/');
|
||||||
|
$middleware = new JwtMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(CodesEnum::UNAUTHORIZED->value, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testInvalidToken()
|
||||||
|
{
|
||||||
|
$class = $this->getClass();
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$handler
|
||||||
|
->shouldReceive('handle')
|
||||||
|
->once()
|
||||||
|
->andReturn(new Response(200));
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/');
|
||||||
|
$request = $request->withHeader('Authorization', 'Bearer ' . 'invalid_token_string');
|
||||||
|
$middleware = new JwtMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(CodesEnum::UNAUTHORIZED->value, $response->getStatusCode());
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
'Unauthorized: Invalid token',
|
||||||
|
$response->getBody()->getContents()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testJwtAttributeWithTokenButWrongAud()
|
||||||
|
{
|
||||||
|
$class = $this->getClass();
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$handler
|
||||||
|
->shouldReceive('handle')
|
||||||
|
->once()
|
||||||
|
->andReturn(new Response(200));
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/');
|
||||||
|
$request = $request->withHeader('Authorization', 'Bearer ' . $this->getJwt());
|
||||||
|
$middleware = new JwtMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(CodesEnum::UNAUTHORIZED->value, $response->getStatusCode());
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
'The token is not allowed to be used by this audience',
|
||||||
|
$response->getBody()->getContents()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testJwtAttributeWithTokenButWrongIss()
|
||||||
|
{
|
||||||
|
Config::set('jwt.audience', 'https://client-app.io');
|
||||||
|
|
||||||
|
$class = $this->getClass();
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$handler
|
||||||
|
->shouldReceive('handle')
|
||||||
|
->once()
|
||||||
|
->andReturn(new Response(200));
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/');
|
||||||
|
$request = $request->withHeader('Authorization', 'Bearer ' . $this->getJwt());
|
||||||
|
$middleware = new JwtMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(CodesEnum::UNAUTHORIZED->value, $response->getStatusCode());
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
'The token was not issued by the given issuers',
|
||||||
|
$response->getBody()->getContents()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testJwtAttributeWithTokenWithDiffIssuer()
|
||||||
|
{
|
||||||
|
Config::set('jwt.audience', 'https://client-app.io');
|
||||||
|
Config::set('jwt.issuer', 'https://different-issuer.io');
|
||||||
|
|
||||||
|
$class = $this->getClass();
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$handler
|
||||||
|
->shouldReceive('handle')
|
||||||
|
->once()
|
||||||
|
->andReturn(new Response(200));
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/');
|
||||||
|
$request = $request->withHeader('Authorization', 'Bearer ' . $this->getJwt());
|
||||||
|
$middleware = new JwtMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(CodesEnum::UNAUTHORIZED->value, $response->getStatusCode());
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
'The token was not issued by the given issuers',
|
||||||
|
$response->getBody()->getContents()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testJwtAttributeWithToken()
|
||||||
|
{
|
||||||
|
Config::set('jwt.audience', 'https://client-app.io');
|
||||||
|
Config::set('jwt.issuer', 'https://api.my-awesome-app.io');
|
||||||
|
|
||||||
|
$class = $this->getClass();
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$handler
|
||||||
|
->shouldReceive('handle')
|
||||||
|
->once()
|
||||||
|
->andReturn(new Response(200));
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/');
|
||||||
|
$request = $request->withHeader('Authorization', 'Bearer ' . $this->getJwt());
|
||||||
|
$middleware = new JwtMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(CodesEnum::OK->value, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getJwt(): string
|
||||||
|
{
|
||||||
|
$key = InMemory::plainText(self::TEST_SIGNING_KEY);
|
||||||
|
$signer = new Sha256();
|
||||||
|
|
||||||
|
$token = new JwtFacade()->issue(
|
||||||
|
$signer,
|
||||||
|
$key,
|
||||||
|
static fn (
|
||||||
|
Builder $builder,
|
||||||
|
DateTimeImmutable $issuedAt
|
||||||
|
): Builder => $builder
|
||||||
|
->issuedBy('https://api.my-awesome-app.io')
|
||||||
|
->permittedFor('https://client-app.io')
|
||||||
|
->expiresAt($issuedAt->modify('+10 minutes'))
|
||||||
|
);
|
||||||
|
|
||||||
|
return $token->toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
32
tests/Http/Middleware/Middleware.php
Normal file
32
tests/Http/Middleware/Middleware.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests\Http\Middleware;
|
||||||
|
|
||||||
|
use Nyholm\Psr7\Response;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use Siteworxpro\Tests\Unit;
|
||||||
|
|
||||||
|
abstract class Middleware extends Unit
|
||||||
|
{
|
||||||
|
protected function mockHandler(Response $response): RequestHandlerInterface
|
||||||
|
{
|
||||||
|
return new class ($response) implements RequestHandlerInterface {
|
||||||
|
private Response $response;
|
||||||
|
|
||||||
|
public function __construct(Response $response)
|
||||||
|
{
|
||||||
|
$this->response = $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(
|
||||||
|
ServerRequestInterface $request
|
||||||
|
): ResponseInterface {
|
||||||
|
return $this->response;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
111
tests/Http/Middleware/ScopeMiddlewareTest.php
Normal file
111
tests/Http/Middleware/ScopeMiddlewareTest.php
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests\Http\Middleware;
|
||||||
|
|
||||||
|
use League\Route\Dispatcher;
|
||||||
|
use Nyholm\Psr7\Response;
|
||||||
|
use Nyholm\Psr7\ServerRequest;
|
||||||
|
use Siteworxpro\App\Attributes\Guards\Scope;
|
||||||
|
use Siteworxpro\App\Http\Middleware\ScopeMiddleware;
|
||||||
|
use Siteworxpro\HttpStatus\CodesEnum;
|
||||||
|
|
||||||
|
class ScopeMiddlewareTest extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws \ReflectionException
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testHandlesNoScopes()
|
||||||
|
{
|
||||||
|
$class = new class {
|
||||||
|
public function getCallable(): array
|
||||||
|
{
|
||||||
|
return [ $this, 'index' ];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Dummy method for testing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$handler
|
||||||
|
->shouldReceive('handle')
|
||||||
|
->once()
|
||||||
|
->andReturn(new Response(200));
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/');
|
||||||
|
$middleware = new ScopeMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \ReflectionException
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testAllowsWithScope()
|
||||||
|
{
|
||||||
|
$class = new class {
|
||||||
|
public function getCallable(): array
|
||||||
|
{
|
||||||
|
return [ $this, 'index' ];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Scope(['admin'])]
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Dummy method for testing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$handler
|
||||||
|
->shouldReceive('handle')
|
||||||
|
->once()
|
||||||
|
->andReturn(new Response(200));
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/')->withAttribute('scopes', ['admin', 'user']);
|
||||||
|
$middleware = new ScopeMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(CodesEnum::OK->value, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \ReflectionException
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function testDisallowsWithScope()
|
||||||
|
{
|
||||||
|
$class = new class {
|
||||||
|
public function getCallable(): array
|
||||||
|
{
|
||||||
|
return [ $this, 'index' ];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Scope(['admin'])]
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Dummy method for testing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$handler = \Mockery::mock(Dispatcher::class);
|
||||||
|
$handler->shouldReceive('getMiddlewareStack')
|
||||||
|
->andReturn([$class]);
|
||||||
|
|
||||||
|
$request = new ServerRequest('GET', '/');
|
||||||
|
$middleware = new ScopeMiddleware();
|
||||||
|
$response = $middleware->process($request, $handler);
|
||||||
|
$this->assertEquals(CodesEnum::FORBIDDEN->value, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user