first commit
This commit is contained in:
56
tests/Kernel/BootstrapTest.php
Normal file
56
tests/Kernel/BootstrapTest.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Runtime\Bootstrap;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use ReflectionProperty;
|
||||
use Slim\Factory\AppFactory;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
|
||||
final class BootstrapTest extends TestCase
|
||||
{
|
||||
public function testInitializeInfrastructureReturnsPreloadedContainer(): void
|
||||
{
|
||||
$bootstrap = Bootstrap::create();
|
||||
$container = $this->createStub(ContainerInterface::class);
|
||||
|
||||
$this->setPrivate($bootstrap, 'container', $container);
|
||||
|
||||
self::assertSame($container, $bootstrap->initializeInfrastructure());
|
||||
self::assertSame($container, $bootstrap->getContainer());
|
||||
}
|
||||
|
||||
public function testCreateHttpAppReturnsPreloadedApp(): void
|
||||
{
|
||||
$bootstrap = Bootstrap::create();
|
||||
$app = AppFactory::create();
|
||||
|
||||
$this->setPrivate($bootstrap, 'app', $app);
|
||||
|
||||
self::assertSame($app, $bootstrap->createHttpApp());
|
||||
}
|
||||
|
||||
public function testInitializeReturnsPreloadedApp(): void
|
||||
{
|
||||
$bootstrap = Bootstrap::create();
|
||||
$container = $this->createStub(ContainerInterface::class);
|
||||
$app = AppFactory::create();
|
||||
|
||||
$this->setPrivate($bootstrap, 'container', $container);
|
||||
$this->setPrivate($bootstrap, 'app', $app);
|
||||
|
||||
self::assertSame($app, $bootstrap->initialize());
|
||||
}
|
||||
|
||||
private function setPrivate(Bootstrap $bootstrap, string $property, mixed $value): void
|
||||
{
|
||||
$reflection = new ReflectionProperty($bootstrap, $property);
|
||||
$reflection->setAccessible(true);
|
||||
$reflection->setValue($bootstrap, $value);
|
||||
}
|
||||
}
|
||||
36
tests/Kernel/ClientIpResolverCoverageTest.php
Normal file
36
tests/Kernel/ClientIpResolverCoverageTest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Http\Infrastructure\Request\ClientIpResolver;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Slim\Psr7\Factory\ServerRequestFactory;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
final class ClientIpResolverCoverageTest extends TestCase
|
||||
{
|
||||
public function testResolveReturnsRemoteAddrWhenTrustedProxyHasNoForwardedHeader(): void
|
||||
{
|
||||
$request = (new ServerRequestFactory())->createServerRequest('GET', '/', [
|
||||
'REMOTE_ADDR' => '127.0.0.1',
|
||||
]);
|
||||
|
||||
$resolver = new ClientIpResolver(['127.0.0.1']);
|
||||
|
||||
self::assertSame('127.0.0.1', $resolver->resolve($request));
|
||||
}
|
||||
|
||||
public function testResolveTrimsForwardedIpWhenProxyWildcardIsTrusted(): void
|
||||
{
|
||||
$request = (new ServerRequestFactory())->createServerRequest('GET', '/', [
|
||||
'REMOTE_ADDR' => '10.0.0.1',
|
||||
'HTTP_X_FORWARDED_FOR' => ' 203.0.113.77 , 198.51.100.12',
|
||||
]);
|
||||
|
||||
$resolver = new ClientIpResolver(['*']);
|
||||
|
||||
self::assertSame('203.0.113.77', $resolver->resolve($request));
|
||||
}
|
||||
}
|
||||
58
tests/Kernel/ClientIpResolverTest.php
Normal file
58
tests/Kernel/ClientIpResolverTest.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Http\Infrastructure\Request\ClientIpResolver;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Slim\Psr7\Factory\ServerRequestFactory;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
|
||||
final class ClientIpResolverTest extends TestCase
|
||||
{
|
||||
public function testResolveReturnsDefaultWhenRemoteAddrMissing(): void
|
||||
{
|
||||
$request = (new ServerRequestFactory())->createServerRequest('GET', '/');
|
||||
$resolver = new ClientIpResolver();
|
||||
|
||||
self::assertSame('0.0.0.0', $resolver->resolve($request));
|
||||
}
|
||||
|
||||
public function testResolveReturnsRemoteAddrWhenProxyNotTrusted(): void
|
||||
{
|
||||
$request = (new ServerRequestFactory())->createServerRequest('GET', '/', [
|
||||
'REMOTE_ADDR' => '10.0.0.1',
|
||||
'HTTP_X_FORWARDED_FOR' => '203.0.113.10',
|
||||
]);
|
||||
|
||||
$resolver = new ClientIpResolver(['127.0.0.1']);
|
||||
|
||||
self::assertSame('10.0.0.1', $resolver->resolve($request));
|
||||
}
|
||||
|
||||
public function testResolveReturnsForwardedIpWhenProxyTrusted(): void
|
||||
{
|
||||
$request = (new ServerRequestFactory())->createServerRequest('GET', '/', [
|
||||
'REMOTE_ADDR' => '127.0.0.1',
|
||||
'HTTP_X_FORWARDED_FOR' => '203.0.113.10, 198.51.100.12',
|
||||
]);
|
||||
|
||||
$resolver = new ClientIpResolver(['127.0.0.1']);
|
||||
|
||||
self::assertSame('203.0.113.10', $resolver->resolve($request));
|
||||
}
|
||||
|
||||
public function testResolveFallsBackToRemoteAddrWhenForwardedIpInvalid(): void
|
||||
{
|
||||
$request = (new ServerRequestFactory())->createServerRequest('GET', '/', [
|
||||
'REMOTE_ADDR' => '127.0.0.1',
|
||||
'HTTP_X_FORWARDED_FOR' => 'not-an-ip',
|
||||
]);
|
||||
|
||||
$resolver = new ClientIpResolver(['*']);
|
||||
|
||||
self::assertSame('127.0.0.1', $resolver->resolve($request));
|
||||
}
|
||||
}
|
||||
184
tests/Kernel/ContainerWiringIntegrationTest.php
Normal file
184
tests/Kernel/ContainerWiringIntegrationTest.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use DI\Container;
|
||||
use DI\ContainerBuilder;
|
||||
use Netig\Netslim\AuditLog\Application\AuditLogServiceInterface;
|
||||
use Netig\Netslim\Identity\Application\AuthorizationServiceInterface;
|
||||
use Netig\Netslim\Identity\Application\AuthServiceInterface;
|
||||
use Netig\Netslim\Identity\Application\PasswordResetServiceInterface;
|
||||
use Netig\Netslim\Identity\Application\UserServiceInterface;
|
||||
use Netig\Netslim\Identity\UI\Http\AccountController;
|
||||
use Netig\Netslim\Identity\UI\Http\AuthController;
|
||||
use Netig\Netslim\Identity\UI\Http\PasswordResetController;
|
||||
use Netig\Netslim\Identity\UI\Http\UserController;
|
||||
use Netig\Netslim\Kernel\Http\Infrastructure\Twig\AppExtension;
|
||||
use Netig\Netslim\Kernel\Runtime\Http\MiddlewareRegistrar;
|
||||
use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry;
|
||||
use Netig\Netslim\Kernel\Runtime\RuntimePaths;
|
||||
use Netig\Netslim\Media\Application\MediaServiceInterface;
|
||||
use Netig\Netslim\Media\UI\Http\MediaController;
|
||||
use Netig\Netslim\Notifications\Application\NotificationServiceInterface;
|
||||
use Netig\Netslim\Settings\Application\SettingsServiceInterface;
|
||||
use Netig\Netslim\Taxonomy\Application\TaxonomyServiceInterface;
|
||||
use Netig\Netslim\Taxonomy\Contracts\TaxonomyReaderInterface;
|
||||
use Netig\Netslim\Taxonomy\UI\Http\TaxonomyController;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Slim\Factory\AppFactory;
|
||||
use Slim\Views\Twig;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
|
||||
final class ContainerWiringIntegrationTest extends TestCase
|
||||
{
|
||||
private const ENV_DEFAULTS = [
|
||||
'APP_ENV' => 'development',
|
||||
'APP_URL' => 'http://localhost',
|
||||
'APP_NAME' => 'NETslim Test',
|
||||
'ADMIN_USERNAME' => 'admin',
|
||||
'ADMIN_EMAIL' => 'admin@example.test',
|
||||
'ADMIN_PASSWORD' => 'secret123456',
|
||||
'MAIL_HOST' => 'localhost',
|
||||
'MAIL_PORT' => '1025',
|
||||
'MAIL_USERNAME' => '',
|
||||
'MAIL_PASSWORD' => '',
|
||||
'MAIL_ENCRYPTION' => 'tls',
|
||||
'MAIL_FROM' => 'noreply@example.test',
|
||||
'MAIL_FROM_NAME' => 'NETslim Test',
|
||||
'TIMEZONE' => 'UTC',
|
||||
];
|
||||
|
||||
/** @var array<string, string|null> */
|
||||
private array $envBackup = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application');
|
||||
ModuleRegistry::reset();
|
||||
foreach (self::ENV_DEFAULTS as $key => $value) {
|
||||
$this->envBackup[$key] = $_ENV[$key] ?? null;
|
||||
$_ENV[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
RuntimePaths::resetApplicationRoot();
|
||||
RuntimePaths::resetProjectRoot();
|
||||
ModuleRegistry::reset();
|
||||
foreach (self::ENV_DEFAULTS as $key => $_) {
|
||||
$previous = $this->envBackup[$key] ?? null;
|
||||
|
||||
if ($previous === null) {
|
||||
unset($_ENV[$key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$_ENV[$key] = $previous;
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/** @return iterable<string, array{class-string}> */
|
||||
public static function containerServicesProvider(): iterable
|
||||
{
|
||||
yield 'auth service' => [AuthServiceInterface::class];
|
||||
yield 'password reset service' => [PasswordResetServiceInterface::class];
|
||||
yield 'authorization service' => [AuthorizationServiceInterface::class];
|
||||
yield 'settings service' => [SettingsServiceInterface::class];
|
||||
yield 'audit log service' => [AuditLogServiceInterface::class];
|
||||
yield 'notification service' => [NotificationServiceInterface::class];
|
||||
yield 'taxonomy service' => [TaxonomyServiceInterface::class];
|
||||
yield 'media service' => [MediaServiceInterface::class];
|
||||
yield 'taxonomy reader' => [TaxonomyReaderInterface::class];
|
||||
yield 'user service' => [UserServiceInterface::class];
|
||||
yield 'auth controller' => [AuthController::class];
|
||||
yield 'account controller' => [AccountController::class];
|
||||
yield 'password reset controller' => [PasswordResetController::class];
|
||||
yield 'taxonomy controller' => [TaxonomyController::class];
|
||||
yield 'media controller' => [MediaController::class];
|
||||
yield 'user controller' => [UserController::class];
|
||||
yield 'twig' => [Twig::class];
|
||||
yield 'app extension' => [AppExtension::class];
|
||||
}
|
||||
|
||||
#[DataProvider('containerServicesProvider')]
|
||||
public function testContainerResolvesExpectedServices(string $id): void
|
||||
{
|
||||
$container = $this->buildContainer();
|
||||
|
||||
$resolved = $container->get($id);
|
||||
|
||||
self::assertInstanceOf($id, $resolved);
|
||||
}
|
||||
|
||||
public function testTwigFactoryRegistersModuleTemplateNamespaces(): void
|
||||
{
|
||||
$container = $this->buildContainer();
|
||||
$twig = $container->get(Twig::class);
|
||||
$loader = $twig->getEnvironment()->getLoader();
|
||||
|
||||
self::assertInstanceOf(FilesystemLoader::class, $loader);
|
||||
|
||||
foreach (ModuleRegistry::modules() as $module) {
|
||||
foreach ($module->templateNamespaces() as $namespace => $templatePath) {
|
||||
self::assertContains(
|
||||
$templatePath,
|
||||
$loader->getPaths($namespace),
|
||||
sprintf('Le namespace Twig "%s" doit pointer vers "%s".', $namespace, $templatePath),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testMiddlewareRegistrationLoadsModuleTwigExtensions(): void
|
||||
{
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
session_write_close();
|
||||
}
|
||||
|
||||
session_id('netslim-container-wiring-test');
|
||||
session_start();
|
||||
|
||||
try {
|
||||
$container = $this->buildContainer();
|
||||
AppFactory::setContainer($container);
|
||||
$app = AppFactory::create();
|
||||
$registrar = new MiddlewareRegistrar();
|
||||
|
||||
$registrar->register($app, $container);
|
||||
|
||||
$extensions = $container->get(Twig::class)->getEnvironment()->getExtensions();
|
||||
|
||||
$assertions = 0;
|
||||
|
||||
foreach (ModuleRegistry::modules() as $module) {
|
||||
foreach ($module->twigExtensions() as $twigExtensionClass) {
|
||||
self::assertArrayHasKey(
|
||||
$twigExtensionClass,
|
||||
$extensions,
|
||||
sprintf("L'extension Twig \"%s\" doit être chargée par MiddlewareRegistrar.", $twigExtensionClass),
|
||||
);
|
||||
self::assertInstanceOf($twigExtensionClass, $extensions[$twigExtensionClass]);
|
||||
++$assertions;
|
||||
}
|
||||
}
|
||||
self::assertGreaterThanOrEqual(0, $assertions);
|
||||
} finally {
|
||||
session_write_close();
|
||||
}
|
||||
}
|
||||
|
||||
private function buildContainer(): Container
|
||||
{
|
||||
$builder = new ContainerBuilder();
|
||||
$builder->useAutowiring(true);
|
||||
$builder->addDefinitions(dirname(__DIR__, 2) . '/src/Kernel/Runtime/DI/container.php');
|
||||
|
||||
return $builder->build();
|
||||
}
|
||||
}
|
||||
42
tests/Kernel/DatabaseReadinessTest.php
Normal file
42
tests/Kernel/DatabaseReadinessTest.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Persistence\Infrastructure\DatabaseNotProvisionedException;
|
||||
use Netig\Netslim\Kernel\Persistence\Infrastructure\DatabaseReadiness;
|
||||
use PDO;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class DatabaseReadinessTest extends TestCase
|
||||
{
|
||||
public function testAssertProvisionedFailsWhenModuleTablesAreMissing(): void
|
||||
{
|
||||
$db = new PDO('sqlite::memory:');
|
||||
$db->exec('CREATE TABLE migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, version TEXT, run_at TEXT)');
|
||||
|
||||
$this->expectException(DatabaseNotProvisionedException::class);
|
||||
$this->expectExceptionMessage('users');
|
||||
|
||||
DatabaseReadiness::assertProvisioned($db);
|
||||
}
|
||||
|
||||
public function testAssertProvisionedAcceptsCompleteCoreSchema(): void
|
||||
{
|
||||
$db = new PDO('sqlite::memory:');
|
||||
|
||||
$db->exec('CREATE TABLE migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, version TEXT, run_at TEXT)');
|
||||
$db->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, email TEXT, password_hash TEXT, role TEXT, session_version INTEGER, created_at TEXT)');
|
||||
$db->exec('CREATE TABLE password_resets (id INTEGER PRIMARY KEY, user_id INTEGER, token_hash TEXT, expires_at TEXT, used_at TEXT, created_at TEXT)');
|
||||
$db->exec('CREATE TABLE rate_limits (scope TEXT, rate_key TEXT, attempts INTEGER, locked_until TEXT, updated_at TEXT)');
|
||||
$db->exec('CREATE TABLE settings (setting_key TEXT PRIMARY KEY, setting_value TEXT, value_type TEXT, updated_at TEXT)');
|
||||
$db->exec('CREATE TABLE audit_log (id INTEGER PRIMARY KEY, action TEXT, resource_type TEXT, resource_id TEXT, actor_user_id INTEGER, context_json TEXT, created_at TEXT)');
|
||||
$db->exec('CREATE TABLE notification_dispatches (id INTEGER PRIMARY KEY, recipient TEXT, subject TEXT, template TEXT, status TEXT, notification_key TEXT, error_message TEXT, created_at TEXT, sent_at TEXT)');
|
||||
$db->exec('CREATE TABLE categories (id INTEGER PRIMARY KEY, name TEXT, slug TEXT)');
|
||||
$db->exec('CREATE TABLE media (id INTEGER PRIMARY KEY, filename TEXT, url TEXT, hash TEXT, user_id INTEGER, created_at TEXT)');
|
||||
|
||||
DatabaseReadiness::assertProvisioned($db);
|
||||
$this->addToAssertionCount(1);
|
||||
}
|
||||
}
|
||||
94
tests/Kernel/DateParserTest.php
Normal file
94
tests/Kernel/DateParserTest.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use DateTime;
|
||||
use Netig\Netslim\Kernel\Support\Util\DateParser;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour DateParser::parse().
|
||||
*
|
||||
* Couvre la conversion de valeurs brutes de base de données en DateTime,
|
||||
* ainsi que les cas silencieux (null, chaîne vide, valeur invalide).
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
final class DateParserTest extends TestCase
|
||||
{
|
||||
// ── Valeurs valides ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Une date au format SQLite standard doit produire un DateTime correct.
|
||||
*/
|
||||
public function testStandardSqliteDate(): void
|
||||
{
|
||||
$result = DateParser::parse('2024-06-15 10:30:00');
|
||||
|
||||
$this->assertInstanceOf(DateTime::class, $result);
|
||||
$this->assertSame('2024-06-15', $result->format('Y-m-d'));
|
||||
$this->assertSame('10:30:00', $result->format('H:i:s'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Une date ISO 8601 doit être correctement parsée.
|
||||
*/
|
||||
public function testIso8601Date(): void
|
||||
{
|
||||
$result = DateParser::parse('2024-01-01T00:00:00');
|
||||
|
||||
$this->assertInstanceOf(DateTime::class, $result);
|
||||
$this->assertSame('2024-01-01', $result->format('Y-m-d'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Une date seule (sans heure) doit être parsée correctement.
|
||||
*/
|
||||
public function testDateOnly(): void
|
||||
{
|
||||
$result = DateParser::parse('2024-12-31');
|
||||
|
||||
$this->assertInstanceOf(DateTime::class, $result);
|
||||
$this->assertSame('2024-12-31', $result->format('Y-m-d'));
|
||||
}
|
||||
|
||||
|
||||
// ── Valeurs silencieuses — doit retourner null sans exception ────
|
||||
|
||||
/**
|
||||
* null doit retourner null.
|
||||
*/
|
||||
public function testNullReturnsNull(): void
|
||||
{
|
||||
$this->assertNull(DateParser::parse(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Une chaîne vide doit retourner null.
|
||||
*/
|
||||
public function testEmptyStringReturnsNull(): void
|
||||
{
|
||||
$this->assertNull(DateParser::parse(''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Une valeur non parseable doit retourner null sans lever d'exception.
|
||||
*/
|
||||
public function testInvalidValueReturnsNull(): void
|
||||
{
|
||||
$this->assertNull(DateParser::parse('pas-une-date'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Un entier doit être interprété comme un timestamp si valide,
|
||||
* ou retourner null si la conversion échoue.
|
||||
* Ici on vérifie simplement qu'aucune exception n'est levée.
|
||||
*/
|
||||
public function testIntegerThrowsNoException(): void
|
||||
{
|
||||
$result = DateParser::parse(0);
|
||||
// Pas d'assertion sur la valeur — on vérifie juste la robustesse
|
||||
$this->assertTrue($result === null || $result instanceof DateTime);
|
||||
}
|
||||
}
|
||||
126
tests/Kernel/DefaultErrorHandlerTest.php
Normal file
126
tests/Kernel/DefaultErrorHandlerTest.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Persistence\Infrastructure\DatabaseNotProvisionedException;
|
||||
use Netig\Netslim\Kernel\Runtime\Http\DefaultErrorHandler;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Http\Message\ResponseFactoryInterface;
|
||||
use Slim\Exception\HttpNotFoundException;
|
||||
use Slim\Psr7\Factory\ServerRequestFactory;
|
||||
use Slim\Psr7\Response;
|
||||
use Slim\Views\Twig;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
final class DefaultErrorHandlerTest extends TestCase
|
||||
{
|
||||
/** @var ResponseFactoryInterface&MockObject */
|
||||
private ResponseFactoryInterface $responseFactory;
|
||||
|
||||
/** @var Twig&MockObject */
|
||||
private Twig $twig;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->responseFactory = $this->createMock(ResponseFactoryInterface::class);
|
||||
$this->responseFactory
|
||||
->method('createResponse')
|
||||
->willReturnCallback(static fn (int $statusCode): Response => new Response($statusCode));
|
||||
|
||||
$this->twig = $this->createMock(Twig::class);
|
||||
}
|
||||
|
||||
public function testInvokeRendersFriendlyNotFoundPage(): void
|
||||
{
|
||||
$request = (new ServerRequestFactory())->createServerRequest('GET', '/missing');
|
||||
$exception = new HttpNotFoundException($request);
|
||||
|
||||
$this->twig
|
||||
->expects(self::once())
|
||||
->method('render')
|
||||
->willReturnCallback(function (Response $response, string $template, array $data): Response {
|
||||
self::assertSame(404, $response->getStatusCode());
|
||||
self::assertSame('@Kernel/error.twig', $template);
|
||||
self::assertSame(404, $data['status']);
|
||||
self::assertSame('La page demandée est introuvable.', $data['message']);
|
||||
|
||||
$response->getBody()->write('404: ' . $data['message']);
|
||||
|
||||
return $response;
|
||||
});
|
||||
|
||||
$handler = new DefaultErrorHandler($this->responseFactory, $this->twig, false);
|
||||
$response = $handler($request, $exception, false, true, true);
|
||||
|
||||
self::assertSame(404, $response->getStatusCode());
|
||||
self::assertStringContainsString('404: La page demandée est introuvable.', (string) $response->getBody());
|
||||
}
|
||||
|
||||
public function testInvokeRendersDatabaseProvisioningErrorAs503(): void
|
||||
{
|
||||
$request = (new ServerRequestFactory())->createServerRequest('GET', '/');
|
||||
$exception = new DatabaseNotProvisionedException('La base de données doit être provisionnée.');
|
||||
|
||||
$this->twig
|
||||
->expects(self::once())
|
||||
->method('render')
|
||||
->willReturnCallback(function (Response $response, string $template, array $data): Response {
|
||||
self::assertSame(503, $response->getStatusCode());
|
||||
self::assertSame('@Kernel/error.twig', $template);
|
||||
self::assertSame(503, $data['status']);
|
||||
self::assertSame('La base de données doit être provisionnée.', $data['message']);
|
||||
|
||||
$response->getBody()->write('503: ' . $data['message']);
|
||||
|
||||
return $response;
|
||||
});
|
||||
|
||||
$handler = new DefaultErrorHandler($this->responseFactory, $this->twig, false);
|
||||
$response = $handler($request, $exception, false, true, true);
|
||||
|
||||
self::assertSame(503, $response->getStatusCode());
|
||||
self::assertStringContainsString('503: La base de données doit être provisionnée.', (string) $response->getBody());
|
||||
}
|
||||
|
||||
public function testInvokeDoesNotRethrowHttpExceptionsInDevelopment(): void
|
||||
{
|
||||
$request = (new ServerRequestFactory())->createServerRequest('GET', '/missing');
|
||||
$exception = new HttpNotFoundException($request);
|
||||
|
||||
$this->twig
|
||||
->expects(self::once())
|
||||
->method('render')
|
||||
->willReturnCallback(function (Response $response, string $template, array $data): Response {
|
||||
self::assertSame(404, $response->getStatusCode());
|
||||
self::assertSame('@Kernel/error.twig', $template);
|
||||
self::assertSame(404, $data['status']);
|
||||
self::assertSame('La page demandée est introuvable.', $data['message']);
|
||||
|
||||
$response->getBody()->write('404 dev: ' . $data['message']);
|
||||
|
||||
return $response;
|
||||
});
|
||||
|
||||
$handler = new DefaultErrorHandler($this->responseFactory, $this->twig, true);
|
||||
$response = $handler($request, $exception, true, true, true);
|
||||
|
||||
self::assertSame(404, $response->getStatusCode());
|
||||
self::assertStringContainsString('404 dev: La page demandée est introuvable.', (string) $response->getBody());
|
||||
}
|
||||
|
||||
public function testInvokeRethrowsUnexpectedExceptionsInDevelopment(): void
|
||||
{
|
||||
$request = (new ServerRequestFactory())->createServerRequest('GET', '/boom');
|
||||
$exception = new \RuntimeException('boom');
|
||||
|
||||
$this->twig->expects(self::never())->method('render');
|
||||
|
||||
$handler = new DefaultErrorHandler($this->responseFactory, $this->twig, true);
|
||||
|
||||
$this->expectExceptionObject($exception);
|
||||
$handler($request, $exception, true, true, true);
|
||||
}
|
||||
}
|
||||
108
tests/Kernel/ErrorHandlerConfiguratorTest.php
Normal file
108
tests/Kernel/ErrorHandlerConfiguratorTest.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Persistence\Infrastructure\DatabaseNotProvisionedException;
|
||||
use Netig\Netslim\Kernel\Runtime\Http\ErrorHandlerConfigurator;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Slim\Factory\AppFactory;
|
||||
use Slim\Psr7\Factory\ServerRequestFactory;
|
||||
use Slim\Views\Twig;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
final class ErrorHandlerConfiguratorTest extends TestCase
|
||||
{
|
||||
/** @var Twig&MockObject */
|
||||
private Twig $twig;
|
||||
|
||||
/** @var ContainerInterface&MockObject */
|
||||
private ContainerInterface $container;
|
||||
|
||||
private ?string $originalAppEnv = null;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->originalAppEnv = $_ENV['APP_ENV'] ?? null;
|
||||
$_ENV['APP_ENV'] = 'production';
|
||||
|
||||
$this->twig = $this->createMock(Twig::class);
|
||||
$logger = $this->createStub(LoggerInterface::class);
|
||||
|
||||
$this->container = $this->createMock(ContainerInterface::class);
|
||||
$this->container
|
||||
->method('get')
|
||||
->willReturnMap([
|
||||
[LoggerInterface::class, $logger],
|
||||
[Twig::class, $this->twig],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if ($this->originalAppEnv === null) {
|
||||
unset($_ENV['APP_ENV']);
|
||||
} else {
|
||||
$_ENV['APP_ENV'] = $this->originalAppEnv;
|
||||
}
|
||||
}
|
||||
|
||||
public function testConfigureRegistersHandlerThatRenders404Responses(): void
|
||||
{
|
||||
$app = AppFactory::create();
|
||||
|
||||
$this->twig
|
||||
->expects(self::once())
|
||||
->method('render')
|
||||
->willReturnCallback(function ($response, string $template, array $data) {
|
||||
self::assertSame('@Kernel/error.twig', $template);
|
||||
self::assertSame(404, $data['status']);
|
||||
self::assertSame('La page demandée est introuvable.', $data['message']);
|
||||
|
||||
$response->getBody()->write('configured 404');
|
||||
|
||||
return $response;
|
||||
});
|
||||
|
||||
(new ErrorHandlerConfigurator())->configure($app, $this->container);
|
||||
|
||||
$request = (new ServerRequestFactory())->createServerRequest('GET', '/missing');
|
||||
$response = $app->handle($request);
|
||||
|
||||
self::assertSame(404, $response->getStatusCode());
|
||||
self::assertStringContainsString('configured 404', (string) $response->getBody());
|
||||
}
|
||||
|
||||
public function testConfigureRegistersHandlerThatRenders503Responses(): void
|
||||
{
|
||||
$app = AppFactory::create();
|
||||
$app->get('/db-check', function (): never {
|
||||
throw new DatabaseNotProvisionedException('Provisionnement requis');
|
||||
});
|
||||
|
||||
$this->twig
|
||||
->expects(self::once())
|
||||
->method('render')
|
||||
->willReturnCallback(function ($response, string $template, array $data) {
|
||||
self::assertSame('@Kernel/error.twig', $template);
|
||||
self::assertSame(503, $data['status']);
|
||||
self::assertSame('Provisionnement requis', $data['message']);
|
||||
|
||||
$response->getBody()->write('configured 503');
|
||||
|
||||
return $response;
|
||||
});
|
||||
|
||||
(new ErrorHandlerConfigurator())->configure($app, $this->container);
|
||||
|
||||
$request = (new ServerRequestFactory())->createServerRequest('GET', '/db-check');
|
||||
$response = $app->handle($request);
|
||||
|
||||
self::assertSame(503, $response->getStatusCode());
|
||||
self::assertStringContainsString('configured 503', (string) $response->getBody());
|
||||
}
|
||||
}
|
||||
63
tests/Kernel/ExtensionTest.php
Normal file
63
tests/Kernel/ExtensionTest.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Http\Infrastructure\Twig\AppExtension;
|
||||
use Netig\Netslim\Kernel\Http\Infrastructure\Twig\CsrfExtension;
|
||||
use Netig\Netslim\Kernel\Http\Infrastructure\Twig\SessionExtension;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Slim\Csrf\Guard;
|
||||
use Slim\Psr7\Factory\ResponseFactory;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
|
||||
final class ExtensionTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
public function testAppExtensionExposesAppUrl(): void
|
||||
{
|
||||
$extension = new AppExtension('https://example.test');
|
||||
|
||||
self::assertSame(['app_url' => 'https://example.test'], $extension->getGlobals());
|
||||
}
|
||||
|
||||
public function testSessionExtensionExposesSelectedSessionKeys(): void
|
||||
{
|
||||
$_SESSION = [
|
||||
'user_id' => 12,
|
||||
'username' => 'julien',
|
||||
'role' => 'admin',
|
||||
'flash' => ['notice' => 'x'],
|
||||
];
|
||||
|
||||
$extension = new SessionExtension();
|
||||
|
||||
self::assertSame([
|
||||
'session' => [
|
||||
'user_id' => 12,
|
||||
'username' => 'julien',
|
||||
'role' => 'admin',
|
||||
],
|
||||
], $extension->getGlobals());
|
||||
}
|
||||
|
||||
public function testCsrfExtensionExposesTokens(): void
|
||||
{
|
||||
$storage = [];
|
||||
$guard = new Guard(new ResponseFactory(), storage: $storage);
|
||||
$extension = new CsrfExtension($guard);
|
||||
$globals = $extension->getGlobals();
|
||||
|
||||
self::assertArrayHasKey('csrf', $globals);
|
||||
self::assertSame($guard->getTokenNameKey(), $globals['csrf']['keys']['name']);
|
||||
self::assertSame($guard->getTokenValueKey(), $globals['csrf']['keys']['value']);
|
||||
self::assertSame($guard->getTokenName(), $globals['csrf']['name']);
|
||||
self::assertSame($guard->getTokenValue(), $globals['csrf']['value']);
|
||||
}
|
||||
}
|
||||
29
tests/Kernel/FlashServiceConsumeTest.php
Normal file
29
tests/Kernel/FlashServiceConsumeTest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Http\Infrastructure\Flash\FlashService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
|
||||
final class FlashServiceConsumeTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
public function testGetReturnsNullWhenMissingAndConsumesWhenPresent(): void
|
||||
{
|
||||
$flash = new FlashService();
|
||||
|
||||
self::assertNull($flash->get('missing'));
|
||||
|
||||
$flash->set('notice', 'Bonjour');
|
||||
self::assertSame('Bonjour', $flash->get('notice'));
|
||||
self::assertNull($flash->get('notice'));
|
||||
}
|
||||
}
|
||||
27
tests/Kernel/FlashServiceCoverageTest.php
Normal file
27
tests/Kernel/FlashServiceCoverageTest.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Http\Infrastructure\Flash\FlashService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
final class FlashServiceCoverageTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
public function testGetCastsFalseToEmptyString(): void
|
||||
{
|
||||
$_SESSION['flash']['flag'] = false;
|
||||
|
||||
$flash = new FlashService();
|
||||
|
||||
self::assertSame('', $flash->get('flag'));
|
||||
self::assertArrayNotHasKey('flag', $_SESSION['flash']);
|
||||
}
|
||||
}
|
||||
45
tests/Kernel/FlashServiceTest.php
Normal file
45
tests/Kernel/FlashServiceTest.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Http\Infrastructure\Flash\FlashService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
|
||||
final class FlashServiceTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
public function testSetAndGetConsumesFlashMessage(): void
|
||||
{
|
||||
$flash = new FlashService();
|
||||
|
||||
$flash->set('notice', 'Bonjour');
|
||||
|
||||
self::assertSame('Bonjour', $flash->get('notice'));
|
||||
self::assertNull($flash->get('notice'));
|
||||
}
|
||||
|
||||
public function testGetCastsNonStringValueAndRemovesIt(): void
|
||||
{
|
||||
$_SESSION['flash']['count'] = 123;
|
||||
|
||||
$flash = new FlashService();
|
||||
|
||||
self::assertSame('123', $flash->get('count'));
|
||||
self::assertArrayNotHasKey('count', $_SESSION['flash']);
|
||||
}
|
||||
|
||||
public function testGetReturnsNullWhenMissing(): void
|
||||
{
|
||||
$flash = new FlashService();
|
||||
|
||||
self::assertNull($flash->get('missing'));
|
||||
}
|
||||
}
|
||||
27
tests/Kernel/HelperEdgeCasesTest.php
Normal file
27
tests/Kernel/HelperEdgeCasesTest.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Http\Infrastructure\Request\ClientIpResolver;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Slim\Psr7\Factory\ServerRequestFactory;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
|
||||
final class HelperEdgeCasesTest extends TestCase
|
||||
{
|
||||
public function testClientIpResolverFallsBackToRemoteAddr(): void
|
||||
{
|
||||
$resolver = new ClientIpResolver([]);
|
||||
|
||||
$request = new ServerRequestFactory()->createServerRequest(
|
||||
'GET',
|
||||
'/',
|
||||
['REMOTE_ADDR' => '127.0.0.1'],
|
||||
);
|
||||
|
||||
self::assertSame('127.0.0.1', $resolver->resolve($request));
|
||||
}
|
||||
}
|
||||
36
tests/Kernel/HtmlPurifierFactoryTest.php
Normal file
36
tests/Kernel/HtmlPurifierFactoryTest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Html\Infrastructure\HtmlPurifierFactory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
|
||||
final class HtmlPurifierFactoryTest extends TestCase
|
||||
{
|
||||
public function testCreateBuildsPurifierAndSanitizesDangerousHtml(): void
|
||||
{
|
||||
$cacheDir = sys_get_temp_dir() . '/htmlpurifier-test-' . bin2hex(random_bytes(4));
|
||||
|
||||
try {
|
||||
$purifier = HtmlPurifierFactory::create($cacheDir);
|
||||
$result = $purifier->purify('<p style="text-align:center">ok</p><a href="javascript:alert(1)">x</a><img src="/media/image.webp" data-media-id="42" alt=""> https://example.test');
|
||||
|
||||
self::assertDirectoryExists($cacheDir);
|
||||
self::assertStringContainsString('text-align:center', $result);
|
||||
self::assertStringNotContainsString('javascript:', $result);
|
||||
self::assertStringContainsString('https://example.test', $result);
|
||||
self::assertStringContainsString('data-media-id="42"', $result);
|
||||
} finally {
|
||||
if (is_dir($cacheDir)) {
|
||||
foreach (glob($cacheDir . '/*') ?: [] as $file) {
|
||||
@unlink($file);
|
||||
}
|
||||
@rmdir($cacheDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
252
tests/Kernel/HtmlSanitizerTest.php
Normal file
252
tests/Kernel/HtmlSanitizerTest.php
Normal file
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Html\Infrastructure\HtmlPurifierFactory;
|
||||
use Netig\Netslim\Kernel\Html\Infrastructure\HtmlSanitizer;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour HtmlSanitizer.
|
||||
*
|
||||
* Vérifie que HTMLPurifier supprime bien les contenus dangereux
|
||||
* (XSS, balises non autorisées, schémas URI interdits) et conserve
|
||||
* les balises légitimes produites par l'éditeur riche.
|
||||
*
|
||||
* Ces tests utilisent une vraie instance HTMLPurifier (pas de mock)
|
||||
* car c'est le comportement de purification lui-même qui est testé.
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
final class HtmlSanitizerTest extends TestCase
|
||||
{
|
||||
private HtmlSanitizer $sanitizer;
|
||||
|
||||
/**
|
||||
* Crée une instance réelle de HtmlSanitizer avant chaque test.
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
$purifier = HtmlPurifierFactory::create(sys_get_temp_dir() . '/htmlpurifier_tests');
|
||||
$this->sanitizer = new HtmlSanitizer($purifier);
|
||||
}
|
||||
|
||||
|
||||
// ── Balises autorisées ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Les balises de texte courantes doivent être conservées.
|
||||
*/
|
||||
public function testTextTagsPreserved(): void
|
||||
{
|
||||
$html = '<p>Un <strong>texte</strong> avec <em>emphase</em> et <u>soulignement</u>.</p>';
|
||||
$result = $this->sanitizer->sanitize($html);
|
||||
|
||||
$this->assertStringContainsString('<strong>texte</strong>', $result);
|
||||
$this->assertStringContainsString('<em>emphase</em>', $result);
|
||||
$this->assertStringContainsString('<u>soulignement</u>', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Les titres h1 à h6 doivent être conservés.
|
||||
*/
|
||||
public function testHeadingsPreserved(): void
|
||||
{
|
||||
$html = '<h1>Titre 1</h1><h2>Titre 2</h2><h3>Titre 3</h3>';
|
||||
$result = $this->sanitizer->sanitize($html);
|
||||
|
||||
$this->assertStringContainsString('<h1>', $result);
|
||||
$this->assertStringContainsString('<h2>', $result);
|
||||
$this->assertStringContainsString('<h3>', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Les listes ordonnées et non ordonnées doivent être conservées.
|
||||
*/
|
||||
public function testListsPreserved(): void
|
||||
{
|
||||
$html = '<ul><li>Item 1</li><li>Item 2</li></ul><ol><li>A</li></ol>';
|
||||
$result = $this->sanitizer->sanitize($html);
|
||||
|
||||
$this->assertStringContainsString('<ul>', $result);
|
||||
$this->assertStringContainsString('<ol>', $result);
|
||||
$this->assertStringContainsString('<li>', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Les liens avec href http/https doivent être conservés.
|
||||
*/
|
||||
public function testHttpLinksPreserved(): void
|
||||
{
|
||||
$html = '<a href="https://example.com">Lien</a>';
|
||||
$result = $this->sanitizer->sanitize($html);
|
||||
|
||||
$this->assertStringContainsString('href="https://example.com"', $result);
|
||||
$this->assertStringContainsString('Lien', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Les images avec src doivent être conservées.
|
||||
*/
|
||||
public function testImagesPreserved(): void
|
||||
{
|
||||
$html = '<img src="https://example.com/image.jpg" alt="Description">';
|
||||
$result = $this->sanitizer->sanitize($html);
|
||||
|
||||
$this->assertStringContainsString('<img', $result);
|
||||
$this->assertStringContainsString('src="https://example.com/image.jpg"', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* L'identifiant métier d'un média doit être conservé sur l'image.
|
||||
*/
|
||||
public function testImageDataMediaIdPreserved(): void
|
||||
{
|
||||
$html = '<img src="https://example.com/image.jpg" data-media-id="42" alt="Description">';
|
||||
$result = $this->sanitizer->sanitize($html);
|
||||
|
||||
$this->assertStringContainsString('data-media-id="42"', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Les blocs de code doivent être conservés.
|
||||
*/
|
||||
public function testPreTagPreserved(): void
|
||||
{
|
||||
$html = '<pre>code ici</pre>';
|
||||
$result = $this->sanitizer->sanitize($html);
|
||||
|
||||
$this->assertStringContainsString('<pre>', $result);
|
||||
}
|
||||
|
||||
|
||||
// ── Balises et attributs dangereux — suppression XSS ───────────
|
||||
|
||||
/**
|
||||
* Les balises <script> doivent être supprimées.
|
||||
*/
|
||||
public function testScriptTagRemoved(): void
|
||||
{
|
||||
$html = '<p>Texte</p><script>alert("xss")</script>';
|
||||
$result = $this->sanitizer->sanitize($html);
|
||||
|
||||
$this->assertStringNotContainsString('<script>', $result);
|
||||
$this->assertStringNotContainsString('alert(', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Les attributs onclick et autres handlers JavaScript doivent être supprimés.
|
||||
*/
|
||||
public function testJavascriptAttributesRemoved(): void
|
||||
{
|
||||
$html = '<p onclick="alert(1)" onmouseover="evil()">Texte</p>';
|
||||
$result = $this->sanitizer->sanitize($html);
|
||||
|
||||
$this->assertStringNotContainsString('onclick', $result);
|
||||
$this->assertStringNotContainsString('onmouseover', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Les liens javascript: doivent être supprimés.
|
||||
*/
|
||||
public function testJavascriptLinkRemoved(): void
|
||||
{
|
||||
$html = '<a href="javascript:alert(1)">Cliquez</a>';
|
||||
$result = $this->sanitizer->sanitize($html);
|
||||
|
||||
$this->assertStringNotContainsString('javascript:', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Les liens data: doivent être supprimés.
|
||||
*/
|
||||
public function testDataLinkRemoved(): void
|
||||
{
|
||||
$html = '<a href="data:text/html,<script>alert(1)</script>">XSS</a>';
|
||||
$result = $this->sanitizer->sanitize($html);
|
||||
|
||||
$this->assertStringNotContainsString('data:', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* La balise <iframe> doit être supprimée.
|
||||
*/
|
||||
public function testIframeTagRemoved(): void
|
||||
{
|
||||
$html = '<iframe src="https://evil.com"></iframe>';
|
||||
$result = $this->sanitizer->sanitize($html);
|
||||
|
||||
$this->assertStringNotContainsString('<iframe', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* La balise <object> doit être supprimée.
|
||||
*/
|
||||
public function testObjectTagRemoved(): void
|
||||
{
|
||||
$html = '<object data="malware.swf"></object>';
|
||||
$result = $this->sanitizer->sanitize($html);
|
||||
|
||||
$this->assertStringNotContainsString('<object', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* La balise <form> doit être supprimée.
|
||||
*/
|
||||
public function testFormTagRemoved(): void
|
||||
{
|
||||
$html = '<form action="/steal"><input type="password"></form>';
|
||||
$result = $this->sanitizer->sanitize($html);
|
||||
|
||||
$this->assertStringNotContainsString('<form', $result);
|
||||
$this->assertStringNotContainsString('<input', $result);
|
||||
}
|
||||
|
||||
|
||||
// ── Cas limites ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Une chaîne vide doit retourner une chaîne vide (ou quasi-vide).
|
||||
*/
|
||||
public function testEmptyStringReturnsEmptyOrBlank(): void
|
||||
{
|
||||
$result = $this->sanitizer->sanitize('');
|
||||
|
||||
$this->assertSame('', trim($result));
|
||||
}
|
||||
|
||||
/**
|
||||
* Du texte brut sans balises doit être conservé.
|
||||
*/
|
||||
public function testPlainTextWithoutTags(): void
|
||||
{
|
||||
$html = 'Bonjour le monde';
|
||||
$result = $this->sanitizer->sanitize($html);
|
||||
|
||||
$this->assertStringContainsString('Bonjour le monde', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Les attributs CSS text-align doivent être conservés.
|
||||
*/
|
||||
public function testStyleTextAlignAttributePreserved(): void
|
||||
{
|
||||
$html = '<p style="text-align: center;">Centré</p>';
|
||||
$result = $this->sanitizer->sanitize($html);
|
||||
|
||||
$this->assertStringContainsString('text-align', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Les propriétés CSS autres que text-align doivent être supprimées.
|
||||
*/
|
||||
public function testOtherCssPropertiesRemoved(): void
|
||||
{
|
||||
$html = '<p style="color: red; background: url(evil.php);">Texte</p>';
|
||||
$result = $this->sanitizer->sanitize($html);
|
||||
|
||||
$this->assertStringNotContainsString('color', $result);
|
||||
$this->assertStringNotContainsString('background', $result);
|
||||
}
|
||||
}
|
||||
155
tests/Kernel/InfrastructureBootstrapperTest.php
Normal file
155
tests/Kernel/InfrastructureBootstrapperTest.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Runtime\Startup\InfrastructureBootstrapper;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
final class InfrastructureBootstrapperTest extends TestCase
|
||||
{
|
||||
private string $workspace;
|
||||
|
||||
/** @var array<string, string|null> */
|
||||
private array $envBackup = [];
|
||||
|
||||
/** @var array<string, string|false> */
|
||||
private array $putenvBackup = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->workspace = sys_get_temp_dir() . '/netslim-bootstrapper-' . bin2hex(random_bytes(8));
|
||||
mkdir($this->workspace, 0777, true);
|
||||
|
||||
foreach (['APP_ENV', 'APP_URL', 'TIMEZONE'] as $key) {
|
||||
$this->envBackup[$key] = $_ENV[$key] ?? null;
|
||||
$this->putenvBackup[$key] = getenv($key);
|
||||
unset($_ENV[$key], $_SERVER[$key]);
|
||||
putenv($key);
|
||||
}
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
foreach ($this->envBackup as $key => $value) {
|
||||
if ($value === null) {
|
||||
unset($_ENV[$key], $_SERVER[$key]);
|
||||
} else {
|
||||
$_ENV[$key] = $value;
|
||||
$_SERVER[$key] = $value;
|
||||
}
|
||||
|
||||
$previous = $this->putenvBackup[$key] ?? false;
|
||||
if ($previous === false) {
|
||||
putenv($key);
|
||||
} else {
|
||||
putenv($key . '=' . $previous);
|
||||
}
|
||||
}
|
||||
|
||||
$this->removeDirectory($this->workspace);
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testBootstrapCreatesRuntimeDirectoriesLoadsEnvironmentAndMemoizesContainer(): void
|
||||
{
|
||||
$this->writeEnvFile([
|
||||
'APP_ENV=development',
|
||||
'APP_URL=http://localhost',
|
||||
'ADMIN_USERNAME=admin',
|
||||
'ADMIN_EMAIL=admin@example.test',
|
||||
'ADMIN_PASSWORD=secret123456',
|
||||
'TIMEZONE=Europe/Paris',
|
||||
]);
|
||||
$definitionsPath = $this->writeDefinitionsFile();
|
||||
$bootstrapper = new InfrastructureBootstrapper($this->workspace, $definitionsPath);
|
||||
|
||||
$container = $bootstrapper->bootstrap();
|
||||
$sameContainer = $bootstrapper->bootstrap();
|
||||
|
||||
self::assertInstanceOf(ContainerInterface::class, $container);
|
||||
self::assertSame($container, $sameContainer);
|
||||
self::assertSame('from-test-definitions', $container->get('bootstrap.flag'));
|
||||
self::assertSame('Europe/Paris', date_default_timezone_get());
|
||||
|
||||
foreach ([
|
||||
'var/cache/twig',
|
||||
'var/cache/htmlpurifier',
|
||||
'var/cache/di',
|
||||
'var/logs',
|
||||
'database',
|
||||
'public/media',
|
||||
] as $directory) {
|
||||
self::assertDirectoryExists($this->workspace . '/' . $directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function testBootstrapFailsWhenEnvironmentFileIsMissing(): void
|
||||
{
|
||||
$bootstrapper = new InfrastructureBootstrapper($this->workspace, $this->writeDefinitionsFile());
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('Fichier .env introuvable');
|
||||
|
||||
$bootstrapper->bootstrap();
|
||||
}
|
||||
|
||||
public function testBootstrapNoLongerRequiresIdentityProvisioningVariables(): void
|
||||
{
|
||||
$this->writeEnvFile([
|
||||
'APP_ENV=production',
|
||||
'APP_URL=https://example.test',
|
||||
]);
|
||||
$bootstrapper = new InfrastructureBootstrapper($this->workspace, $this->writeDefinitionsFile());
|
||||
|
||||
$container = $bootstrapper->bootstrap();
|
||||
|
||||
self::assertInstanceOf(ContainerInterface::class, $container);
|
||||
}
|
||||
|
||||
private function writeEnvFile(array $lines): void
|
||||
{
|
||||
file_put_contents($this->workspace . '/.env', implode(PHP_EOL, $lines) . PHP_EOL);
|
||||
}
|
||||
|
||||
private function writeDefinitionsFile(): string
|
||||
{
|
||||
$definitionsPath = $this->workspace . '/definitions.php';
|
||||
file_put_contents($definitionsPath, <<<'PHP'
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'bootstrap.flag' => 'from-test-definitions',
|
||||
];
|
||||
PHP);
|
||||
|
||||
return $definitionsPath;
|
||||
}
|
||||
|
||||
private function removeDirectory(string $directory): void
|
||||
{
|
||||
if (!is_dir($directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST,
|
||||
);
|
||||
|
||||
foreach ($iterator as $fileInfo) {
|
||||
if ($fileInfo->isDir()) {
|
||||
rmdir($fileInfo->getPathname());
|
||||
continue;
|
||||
}
|
||||
|
||||
unlink($fileInfo->getPathname());
|
||||
}
|
||||
|
||||
rmdir($directory);
|
||||
}
|
||||
}
|
||||
72
tests/Kernel/MailServiceTest.php
Normal file
72
tests/Kernel/MailServiceTest.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Mail\Infrastructure\MailService;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionMethod;
|
||||
use Slim\Views\Twig;
|
||||
use Twig\Loader\ArrayLoader;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
|
||||
final class MailServiceTest extends TestCase
|
||||
{
|
||||
public function testCreateMailerUsesSslConfiguration(): void
|
||||
{
|
||||
$service = $this->makeService('ssl', 465);
|
||||
$mailer = $this->invokeCreateMailer($service);
|
||||
|
||||
self::assertSame('smtp', $mailer->Mailer);
|
||||
self::assertSame('smtp.example.test', $mailer->Host);
|
||||
self::assertTrue($mailer->SMTPAuth);
|
||||
self::assertSame('mailer-user', $mailer->Username);
|
||||
self::assertSame('mailer-pass', $mailer->Password);
|
||||
self::assertSame(PHPMailer::ENCRYPTION_SMTPS, $mailer->SMTPSecure);
|
||||
self::assertSame(465, $mailer->Port);
|
||||
self::assertSame(PHPMailer::CHARSET_UTF8, $mailer->CharSet);
|
||||
self::assertSame('no-reply@example.test', $mailer->From);
|
||||
self::assertSame('NETslim', $mailer->FromName);
|
||||
}
|
||||
|
||||
public function testCreateMailerUsesStartTlsWhenEncryptionIsNotSsl(): void
|
||||
{
|
||||
$service = $this->makeService('tls', 587);
|
||||
$mailer = $this->invokeCreateMailer($service);
|
||||
|
||||
self::assertSame(PHPMailer::ENCRYPTION_STARTTLS, $mailer->SMTPSecure);
|
||||
self::assertSame(587, $mailer->Port);
|
||||
}
|
||||
|
||||
private function makeService(string $encryption, int $port): MailService
|
||||
{
|
||||
$twig = new Twig(new ArrayLoader([
|
||||
'@Test/emails/test.twig' => '<p>Bonjour {{ name }}</p>',
|
||||
]));
|
||||
|
||||
return new MailService(
|
||||
$twig,
|
||||
'smtp.example.test',
|
||||
$port,
|
||||
'mailer-user',
|
||||
'mailer-pass',
|
||||
$encryption,
|
||||
'no-reply@example.test',
|
||||
'NETslim',
|
||||
);
|
||||
}
|
||||
|
||||
private function invokeCreateMailer(MailService $service): PHPMailer
|
||||
{
|
||||
$method = new ReflectionMethod($service, 'createMailer');
|
||||
$method->setAccessible(true);
|
||||
|
||||
/** @var PHPMailer $mailer */
|
||||
$mailer = $method->invoke($service);
|
||||
|
||||
return $mailer;
|
||||
}
|
||||
}
|
||||
82
tests/Kernel/MigratorTest.php
Normal file
82
tests/Kernel/MigratorTest.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Persistence\Infrastructure\Migrator;
|
||||
use PDO;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour Migrator.
|
||||
*
|
||||
* Vérifie que run() crée la table de suivi, exécute uniquement les migrations
|
||||
* en attente puis agrège correctement les migrations du socle (`Identity`,
|
||||
* `Settings`, `AuditLog`, `Notifications`, `Taxonomy`, `Media`).
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
final class MigratorTest extends TestCase
|
||||
{
|
||||
private PDO $db;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->db = new PDO('sqlite::memory:', options: [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testRunCreatesMigrationsTable(): void
|
||||
{
|
||||
Migrator::run($this->db);
|
||||
|
||||
$stmt = $this->db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='migrations'");
|
||||
self::assertNotFalse($stmt->fetchColumn(), 'La table migrations doit exister après run().');
|
||||
}
|
||||
|
||||
public function testRunIsIdempotent(): void
|
||||
{
|
||||
Migrator::run($this->db);
|
||||
Migrator::run($this->db);
|
||||
|
||||
$this->addToAssertionCount(1);
|
||||
}
|
||||
|
||||
public function testAlreadyAppliedMigrationIsSkipped(): void
|
||||
{
|
||||
Migrator::run($this->db);
|
||||
$before = $this->countMigrations();
|
||||
|
||||
$stmt = $this->db->prepare('INSERT INTO migrations (version, run_at) VALUES (:v, :r)');
|
||||
$stmt->execute([':v' => '999_future_migration', ':r' => date('Y-m-d H:i:s')]);
|
||||
|
||||
Migrator::run($this->db);
|
||||
$after = $this->countMigrations();
|
||||
|
||||
self::assertSame($before + 1, $after);
|
||||
}
|
||||
|
||||
public function testRunRecordsModuleMigrationsInTable(): void
|
||||
{
|
||||
Migrator::run($this->db);
|
||||
|
||||
self::assertGreaterThan(0, $this->countMigrations(), 'Au moins une migration du socle doit être enregistrée.');
|
||||
}
|
||||
|
||||
public function testRunCreatesCoreModuleTables(): void
|
||||
{
|
||||
Migrator::run($this->db);
|
||||
|
||||
foreach (['users', 'password_resets', 'rate_limits', 'settings', 'audit_log', 'notification_dispatches', 'categories', 'media'] as $table) {
|
||||
$stmt = $this->db->query(sprintf("SELECT name FROM sqlite_master WHERE type='table' AND name='%s'", $table));
|
||||
self::assertNotFalse($stmt->fetchColumn(), sprintf('La table %s doit exister après run().', $table));
|
||||
}
|
||||
}
|
||||
|
||||
private function countMigrations(): int
|
||||
{
|
||||
return (int) $this->db->query('SELECT COUNT(*) FROM migrations')->fetchColumn();
|
||||
}
|
||||
}
|
||||
87
tests/Kernel/ModuleRegistryTest.php
Normal file
87
tests/Kernel/ModuleRegistryTest.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Runtime\Module\ModuleInterface;
|
||||
use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry;
|
||||
use Netig\Netslim\Kernel\Runtime\RuntimePaths;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ModuleRegistryTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
RuntimePaths::resetApplicationRoot();
|
||||
RuntimePaths::resetProjectRoot();
|
||||
ModuleRegistry::reset();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
RuntimePaths::resetApplicationRoot();
|
||||
RuntimePaths::resetProjectRoot();
|
||||
ModuleRegistry::reset();
|
||||
}
|
||||
|
||||
public function testModulesAreDeclaredInExpectedOrderForFixtureApplication(): void
|
||||
{
|
||||
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application');
|
||||
|
||||
$moduleClasses = array_map(
|
||||
static fn (ModuleInterface $module): string => $module::class,
|
||||
ModuleRegistry::modules(),
|
||||
);
|
||||
|
||||
self::assertSame([
|
||||
'Netig\Netslim\Kernel\Runtime\KernelModule',
|
||||
'Netig\Netslim\Identity\IdentityModule',
|
||||
'Netig\Netslim\Settings\SettingsModule',
|
||||
'Netig\Netslim\AuditLog\AuditLogModule',
|
||||
'Netig\Netslim\Notifications\NotificationsModule',
|
||||
'Netig\Netslim\Taxonomy\TaxonomyModule',
|
||||
'Netig\Netslim\Media\MediaModule',
|
||||
], $moduleClasses);
|
||||
}
|
||||
|
||||
public function testModuleClassNamesExposeTheActiveApplicationComposition(): void
|
||||
{
|
||||
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application');
|
||||
|
||||
self::assertSame([
|
||||
'Netig\Netslim\Kernel\Runtime\KernelModule',
|
||||
'Netig\Netslim\Identity\IdentityModule',
|
||||
'Netig\Netslim\Settings\SettingsModule',
|
||||
'Netig\Netslim\AuditLog\AuditLogModule',
|
||||
'Netig\Netslim\Notifications\NotificationsModule',
|
||||
'Netig\Netslim\Taxonomy\TaxonomyModule',
|
||||
'Netig\Netslim\Media\MediaModule',
|
||||
], ModuleRegistry::moduleClassNames());
|
||||
}
|
||||
|
||||
public function testApplicationManifestIsResolvedFromTheActiveApplicationRoot(): void
|
||||
{
|
||||
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application');
|
||||
|
||||
self::assertFileExists(RuntimePaths::getApplicationConfigPath('modules.php'));
|
||||
}
|
||||
|
||||
public function testEveryModuleExposesResolvableMetadata(): void
|
||||
{
|
||||
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application');
|
||||
|
||||
foreach (ModuleRegistry::modules() as $module) {
|
||||
self::assertNotSame([], $module->definitions(), $module::class . ' should expose DI definitions.');
|
||||
|
||||
foreach ($module->templateNamespaces() as $namespace => $templatePath) {
|
||||
self::assertNotSame('', $namespace, $module::class . ' should declare a non-empty Twig namespace.');
|
||||
self::assertDirectoryExists($templatePath, $module::class . ' should point Twig to an existing template directory.');
|
||||
}
|
||||
|
||||
foreach ($module->twigExtensions() as $twigExtensionClass) {
|
||||
self::assertTrue(class_exists($twigExtensionClass), $module::class . ' should reference an existing Twig extension class.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
tests/Kernel/ModuleSchemaTest.php
Normal file
50
tests/Kernel/ModuleSchemaTest.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry;
|
||||
use Netig\Netslim\Kernel\Runtime\Module\ProvidesSchemaInterface;
|
||||
use Netig\Netslim\Kernel\Runtime\RuntimePaths;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ModuleSchemaTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application');
|
||||
ModuleRegistry::reset();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
RuntimePaths::resetApplicationRoot();
|
||||
RuntimePaths::resetProjectRoot();
|
||||
ModuleRegistry::reset();
|
||||
}
|
||||
|
||||
public function testFeatureModulesDeclareOwnedMigrationDirectoriesAndTables(): void
|
||||
{
|
||||
$schemaModules = array_values(array_filter(
|
||||
ModuleRegistry::modules(),
|
||||
static fn (object $module): bool => $module instanceof ProvidesSchemaInterface,
|
||||
));
|
||||
|
||||
self::assertCount(6, $schemaModules);
|
||||
|
||||
foreach ($schemaModules as $module) {
|
||||
foreach ($module->migrationDirectories() as $directory) {
|
||||
self::assertDirectoryExists($directory, $module::class . ' should expose an existing migration directory.');
|
||||
self::assertNotSame([], glob(rtrim($directory, '/') . '/*.php') ?: [], $module::class . ' should expose at least one migration file.');
|
||||
}
|
||||
|
||||
self::assertNotSame([], $module->requiredTables(), $module::class . ' should expose owned tables for readiness checks.');
|
||||
}
|
||||
}
|
||||
|
||||
public function testLegacyGlobalMigrationsDirectoryIsNoLongerUsed(): void
|
||||
{
|
||||
self::assertSame([], glob(dirname(__DIR__, 2) . '/database/migrations/*.php') ?: []);
|
||||
}
|
||||
}
|
||||
20
tests/Kernel/NotFoundExceptionTest.php
Normal file
20
tests/Kernel/NotFoundExceptionTest.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Support\Exception\NotFoundException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
|
||||
final class NotFoundExceptionTest extends TestCase
|
||||
{
|
||||
public function testMessageContainsEntityAndIdentifier(): void
|
||||
{
|
||||
$exception = new NotFoundException('Ressource', 'mon-slug');
|
||||
|
||||
self::assertSame('Ressource introuvable : mon-slug', $exception->getMessage());
|
||||
}
|
||||
}
|
||||
79
tests/Kernel/ProvisionerTest.php
Normal file
79
tests/Kernel/ProvisionerTest.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Persistence\Infrastructure\Provisioner;
|
||||
use PDO;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
|
||||
final class ProvisionerTest extends TestCase
|
||||
{
|
||||
private PDO $db;
|
||||
|
||||
private string $lockPath;
|
||||
|
||||
private array $envBackup = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->db = new PDO('sqlite::memory:', options: [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
$this->db->sqliteCreateFunction('strip_tags', 'strip_tags', 1);
|
||||
|
||||
$this->lockPath = dirname(__DIR__, 2) . '/database/.provision.lock';
|
||||
@unlink($this->lockPath);
|
||||
|
||||
$this->envBackup = [
|
||||
'ADMIN_USERNAME' => $_ENV['ADMIN_USERNAME'] ?? null,
|
||||
'ADMIN_EMAIL' => $_ENV['ADMIN_EMAIL'] ?? null,
|
||||
'ADMIN_PASSWORD' => $_ENV['ADMIN_PASSWORD'] ?? null,
|
||||
];
|
||||
|
||||
$_ENV['ADMIN_USERNAME'] = 'Admin';
|
||||
$_ENV['ADMIN_EMAIL'] = 'Admin@example.com';
|
||||
$_ENV['ADMIN_PASSWORD'] = 'secret123456';
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
@unlink($this->lockPath);
|
||||
|
||||
foreach ($this->envBackup as $key => $value) {
|
||||
if ($value === null) {
|
||||
unset($_ENV[$key]);
|
||||
} else {
|
||||
$_ENV[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testRunCreatesProvisionLockAndSeedsAdminUser(): void
|
||||
{
|
||||
Provisioner::run($this->db);
|
||||
|
||||
self::assertFileExists($this->lockPath);
|
||||
|
||||
$row = $this->db->query('SELECT username, email, role FROM users')->fetch();
|
||||
|
||||
self::assertIsArray($row);
|
||||
self::assertSame('admin', $row['username']);
|
||||
self::assertSame('admin@example.com', $row['email']);
|
||||
self::assertSame('admin', $row['role']);
|
||||
}
|
||||
|
||||
public function testRunIsIdempotent(): void
|
||||
{
|
||||
Provisioner::run($this->db);
|
||||
Provisioner::run($this->db);
|
||||
|
||||
$count = (int) $this->db->query('SELECT COUNT(*) FROM users WHERE username = "admin"')->fetchColumn();
|
||||
|
||||
self::assertSame(1, $count);
|
||||
}
|
||||
}
|
||||
55
tests/Kernel/RequestContextTest.php
Normal file
55
tests/Kernel/RequestContextTest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Http\Infrastructure\Request\RequestContext;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
final class RequestContextTest extends TestCase
|
||||
{
|
||||
public function testIsHttpsReturnsTrueWhenNativeHttpsFlagIsEnabled(): void
|
||||
{
|
||||
self::assertTrue(RequestContext::isHttps([
|
||||
'HTTPS' => 'on',
|
||||
]));
|
||||
}
|
||||
|
||||
public function testIsHttpsReturnsTrueWhenTrustedProxyForwardsHttps(): void
|
||||
{
|
||||
self::assertTrue(RequestContext::isHttps([
|
||||
'REMOTE_ADDR' => '127.0.0.1',
|
||||
'HTTP_X_FORWARDED_PROTO' => 'https, http',
|
||||
], ['127.0.0.1']));
|
||||
}
|
||||
|
||||
public function testIsHttpsIgnoresForwardedProtoWhenProxyIsNotTrusted(): void
|
||||
{
|
||||
self::assertFalse(RequestContext::isHttps([
|
||||
'REMOTE_ADDR' => '10.0.0.5',
|
||||
'HTTP_X_FORWARDED_PROTO' => 'https',
|
||||
], ['127.0.0.1']));
|
||||
}
|
||||
|
||||
public function testTrustedProxiesFromEnvironmentTrimsValues(): void
|
||||
{
|
||||
self::assertSame(['127.0.0.1', '::1'], RequestContext::trustedProxiesFromEnvironment([
|
||||
'TRUSTED_PROXIES' => ' 127.0.0.1 , ::1 ',
|
||||
]));
|
||||
}
|
||||
|
||||
public function testTrustedProxiesFromEnvironmentFallsBackToProcessEnvWhenDotenvValueIsBlank(): void
|
||||
{
|
||||
putenv('TRUSTED_PROXIES=*');
|
||||
|
||||
try {
|
||||
self::assertSame(['*'], RequestContext::trustedProxiesFromEnvironment([
|
||||
'TRUSTED_PROXIES' => '',
|
||||
]));
|
||||
} finally {
|
||||
putenv('TRUSTED_PROXIES');
|
||||
}
|
||||
}
|
||||
}
|
||||
73
tests/Kernel/RoutesTest.php
Normal file
73
tests/Kernel/RoutesTest.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry;
|
||||
use Netig\Netslim\Kernel\Runtime\Routing\Routes;
|
||||
use Netig\Netslim\Kernel\Runtime\RuntimePaths;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Slim\Factory\AppFactory;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
final class RoutesTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application');
|
||||
ModuleRegistry::reset();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
RuntimePaths::resetApplicationRoot();
|
||||
RuntimePaths::resetProjectRoot();
|
||||
ModuleRegistry::reset();
|
||||
}
|
||||
|
||||
public function testRegisterDeclaresOnlyCoreModuleRoutesForFixtureApplication(): void
|
||||
{
|
||||
$app = AppFactory::create();
|
||||
Routes::register($app);
|
||||
|
||||
$actual = [];
|
||||
|
||||
foreach ($app->getRouteCollector()->getRoutes() as $route) {
|
||||
$pattern = $route->getPattern();
|
||||
$methods = array_values(array_diff($route->getMethods(), ['HEAD', 'OPTIONS']));
|
||||
|
||||
$actual[$pattern] ??= [];
|
||||
$actual[$pattern] = array_values(array_unique(array_merge($actual[$pattern], $methods)));
|
||||
sort($actual[$pattern]);
|
||||
}
|
||||
|
||||
ksort($actual);
|
||||
|
||||
$expected = [
|
||||
'/account/password' => ['GET', 'POST'],
|
||||
'/admin/categories' => ['GET'],
|
||||
'/admin/categories/create' => ['POST'],
|
||||
'/admin/categories/delete/{id}' => ['POST'],
|
||||
'/admin/media' => ['GET'],
|
||||
'/admin/media/delete/{id}' => ['POST'],
|
||||
'/admin/media/picker' => ['GET'],
|
||||
'/admin/media/upload' => ['POST'],
|
||||
'/admin/users' => ['GET'],
|
||||
'/admin/users/create' => ['GET', 'POST'],
|
||||
'/admin/users/delete/{id}' => ['POST'],
|
||||
'/admin/users/role/{id}' => ['POST'],
|
||||
'/auth/login' => ['GET', 'POST'],
|
||||
'/auth/logout' => ['POST'],
|
||||
'/password/forgot' => ['GET', 'POST'],
|
||||
'/password/reset' => ['GET', 'POST'],
|
||||
];
|
||||
|
||||
foreach ($expected as $pattern => $methods) {
|
||||
sort($methods);
|
||||
}
|
||||
ksort($expected);
|
||||
|
||||
self::assertSame($expected, $actual);
|
||||
}
|
||||
}
|
||||
87
tests/Kernel/RuntimePathsTest.php
Normal file
87
tests/Kernel/RuntimePathsTest.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Runtime\RuntimePaths;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
final class RuntimePathsTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
RuntimePaths::resetApplicationRoot();
|
||||
RuntimePaths::resetProjectRoot();
|
||||
}
|
||||
|
||||
public function testGetProjectRootReturnsRepositoryRoot(): void
|
||||
{
|
||||
self::assertSame(dirname(__DIR__, 2), RuntimePaths::getProjectRoot());
|
||||
}
|
||||
|
||||
public function testGetConfigPathReturnsRootConfigDirectoryAndFilePath(): void
|
||||
{
|
||||
self::assertSame(dirname(__DIR__, 2) . '/config', RuntimePaths::getConfigPath());
|
||||
self::assertSame(dirname(__DIR__, 2) . '/config/modules.php', RuntimePaths::getConfigPath('modules.php'));
|
||||
}
|
||||
|
||||
public function testApplicationRootDefaultsToProjectRoot(): void
|
||||
{
|
||||
self::assertSame(dirname(__DIR__, 2), RuntimePaths::getApplicationRoot());
|
||||
self::assertSame(dirname(__DIR__, 2) . '/config', RuntimePaths::getApplicationConfigPath());
|
||||
}
|
||||
|
||||
public function testApplicationRootCanPointToFixtureApplication(): void
|
||||
{
|
||||
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application');
|
||||
|
||||
self::assertSame(dirname(__DIR__, 2) . '/tests/Fixtures/Application', RuntimePaths::getApplicationRoot());
|
||||
self::assertSame(dirname(__DIR__, 2) . '/tests/Fixtures/Application/config/modules.php', RuntimePaths::getApplicationConfigPath('modules.php'));
|
||||
self::assertSame(dirname(__DIR__, 2) . '/tests/Fixtures/Application/templates/Kernel', RuntimePaths::getApplicationPath('templates/Kernel'));
|
||||
}
|
||||
|
||||
public function testGetTwigCacheReturnsFalseInDev(): void
|
||||
{
|
||||
self::assertFalse(RuntimePaths::getTwigCache(true));
|
||||
}
|
||||
|
||||
public function testGetTwigCacheReturnsCachePathOutsideDev(): void
|
||||
{
|
||||
$cachePath = RuntimePaths::getTwigCache(false);
|
||||
|
||||
self::assertIsString($cachePath);
|
||||
self::assertStringEndsWith('/var/cache/twig', $cachePath);
|
||||
}
|
||||
|
||||
public function testGetDatabasePathCreatesDatabaseFileWhenMissing(): void
|
||||
{
|
||||
$dbFile = dirname(__DIR__, 2) . '/database/app.sqlite';
|
||||
$dbDir = dirname($dbFile);
|
||||
$backup = $dbFile . '.bak-test';
|
||||
|
||||
if (file_exists($backup)) {
|
||||
@unlink($backup);
|
||||
}
|
||||
|
||||
if (file_exists($dbFile)) {
|
||||
rename($dbFile, $backup);
|
||||
}
|
||||
|
||||
@unlink($dbFile);
|
||||
|
||||
try {
|
||||
$path = RuntimePaths::getDatabasePath();
|
||||
|
||||
self::assertSame($dbFile, $path);
|
||||
self::assertDirectoryExists($dbDir);
|
||||
self::assertFileExists($dbFile);
|
||||
} finally {
|
||||
@unlink($dbFile);
|
||||
if (file_exists($backup)) {
|
||||
rename($backup, $dbFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
tests/Kernel/SessionManagerCoverageTest.php
Normal file
33
tests/Kernel/SessionManagerCoverageTest.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Http\Infrastructure\Session\SessionManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
final class SessionManagerCoverageTest extends TestCase
|
||||
{
|
||||
private SessionManager $manager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$_SESSION = [];
|
||||
$this->manager = new SessionManager();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
public function testGetUserIdCastsNumericStringToInteger(): void
|
||||
{
|
||||
$_SESSION['user_id'] = '42';
|
||||
|
||||
self::assertSame(42, $this->manager->getUserId());
|
||||
self::assertTrue($this->manager->isAuthenticated());
|
||||
}
|
||||
}
|
||||
43
tests/Kernel/SessionManagerEdgeCasesTest.php
Normal file
43
tests/Kernel/SessionManagerEdgeCasesTest.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Http\Infrastructure\Session\SessionManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
|
||||
final class SessionManagerEdgeCasesTest extends TestCase
|
||||
{
|
||||
private SessionManager $manager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$_SESSION = [];
|
||||
$this->manager = new SessionManager();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
public function testGetUserIdReturnsNullForEmptyString(): void
|
||||
{
|
||||
$_SESSION['user_id'] = '';
|
||||
|
||||
self::assertNull($this->manager->getUserId());
|
||||
self::assertFalse($this->manager->isAuthenticated());
|
||||
}
|
||||
|
||||
public function testSetUserUsesDefaultRoleUser(): void
|
||||
{
|
||||
$this->manager->setUser(12, 'julien');
|
||||
|
||||
self::assertSame('user', $_SESSION['role']);
|
||||
self::assertFalse($this->manager->isAdmin());
|
||||
self::assertFalse($this->manager->isEditor());
|
||||
}
|
||||
}
|
||||
168
tests/Kernel/SessionManagerTest.php
Normal file
168
tests/Kernel/SessionManagerTest.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Http\Infrastructure\Session\SessionManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour SessionManager.
|
||||
*
|
||||
* Vérifie la lecture et l'écriture des données d'authentification
|
||||
* dans $_SESSION, ainsi que la destruction de session.
|
||||
*
|
||||
* Note : session_start() n'est pas appelé dans ces tests — SessionManager
|
||||
* manipule directement $_SESSION, ce qui fonctionne en CLI sans session active.
|
||||
* session_regenerate_id() et session_destroy() sont gardés par un test
|
||||
* session_status() === PHP_SESSION_ACTIVE dans SessionManager, ce qui les rend
|
||||
* sans effet en contexte CLI et évite toute notice PHP.
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
final class SessionManagerTest extends TestCase
|
||||
{
|
||||
private SessionManager $manager;
|
||||
|
||||
/**
|
||||
* Réinitialise $_SESSION avant chaque test pour garantir l'isolation.
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
$_SESSION = [];
|
||||
$this->manager = new SessionManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Réinitialise $_SESSION après chaque test.
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
|
||||
// ── isAuthenticated ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Sans session active, isAuthenticated() doit retourner false.
|
||||
*/
|
||||
public function testIsAuthenticatedWithoutSession(): void
|
||||
{
|
||||
$this->assertFalse($this->manager->isAuthenticated());
|
||||
}
|
||||
|
||||
/**
|
||||
* Après setUser(), isAuthenticated() doit retourner true.
|
||||
*/
|
||||
public function testIsAuthenticatedAfterSetUser(): void
|
||||
{
|
||||
$this->manager->setUser(1, 'alice', 'user');
|
||||
|
||||
$this->assertTrue($this->manager->isAuthenticated());
|
||||
}
|
||||
|
||||
|
||||
// ── getUserId ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Sans session active, getUserId() doit retourner null.
|
||||
*/
|
||||
public function testGetUserIdWithoutSession(): void
|
||||
{
|
||||
$this->assertNull($this->manager->getUserId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Après setUser(), getUserId() doit retourner l'identifiant correct.
|
||||
*/
|
||||
public function testGetUserIdAfterSetUser(): void
|
||||
{
|
||||
$this->manager->setUser(42, 'alice', 'user');
|
||||
|
||||
$this->assertSame(42, $this->manager->getUserId());
|
||||
}
|
||||
|
||||
|
||||
// ── Rôles — isAdmin / isEditor ─────────────────────────────────
|
||||
|
||||
/**
|
||||
* Un utilisateur avec le rôle 'admin' doit être reconnu comme administrateur.
|
||||
*/
|
||||
public function testIsAdminWithAdminRole(): void
|
||||
{
|
||||
$this->manager->setUser(1, 'alice', 'admin');
|
||||
|
||||
$this->assertTrue($this->manager->isAdmin());
|
||||
$this->assertFalse($this->manager->isEditor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Un utilisateur avec le rôle 'editor' doit être reconnu comme éditeur.
|
||||
*/
|
||||
public function testIsEditorWithEditorRole(): void
|
||||
{
|
||||
$this->manager->setUser(1, 'alice', 'editor');
|
||||
|
||||
$this->assertFalse($this->manager->isAdmin());
|
||||
$this->assertTrue($this->manager->isEditor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Un utilisateur avec le rôle 'user' ne doit être ni admin ni éditeur.
|
||||
*/
|
||||
public function testUserRoleIsNeitherAdminNorEditor(): void
|
||||
{
|
||||
$this->manager->setUser(1, 'alice', 'user');
|
||||
|
||||
$this->assertFalse($this->manager->isAdmin());
|
||||
$this->assertFalse($this->manager->isEditor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sans session active, isAdmin() doit retourner false.
|
||||
*/
|
||||
public function testIsAdminWithoutSession(): void
|
||||
{
|
||||
$this->assertFalse($this->manager->isAdmin());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sans session active, isEditor() doit retourner false.
|
||||
*/
|
||||
public function testIsEditorWithoutSession(): void
|
||||
{
|
||||
$this->assertFalse($this->manager->isEditor());
|
||||
}
|
||||
|
||||
|
||||
// ── Données en session ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* setUser() doit écrire le username et le rôle dans $_SESSION.
|
||||
*/
|
||||
public function testSetUserWritesToSession(): void
|
||||
{
|
||||
$this->manager->setUser(5, 'bob', 'editor');
|
||||
|
||||
$this->assertSame(5, $_SESSION['user_id']);
|
||||
$this->assertSame('bob', $_SESSION['username']);
|
||||
$this->assertSame('editor', $_SESSION['role']);
|
||||
}
|
||||
|
||||
|
||||
// ── destroy ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Après destroy(), isAuthenticated() doit retourner false.
|
||||
*/
|
||||
public function testDestroyClearsSession(): void
|
||||
{
|
||||
$this->manager->setUser(1, 'alice', 'user');
|
||||
$this->manager->destroy();
|
||||
|
||||
$this->assertFalse($this->manager->isAuthenticated());
|
||||
$this->assertNull($this->manager->getUserId());
|
||||
$this->assertEmpty($_SESSION);
|
||||
}
|
||||
}
|
||||
122
tests/Kernel/SlugHelperTest.php
Normal file
122
tests/Kernel/SlugHelperTest.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Kernel;
|
||||
|
||||
use Netig\Netslim\Kernel\Support\Util\SlugHelper;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour SlugHelper::generate().
|
||||
*
|
||||
* Couvre la translittération ASCII, la normalisation en minuscules,
|
||||
* le remplacement des caractères non alphanumériques, et les cas limites.
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
final class SlugHelperTest extends TestCase
|
||||
{
|
||||
// ── Cas nominaux ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Une chaîne ASCII simple doit être mise en minuscules.
|
||||
*/
|
||||
public function testSimpleAsciiString(): void
|
||||
{
|
||||
$this->assertSame('hello-world', SlugHelper::generate('Hello World'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Les caractères accentués doivent être translittérés en ASCII.
|
||||
*/
|
||||
public function testAccentedCharacters(): void
|
||||
{
|
||||
$this->assertSame('ete-en-foret', SlugHelper::generate('Été en forêt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* La cédille et les caractères spéciaux courants doivent être translittérés.
|
||||
*/
|
||||
public function testCedillaAndSpecialCharacters(): void
|
||||
{
|
||||
$this->assertSame('ca-la', SlugHelper::generate('Ça & Là !'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Les tirets multiples consécutifs doivent être fusionnés en un seul.
|
||||
*/
|
||||
public function testMultipleConsecutiveHyphens(): void
|
||||
{
|
||||
$this->assertSame('foo-bar', SlugHelper::generate('foo bar'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Les tirets en début et fin de slug doivent être supprimés.
|
||||
*/
|
||||
public function testLeadingAndTrailingHyphen(): void
|
||||
{
|
||||
$this->assertSame('foo', SlugHelper::generate(' foo '));
|
||||
}
|
||||
|
||||
/**
|
||||
* Les chiffres doivent être conservés dans le slug.
|
||||
*/
|
||||
public function testDigitsPreserved(): void
|
||||
{
|
||||
$this->assertSame('contenu-2024', SlugHelper::generate('Contenu 2024'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Les tirets déjà présents dans la chaîne doivent être conservés (fusionnés si doublons).
|
||||
*/
|
||||
public function testHyphensInSourceString(): void
|
||||
{
|
||||
$this->assertSame('mon-contenu', SlugHelper::generate('mon-contenu'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Une chaîne entièrement en majuscules doit être passée en minuscules.
|
||||
*/
|
||||
public function testUppercaseString(): void
|
||||
{
|
||||
$this->assertSame('php-est-super', SlugHelper::generate('PHP EST SUPER'));
|
||||
}
|
||||
|
||||
|
||||
// ── Cas limites ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Une chaîne vide doit retourner une chaîne vide.
|
||||
*/
|
||||
public function testEmptyString(): void
|
||||
{
|
||||
$this->assertSame('', SlugHelper::generate(''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Une chaîne composée uniquement d'espaces doit retourner une chaîne vide.
|
||||
*/
|
||||
public function testSpacesOnlyString(): void
|
||||
{
|
||||
$this->assertSame('', SlugHelper::generate(' '));
|
||||
}
|
||||
|
||||
/**
|
||||
* Une chaîne composée uniquement de caractères spéciaux sans équivalent ASCII
|
||||
* doit retourner une chaîne vide.
|
||||
*/
|
||||
public function testCharactersWithoutAsciiEquivalent(): void
|
||||
{
|
||||
// Les caractères CJK n'ont pas d'équivalent ASCII//TRANSLIT
|
||||
$result = SlugHelper::generate('日本語');
|
||||
$this->assertSame('', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Un slug déjà valide doit rester identique.
|
||||
*/
|
||||
public function testAlreadyValidSlug(): void
|
||||
{
|
||||
$this->assertSame('mon-slug-valide', SlugHelper::generate('mon-slug-valide'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user