feat: implement NotFoundResponse and ServerErrorResponse classes with corresponding tests
Some checks failed
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 4m16s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 4m17s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 4m27s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 4m32s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 4m15s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Failing after 3m1s

This commit is contained in:
2025-12-01 14:55:34 -05:00
parent b5779afde9
commit ba2beca107
8 changed files with 219 additions and 17 deletions

View File

@@ -6,6 +6,7 @@ return [
'app' => [
'log_level' => Env::get('LOG_LEVEL', 'debug'),
'dev_mode' => Env::get('DEV_MODE', false, 'bool'),
],
/**

View File

@@ -106,6 +106,7 @@ services:
DEBUG: 1
REDIS_HOST: redis
DB_HOST: postgres
DEV_MODE: 1
## Kafka and Zookeeper for local development
kafka-ui:

View File

@@ -16,9 +16,10 @@ use Siteworxpro\App\Http\JsonResponseFactory;
use Siteworxpro\App\Http\Middleware\CorsMiddleware;
use Siteworxpro\App\Http\Middleware\JwtMiddleware;
use Siteworxpro\App\Http\Middleware\ScopeMiddleware;
use Siteworxpro\App\Http\Responses\NotFoundResponse;
use Siteworxpro\App\Http\Responses\ServerErrorResponse;
use Siteworxpro\App\Services\Facades\Config;
use Siteworxpro\App\Services\Facades\Logger;
use Siteworxpro\HttpStatus\CodesEnum;
use Spiral\RoadRunner\Http\PSR7Worker;
use Spiral\RoadRunner\Worker;
@@ -112,28 +113,20 @@ class Api
$response = $this->router->handle($request);
$this->worker->respond($response);
} catch (MethodNotAllowedException | NotFoundException) {
$uri = '';
if (isset($request)) {
$uri = $request->getUri()->getPath();
}
$this->worker->respond(
JsonResponseFactory::createJsonResponse(
['status_code' => 404, 'reason_phrase' => 'Not Found'],
CodesEnum::NOT_FOUND
)
JsonResponseFactory::createJsonResponse(new NotFoundResponse($uri))
);
} catch (\Throwable $e) {
Logger::error($e->getMessage());
Logger::error($e->getTraceAsString());
$json = ['status_code' => 500, 'reason_phrase' => 'Server Error'];
if (Config::get("server.dev_mode")) {
$json = [
'status_code' => 500,
'reason_phrase' => 'Server Error',
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
];
}
$this->worker->respond(
JsonResponseFactory::createJsonResponse($json, CodesEnum::INTERNAL_SERVER_ERROR)
JsonResponseFactory::createJsonResponse(new ServerErrorResponse($e))
);
}
}

View File

@@ -187,7 +187,7 @@ class JwtMiddleware extends Middleware
} elseif (str_contains($keyConfig, '.well-known/')) {
$jwt = explode('.', $token);
if (count($jwt) !== 3) {
throw new \RuntimeException('Invalid JWT structure for JWKS key retrieval.');
throw new InvalidTokenStructure('Invalid JWT structure for JWKS key retrieval.');
}
$header = json_decode(base64_decode($jwt[0]), true, 512, JSON_THROW_ON_ERROR);
$keyId = $header['kid'] ?? '0'; // Default to '0' if no kid present

View File

@@ -0,0 +1,40 @@
<?php
namespace Siteworxpro\App\Http\Responses;
use Illuminate\Contracts\Support\Arrayable;
use Siteworxpro\HttpStatus\CodesEnum;
use OpenApi\Attributes as OA;
#[OA\Schema(
schema: 'NotFoundResponse',
properties: [
new OA\Property(
property: 'message',
type: 'string',
example: 'The requested resource /api/resource was not found.'
),
new OA\Property(property: 'status_code', type: 'integer', example: 404),
new OA\Property(
property: 'context',
description: 'Additional context about the not found error.',
type: 'object',
example: '{}'
),
]
)]
readonly class NotFoundResponse implements Arrayable
{
public function __construct(private string $uri, private array $context = [])
{
}
public function toArray(): array
{
return [
'status_code' => CodesEnum::NOT_FOUND->value,
'message' => 'The requested resource ' . $this->uri . ' was not found.',
'context' => $this->context,
];
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Siteworxpro\App\Http\Responses;
use Illuminate\Contracts\Support\Arrayable;
use Siteworxpro\App\Services\Facades\Config;
use Siteworxpro\HttpStatus\CodesEnum;
use OpenApi\Attributes as OA;
#[OA\Schema(
schema: 'ServerErrorResponse',
properties: array(
new OA\Property(property: 'message', type: 'string', example: 'An internal server error occurred.'),
new OA\Property(property: 'status_code', type: 'integer', example: 500),
new OA\Property(
property: 'file',
type: 'string',
example: '/var/www/html/app/Http/Controllers/ExampleController.php'
),
new OA\Property(property: 'line', type: 'integer', example: 42),
new OA\Property(
property: 'trace',
type: 'array',
items: new OA\Items(type: 'string'),
)
)
)]
readonly class ServerErrorResponse implements Arrayable
{
public function __construct(private \Throwable $e, private array $context = [])
{
}
public function toArray(): array
{
if (Config::get('app.dev_mode')) {
return [
'status_code' => $this->e->getCode() != 0 ?
$this->e->getCode() :
CodesEnum::INTERNAL_SERVER_ERROR->value,
'message' => $this->e->getMessage(),
'file' => $this->e->getFile(),
'line' => $this->e->getLine(),
'trace' => $this->e->getTrace(),
'context' => $this->context,
];
}
return [
'status_code' => $this->e->getCode() != 0 ?
$this->e->getCode() :
CodesEnum::INTERNAL_SERVER_ERROR->value,
'message' => 'An internal server error occurred.',
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Siteworxpro\Tests\Http\Responses;
use Siteworxpro\App\Http\Responses\NotFoundResponse;
use Siteworxpro\Tests\Unit;
class NotFoundResponseTest extends Unit
{
public function testToArray(): void
{
$response = new NotFoundResponse('/api/resource', ['key' => 'value']);
$expected = [
'status_code' => 404,
'message' => 'The requested resource /api/resource was not found.',
'context' => ['key' => 'value'],
];
$this->assertEquals($expected, $response->toArray());
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Siteworxpro\Tests\Http\Responses;
use Siteworxpro\App\Http\Responses\ServerErrorResponse;
use Siteworxpro\App\Services\Facades\Config;
use Siteworxpro\Tests\Unit;
class ServerErrorResponseTest extends Unit
{
public function testToArrayInDevMode(): void
{
Config::set('app.dev_mode', true);
try {
// Simulate an exception to generate a server error response
throw new \Exception('A Test Error occurred.');
} catch (\Exception $e) {
$response = new ServerErrorResponse($e, ['operation' => 'data_processing']);
$expected = [
'status_code' => 500,
'message' => 'A Test Error occurred.',
'context' => [
'operation' => 'data_processing'
],
'file' => '/app/tests/Http/Responses/ServerErrorResponseTest.php',
'line' => $e->getLine(),
'trace' => $e->getTrace(),
];
$this->assertEquals($expected, $response->toArray());
}
}
public function testToArrayNotInDevMode(): void
{
try {
throw new \Exception('A Test Error occurred.');
} catch (\Exception $exception) {
$response = new ServerErrorResponse($exception);
$expected = [
'status_code' => 500,
'message' => 'An internal server error occurred.',
];
$this->assertEquals($expected, $response->toArray());
}
}
public function testToArrayIfCodeIsSet(): void
{
try {
throw new \Exception('A Test Error occurred.', 1234);
} catch (\Exception $exception) {
$response = new ServerErrorResponse($exception);
$expected = [
'status_code' => 1234,
'message' => 'An internal server error occurred.',
];
$this->assertEquals($expected, $response->toArray());
}
}
public function testToArrayIfCodeIsSetDevMode(): void
{
Config::set('app.dev_mode', true);
try {
throw new \Exception('A Test Error occurred.', 1234);
} catch (\Exception $exception) {
$response = new ServerErrorResponse($exception);
$expected = [
'status_code' => 1234,
'message' => 'A Test Error occurred.',
'file' => '/app/tests/Http/Responses/ServerErrorResponseTest.php',
'line' => $exception->getLine(),
'trace' => $exception->getTrace(),
'context' => [],
];
$this->assertEquals($expected, $response->toArray());
}
}
}