This commit is contained in:
2025-04-25 20:46:09 -04:00
parent 9bdecb1455
commit e84c7cf9ad
11 changed files with 2056 additions and 55 deletions

13
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,13 @@
stages:
- test
Unit Tests:
rules:
- if: '$CI_PIPELINE_SOURCE == "push"'
when: always
- when: never
stage: test
image: siteworxpro/composer
script:
- echo "Running unit tests..."
- composer run tests:unit

View File

@@ -3,7 +3,8 @@
"type": "project", "type": "project",
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Siteworxpro\\App\\": "src/" "Siteworxpro\\App\\": "src/",
"Siteworxpro\\Tests\\": "tests/"
} }
}, },
"require": { "require": {
@@ -13,6 +14,28 @@
"spiral/roadrunner-http": "^3.5", "spiral/roadrunner-http": "^3.5",
"nyholm/psr7": "^1.8", "nyholm/psr7": "^1.8",
"illuminate/support": "^v12.10.2", "illuminate/support": "^v12.10.2",
"roadrunner-php/app-logger": "^1.2" "roadrunner-php/app-logger": "^1.2",
"siteworxpro/config": "^1.1"
},
"require-dev": {
"phpunit/phpunit": "^12.1",
"mockery/mockery": "^1.6"
},
"scripts": {
"tests:unit": [
"phpunit --colors=always --display-deprecations tests "
]
},
"repositories": {
"git.siteworxpro.com/24": {
"type": "composer",
"url": "https://git.siteworxpro.com/api/v4/group/24/-/packages/composer/packages.json",
"options": {
"ssl": {
"verify_peer": false,
"allow_self_signed": true
}
}
}
} }
} }

1785
composer.lock generated

File diff suppressed because it is too large Load Diff

24
config.php Normal file
View File

@@ -0,0 +1,24 @@
<?php
use Siteworxpro\App\Helpers\Env;
return [
/**
* The server configuration.
*/
'server' => [
'port' => Env::get('HTTP_PORT', 9501),
],
/**
* The database configuration.
*/
'db' => [
'driver' => Env::get('DB_DRIVER', 'pgsql'),
'host' => Env::get('DB_HOST', 'localhost'),
'database' => Env::get('DB_DATABASE', 'siteworxpro'),
'username' => Env::get('DB_USERNAME', 'siteworxpro'),
'password' => Env::get('DB_PASSWORD', 'password'),
]
];

View File

@@ -5,6 +5,9 @@ declare(strict_types=1);
namespace Siteworxpro\App\Facades; namespace Siteworxpro\App\Facades;
use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\Facade;
use Siteworx\Config\Exception\EmptyDirectoryException;
use Siteworx\Config\Exception\FileNotFoundException;
use Siteworx\Config\Exception\UnsupportedFormatException;
/** /**
* Class Config * Class Config
@@ -12,24 +15,20 @@ use Illuminate\Support\Facades\Facade;
* This class serves as a facade for the configuration settings of the application. * This class serves as a facade for the configuration settings of the application.
* It extends the Facade class from the Illuminate\Support\Facades namespace. * It extends the Facade class from the Illuminate\Support\Facades namespace.
* *
* @method static string|int|bool|float get(string $key, string $castTo = 'string') Retrieve the configuration value for the given key. * @method static string|int|bool|float get(string $key) Retrieve the configuration value for the given key.
* *
* @package Siteworx\App\Facades * @package Siteworx\App\Facades
*/ */
class Config extends Facade class Config extends Facade
{ {
public const string HTTP_PORT = 'HTTP_PORT'; /**
public const string LOG_LEVEL = 'LOG_LEVEL'; * @throws UnsupportedFormatException
public const string DB_DRIVER = 'DB_DRIVER'; * @throws FileNotFoundException
public const string DB_HOST = 'DB_HOST'; * @throws EmptyDirectoryException
public const string DB_DATABASE = 'DB_DATABASE'; */
public const string DB_USER = 'DB_USER'; public static function getFacadeRoot(): \Siteworx\Config\Config
public const string DB_PASSWORD = 'DB_PASSWORD';
public const string DEV_MODE = 'DEV_MODE';
public static function getFacadeRoot(): \Siteworxpro\App\Helpers\Config
{ {
return new \Siteworxpro\App\Helpers\Config(); return \Siteworx\Config\Config::load(__DIR__ . '/../../config.php');
} }
/** /**
@@ -39,6 +38,6 @@ class Config extends Facade
*/ */
protected static function getFacadeAccessor(): string protected static function getFacadeAccessor(): string
{ {
return \Siteworxpro\App\Helpers\Config::class; return 'config';
} }
} }

View File

@@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Helpers;
use Psr\Log\LogLevel;
use Siteworxpro\App\Facades\Config as FacadeConfig;
class Config
{
private const array DEFAULTS = [
FacadeConfig::DB_DRIVER => 'pgsql',
FacadeConfig::DB_HOST => 'localhost',
FacadeConfig::DB_DATABASE => 'siteworx',
FacadeConfig::DB_USER => 'siteworx',
FacadeConfig::DB_PASSWORD => 'password',
FacadeConfig::LOG_LEVEL => LogLevel::DEBUG,
FacadeConfig::HTTP_PORT => '9501',
FacadeConfig::DEV_MODE => true,
];
/**
* @param string $key
* @param string $castTo
* @return string|int|bool|float
*/
public function get(string $key, string $castTo = 'string'): string|int|bool|float
{
return Env::get($key, self::DEFAULTS[$key] ?? null, $castTo);
}
}

View File

@@ -84,11 +84,11 @@ class Server
{ {
$capsule = new \Illuminate\Database\Capsule\Manager(); $capsule = new \Illuminate\Database\Capsule\Manager();
$capsule->addConnection([ $capsule->addConnection([
'driver' => Config::get(Config::DB_DRIVER), 'driver' => Config::get('db.driver'),
'host' => Config::get(Config::DB_HOST), 'host' => Config::get('db.host'),
'database' => Config::get(Config::DB_DATABASE), 'database' => Config::get('db.database'),
'username' => Config::get(Config::DB_USER), 'username' => Config::get('db.username'),
'password' => Config::get(Config::DB_PASSWORD), 'password' => Config::get('db.password'),
'charset' => 'utf8', 'charset' => 'utf8',
'collation' => 'utf8_unicode_ci', 'collation' => 'utf8_unicode_ci',
'prefix' => '', 'prefix' => '',
@@ -128,7 +128,7 @@ class Server
{ {
Logger::info(sprintf('Server started: %s', microtime(true))); Logger::info(sprintf('Server started: %s', microtime(true)));
Logger::info(sprintf('Server PID: %s', getmypid())); Logger::info(sprintf('Server PID: %s', getmypid()));
Logger::info(sprintf('Server Listening on: %s:%s', Config::get('HTTP_HOST'), Config::get('HTTP_PORT'))); Logger::info(sprintf('Server Listening on: 0.0.0.0:%s', Config::get('server.port')));
while (true) { while (true) {
try { try {

46
tests/Helpers/EnvTest.php Normal file
View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\Tests\Helpers;
use Siteworxpro\App\Helpers\Env;
use Siteworxpro\Tests\Unit;
class EnvTest extends Unit
{
public function testGetReturnsStringByDefault(): void
{
putenv('TEST_KEY=example');
$result = Env::get('TEST_KEY');
$this->assertSame('example', $result);
}
public function testGetReturnsDefaultIfKeyNotSet(): void
{
putenv('TEST_KEY'); // Unset the environment variable
$result = Env::get('TEST_KEY', 'default_value');
$this->assertSame('default_value', $result);
}
public function testGetCastsToBoolean(): void
{
putenv('TEST_KEY=true');
$result = Env::get('TEST_KEY', null, 'bool');
$this->assertTrue($result);
}
public function testGetCastsToInteger(): void
{
putenv('TEST_KEY=123');
$result = Env::get('TEST_KEY', null, 'int');
$this->assertSame(123, $result);
}
public function testGetCastsToFloat(): void
{
putenv('TEST_KEY=123.45');
$result = Env::get('TEST_KEY', null, 'float');
$this->assertSame(123.45, $result);
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\Tests\Http;
use PHPUnit\Framework\TestCase;
use Siteworxpro\App\Http\JsonResponseFactory;
class JsonResponseFactoryTest extends TestCase
{
public function testCreateJsonResponseReturnsValidResponse(): void
{
$data = ['key' => 'value'];
$statusCode = 200;
$response = JsonResponseFactory::createJsonResponse($data, $statusCode);
$this->assertSame($statusCode, $response->getStatusCode());
$this->assertSame('application/json', $response->getHeaderLine('Content-Type'));
$this->assertSame(json_encode($data), (string) $response->getBody());
}
public function testCreateJsonResponseHandlesEmptyData(): void
{
$data = [];
$statusCode = 204;
$response = JsonResponseFactory::createJsonResponse($data, $statusCode);
$this->assertSame($statusCode, $response->getStatusCode());
$this->assertSame('application/json', $response->getHeaderLine('Content-Type'));
$this->assertSame(json_encode($data), (string) $response->getBody());
}
public function testCreateJsonResponseThrowsExceptionOnInvalidData(): void
{
$this->expectException(\JsonException::class);
$data = ["invalid" => "\xB1\x31"];
JsonResponseFactory::createJsonResponse($data);
}
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\Tests\Http\Middleware;
use Nyholm\Psr7\Response;
use Nyholm\Psr7\ServerRequest;
use Psr\Http\Server\RequestHandlerInterface;
use Siteworxpro\App\Facades\Config;
use Siteworxpro\App\Http\Middleware\CorsMiddleware;
use Siteworxpro\Tests\Unit;
class CorsMiddlewareTest extends Unit
{
public function testAllowsConfiguredOrigin(): void
{
Config::shouldReceive('get')
->with('CORS_ALLOWED_ORIGINS', 'https://example.com,https://another.com')
->andReturn('https://example.com,https://another.com');
$middleware = new CorsMiddleware();
$request = new ServerRequest('GET', '/')->withHeader('Origin', 'https://example.com');
$handler = $this->mockHandler(new Response(200));
$response = $middleware->process($request, $handler);
$this->assertEquals('https://example.com', $response->getHeaderLine('Access-Control-Allow-Origin'));
}
public function testBlocksUnconfiguredOrigin(): void
{
Config::shouldReceive('get')
->with('CORS_ALLOWED_ORIGINS', 'https://example.com,https://another.com')
->andReturn('https://example.com,https://another.com');
$middleware = new CorsMiddleware();
$request = new ServerRequest('GET', '/')->withHeader('Origin', 'https://unauthorized.com');
$handler = $this->mockHandler(new Response(200));
$response = $middleware->process($request, $handler);
$this->assertEquals('null', $response->getHeaderLine('Access-Control-Allow-Origin'));
}
public function testHandlesOptionsRequest(): void
{
Config::shouldReceive('get')->with('CORS_ALLOWED_ORIGINS', '...')->andReturn('https://example.com');
Config::shouldReceive('get')->with('CORS_ALLOW_CREDENTIALS', 'bool')->andReturn(false);
Config::shouldReceive('get')->with('CORS_MAX_AGE')->andReturn('86400');
$middleware = new CorsMiddleware();
$request = new ServerRequest('OPTIONS', '/')->withHeader('Origin', 'https://example.com');
$handler = $this->mockHandler(new Response(200));
$response = $middleware->process($request, $handler);
$this->assertEquals(204, $response->getStatusCode());
$this->assertEquals('86400', $response->getHeaderLine('Access-Control-Max-Age'));
}
public function testAddsAllowCredentialsHeader(): void
{
Config::shouldReceive('get')->with('CORS_ALLOWED_ORIGINS', '...')->andReturn('https://example.com');
Config::shouldReceive('get')->with('CORS_ALLOW_CREDENTIALS', 'bool')->andReturn(true);
$middleware = new CorsMiddleware();
$request = new ServerRequest('GET', '/')->withHeader('Origin', 'https://example.com');
$handler = $this->mockHandler(new Response(200));
$response = $middleware->process($request, $handler);
$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;
}
};
}
}

12
tests/Unit.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\Tests;
use PHPUnit\Framework\TestCase;
abstract class Unit extends TestCase
{
}