3 Commits
1.0.0 ... main

Author SHA1 Message Date
julien
6481dd0584 Minor changes 2026-03-22 12:49:53 +01:00
julien
24b3bb4177 Refactor core test runtime and simplify project documentation 2026-03-20 22:52:02 +01:00
julien
9b1dd9417c Gitignore database/ 2026-03-20 22:20:35 +01:00
20 changed files with 284 additions and 138 deletions

24
.gitignore vendored
View File

@@ -1,33 +1,17 @@
# ============================================ # Environment
# Environnement & Configuration
# ============================================
.env .env
# ============================================ # Composer
# Dépendances Composer
# ============================================
vendor/ vendor/
# ============================================ # Runtime caches and reports
# Base de données
# ============================================
database/*.sqlite
database/*.sqlite-shm
database/*.sqlite-wal
database/.provision.lock
# ============================================
# Cache & Logs
# ============================================
coverage/ coverage/
var/ var/
.php-cs-fixer.cache .php-cs-fixer.cache
.phpstan/ .phpstan/
.phpunit.result.cache .phpunit.result.cache
# ============================================ # IDE / OS
# IDE & OS
# ============================================
.vscode/ .vscode/
.idea/ .idea/
*.swp *.swp

View File

@@ -11,8 +11,6 @@ Ce dépôt est conçu pour être consommé par des projets applicatifs séparés
## Installation depuis le dépôt Git en HTTPS ## Installation depuis le dépôt Git en HTTPS
### Pendant le développement du core
```json ```json
{ {
"repositories": [ "repositories": [
@@ -22,30 +20,14 @@ Ce dépôt est conçu pour être consommé par des projets applicatifs séparés
} }
], ],
"require": { "require": {
"netig/netslim-core": "^0.3@dev" "netig/netslim-core": "dev-main"
}
}
```
### Après la première release taguée
```json
{
"repositories": [
{
"type": "vcs",
"url": "https://git.netig.net/netig/netslim-core.git"
}
],
"require": {
"netig/netslim-core": "^0.1"
} }
} }
``` ```
## Option locale pendant le développement ## Option locale pendant le développement
Pour développer le core et une application consommatrice côte à côte, un `path` repository local reste pratique, mais ce n'est pas le mode de consommation par défaut : Pour développer le core et une application consommatrice côte à côte, un `path` repository local reste pratique :
```json ```json
{ {
@@ -56,7 +38,7 @@ Pour développer le core et une application consommatrice côte à côte, un `pa
} }
], ],
"require": { "require": {
"netig/netslim-core": "^0.3@dev" "netig/netslim-core": "dev-main"
} }
} }
``` ```
@@ -69,7 +51,7 @@ Le package ne porte pas d'application concrète. Un projet consommateur doit fou
- ses templates applicatifs ; - ses templates applicatifs ;
- son pipeline d'assets. - son pipeline d'assets.
Si l'application active `Identity` et exécute le provisionnement initial, elle doit aussi définir `ADMIN_USERNAME`, `ADMIN_EMAIL` et `ADMIN_PASSWORD` dans son `.env`. Ces variables ne sont plus exigées par le bootstrap du noyau seul. Si l'application active `Identity` et exécute le provisionnement initial, elle doit aussi définir `ADMIN_USERNAME`, `ADMIN_EMAIL` et `ADMIN_PASSWORD` dans son `.env`.
Si l'application active `Notifications`, elle doit configurer `MAIL_HOST`, `MAIL_PORT`, `MAIL_USERNAME`, `MAIL_PASSWORD`, `MAIL_ENCRYPTION`, `MAIL_FROM` et `MAIL_FROM_NAME` pour permettre l'envoi effectif des emails transactionnels. Si l'application active `Notifications`, elle doit configurer `MAIL_HOST`, `MAIL_PORT`, `MAIL_USERNAME`, `MAIL_PASSWORD`, `MAIL_ENCRYPTION`, `MAIL_FROM` et `MAIL_FROM_NAME` pour permettre l'envoi effectif des emails transactionnels.
@@ -83,12 +65,8 @@ Les templates du socle supposent en particulier :
Le socle expose principalement : Le socle expose principalement :
- `Netig\Netslim\Kernel\...` pour le runtime public ; - `Netig\Netslim\Kernel\...` pour le runtime public ;
- les interfaces applicatives documentées des modules partagés (`Netig\Netslim\Identity\Application\*ServiceInterface`, `Netig\Netslim\Settings\Application\SettingsServiceInterface`, `Netig\Netslim\AuditLog\Application\AuditLogServiceInterface`, `Netig\Netslim\Notifications\Application\NotificationServiceInterface`, `Netig\Netslim\Taxonomy\Application\TaxonomyServiceInterface`, `Netig\Netslim\Media\Application\MediaServiceInterface`) ; - les interfaces applicatives documentées des modules partagés (`Identity`, `Settings`, `AuditLog`, `Notifications`, `Taxonomy`, `Media`) ;
- `Netig\Netslim\Settings\Contracts\...` ; - les contrats publics sous `Netig\Netslim\*/Contracts/` ;
- `Netig\Netslim\AuditLog\Contracts\...` ;
- `Netig\Netslim\Notifications\Contracts\...` ;
- `Netig\Netslim\Taxonomy\Contracts\...` ;
- `Netig\Netslim\Media\Contracts\...` ;
- les classes `*Module` des modules partagés. - les classes `*Module` des modules partagés.
La frontière détaillée entre API publique et API interne est documentée dans [`docs/PUBLIC_API.md`](docs/PUBLIC_API.md). La frontière détaillée entre API publique et API interne est documentée dans [`docs/PUBLIC_API.md`](docs/PUBLIC_API.md).
@@ -100,8 +78,4 @@ composer install
composer qa composer qa
``` ```
## Gouvernance du package > Quand `netslim-core` est installé via Composer, les chemins runtime détectent automatiquement la racine du projet consommateur pour les scripts CLI et les suites de tests qui n'appellent pas explicitement `Bootstrap::create()`.
- `docs/PUBLIC_API.md` définit la frontière supportée entre le package et les projets consommateurs ;
> Quand `netslim-core` est installé via Composer, les chemins runtime détectent automatiquement la racine du projet consommateur pour les scripts CLI et les suites de tests qui n'appellent pas explicitement `Bootstrap::create()`.

View File

@@ -1,6 +1,6 @@
{ {
"name": "netig/netslim-core", "name": "netig/netslim-core",
"description": "Reusable kernel and shared modules for NETslim based applications.", "description": "Reusable kernel and shared modules for Netslim-based applications.",
"license": "MIT", "license": "MIT",
"type": "library", "type": "library",
"require": { "require": {

View File

@@ -1,5 +0,0 @@
<?php
declare(strict_types=1);
return require __DIR__ . '/../tests/Fixtures/Application/config/modules.php';

View File

@@ -33,12 +33,4 @@ Quand un projet applicatif a besoin d'une capacité partagée, il doit préfére
2. à défaut, une extension documentée du runtime ou d'un module ; 2. à défaut, une extension documentée du runtime ou d'un module ;
3. en dernier recours, une évolution du core qui ajoute un nouveau point d'intégration public. 3. en dernier recours, une évolution du core qui ajoute un nouveau point d'intégration public.
Éviter de se brancher directement sur des classes internes permet de garder le core versionnable et évolutif. Éviter de se brancher directement sur des classes internes permet de garder le core petit, lisible et évolutif.
## Stabilité attendue
- l'API publique est la cible de compatibilité entre versions ;
- l'API interne peut évoluer à tout moment tant que le comportement public documenté reste cohérent ;
- tout nouveau point d'extension réutilisable doit être documenté ici ou dans `README.md` / `MODULES.md`.
## Versionnement

View File

@@ -26,4 +26,4 @@ Ce dossier contient les documents de référence du socle `netslim-core`.
| [MODULES.md](MODULES.md) | Charte de module et conventions de frontière | | [MODULES.md](MODULES.md) | Charte de module et conventions de frontière |
| [PUBLIC_API.md](PUBLIC_API.md) | Délimitation de l'API publique et de l'API interne | | [PUBLIC_API.md](PUBLIC_API.md) | Délimitation de l'API publique et de l'API interne |
| [DEVELOPMENT.md](DEVELOPMENT.md) | Guide de travail quotidien, variables d'environnement et checklist avant push | | [DEVELOPMENT.md](DEVELOPMENT.md) | Guide de travail quotidien, variables d'environnement et checklist avant push |
| [../CONTRIBUTING.md](../CONTRIBUTING.md) | Règles de contribution et attentes sur les tests | | [../CONTRIBUTING.md](../CONTRIBUTING.md) | Règles de contribution et attentes sur les tests |

View File

@@ -97,12 +97,11 @@ class LocalMediaStorage implements MediaStorageInterface
public function storePreparedUpload(UploadedMediaInterface $uploadedFile, PreparedMediaUpload $preparedUpload): string public function storePreparedUpload(UploadedMediaInterface $uploadedFile, PreparedMediaUpload $preparedUpload): string
{ {
if (!is_dir($this->uploadDir) && !@mkdir($this->uploadDir, 0755, true)) { $this->ensureUploadDirectoryExists();
throw new StorageException("Impossible de créer le répertoire d'upload");
}
$filename = uniqid('', true) . '_' . bin2hex(random_bytes(4)) . '.' . $preparedUpload->getExtension(); $filename = uniqid('', true) . '_' . bin2hex(random_bytes(4)) . '.' . $preparedUpload->getExtension();
$destPath = $this->uploadDir . DIRECTORY_SEPARATOR . $filename; $destPath = $this->uploadDir . DIRECTORY_SEPARATOR . $filename;
$this->ensureParentDirectoryExists($destPath);
if ($preparedUpload->shouldCopyFromTemporaryPath()) { if ($preparedUpload->shouldCopyFromTemporaryPath()) {
if (!copy($preparedUpload->getTemporaryPath(), $destPath)) { if (!copy($preparedUpload->getTemporaryPath(), $destPath)) {
@@ -136,6 +135,22 @@ class LocalMediaStorage implements MediaStorageInterface
} }
} }
private function ensureUploadDirectoryExists(): void
{
if (!is_dir($this->uploadDir) && !@mkdir($this->uploadDir, 0755, true) && !is_dir($this->uploadDir)) {
throw new StorageException("Impossible de créer le répertoire d'upload");
}
}
private function ensureParentDirectoryExists(string $path): void
{
$directory = dirname($path);
if (!is_dir($directory) && !@mkdir($directory, 0755, true) && !is_dir($directory)) {
throw new StorageException("Impossible de préparer le répertoire cible du média");
}
}
private function assertReasonableDimensions(string $path): void private function assertReasonableDimensions(string $path): void
{ {
$size = @getimagesize($path); $size = @getimagesize($path);

View File

@@ -18,7 +18,6 @@ use Netig\Netslim\Identity\UI\Http\UserController;
use Netig\Netslim\Kernel\Http\Infrastructure\Twig\AppExtension; use Netig\Netslim\Kernel\Http\Infrastructure\Twig\AppExtension;
use Netig\Netslim\Kernel\Runtime\Http\MiddlewareRegistrar; use Netig\Netslim\Kernel\Runtime\Http\MiddlewareRegistrar;
use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry; use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry;
use Netig\Netslim\Kernel\Runtime\RuntimePaths;
use Netig\Netslim\Media\Application\MediaServiceInterface; use Netig\Netslim\Media\Application\MediaServiceInterface;
use Netig\Netslim\Media\UI\Http\MediaController; use Netig\Netslim\Media\UI\Http\MediaController;
use Netig\Netslim\Notifications\Application\NotificationServiceInterface; use Netig\Netslim\Notifications\Application\NotificationServiceInterface;
@@ -30,6 +29,7 @@ use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Slim\Factory\AppFactory; use Slim\Factory\AppFactory;
use Slim\Views\Twig; use Slim\Views\Twig;
use Tests\Support\TestRuntimeFactory;
use Twig\Loader\FilesystemLoader; use Twig\Loader\FilesystemLoader;
final class ContainerWiringIntegrationTest extends TestCase final class ContainerWiringIntegrationTest extends TestCase
@@ -56,8 +56,7 @@ final class ContainerWiringIntegrationTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application'); TestRuntimeFactory::resetRuntime();
ModuleRegistry::reset();
foreach (self::ENV_DEFAULTS as $key => $value) { foreach (self::ENV_DEFAULTS as $key => $value) {
$this->envBackup[$key] = $_ENV[$key] ?? null; $this->envBackup[$key] = $_ENV[$key] ?? null;
$_ENV[$key] = $value; $_ENV[$key] = $value;
@@ -66,8 +65,6 @@ final class ContainerWiringIntegrationTest extends TestCase
protected function tearDown(): void protected function tearDown(): void
{ {
RuntimePaths::resetApplicationRoot();
RuntimePaths::resetProjectRoot();
ModuleRegistry::reset(); ModuleRegistry::reset();
foreach (self::ENV_DEFAULTS as $key => $_) { foreach (self::ENV_DEFAULTS as $key => $_) {
$previous = $this->envBackup[$key] ?? null; $previous = $this->envBackup[$key] ?? null;

View File

@@ -6,14 +6,14 @@ namespace Tests\Kernel;
use Netig\Netslim\Kernel\Persistence\Infrastructure\DatabaseNotProvisionedException; use Netig\Netslim\Kernel\Persistence\Infrastructure\DatabaseNotProvisionedException;
use Netig\Netslim\Kernel\Persistence\Infrastructure\DatabaseReadiness; use Netig\Netslim\Kernel\Persistence\Infrastructure\DatabaseReadiness;
use PDO;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Tests\Support\TestDatabaseFactory;
final class DatabaseReadinessTest extends TestCase final class DatabaseReadinessTest extends TestCase
{ {
public function testAssertProvisionedFailsWhenModuleTablesAreMissing(): void public function testAssertProvisionedFailsWhenModuleTablesAreMissing(): void
{ {
$db = new PDO('sqlite::memory:'); $db = TestDatabaseFactory::createInMemory();
$db->exec('CREATE TABLE migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, version TEXT, run_at TEXT)'); $db->exec('CREATE TABLE migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, version TEXT, run_at TEXT)');
$this->expectException(DatabaseNotProvisionedException::class); $this->expectException(DatabaseNotProvisionedException::class);
@@ -24,7 +24,7 @@ final class DatabaseReadinessTest extends TestCase
public function testAssertProvisionedAcceptsCompleteCoreSchema(): void public function testAssertProvisionedAcceptsCompleteCoreSchema(): void
{ {
$db = new PDO('sqlite::memory:'); $db = TestDatabaseFactory::createInMemory();
$db->exec('CREATE TABLE migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, version TEXT, run_at TEXT)'); $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 users (id INTEGER PRIMARY KEY, username TEXT, email TEXT, password_hash TEXT, role TEXT, session_version INTEGER, created_at TEXT)');

View File

@@ -5,8 +5,8 @@ declare(strict_types=1);
namespace Tests\Kernel; namespace Tests\Kernel;
use Netig\Netslim\Kernel\Persistence\Infrastructure\Migrator; use Netig\Netslim\Kernel\Persistence\Infrastructure\Migrator;
use PDO;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Tests\Support\TestDatabaseFactory;
/** /**
* Tests unitaires pour Migrator. * Tests unitaires pour Migrator.
@@ -18,14 +18,11 @@ use PHPUnit\Framework\TestCase;
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] #[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
final class MigratorTest extends TestCase final class MigratorTest extends TestCase
{ {
private PDO $db; private \PDO $db;
protected function setUp(): void protected function setUp(): void
{ {
$this->db = new PDO('sqlite::memory:', options: [ $this->db = TestDatabaseFactory::createInMemory();
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} }
public function testRunCreatesMigrationsTable(): void public function testRunCreatesMigrationsTable(): void

View File

@@ -8,26 +8,23 @@ use Netig\Netslim\Kernel\Runtime\Module\ModuleInterface;
use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry; use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry;
use Netig\Netslim\Kernel\Runtime\RuntimePaths; use Netig\Netslim\Kernel\Runtime\RuntimePaths;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Tests\Support\TestRuntimeFactory;
final class ModuleRegistryTest extends TestCase final class ModuleRegistryTest extends TestCase
{ {
protected function setUp(): void protected function setUp(): void
{ {
RuntimePaths::resetApplicationRoot(); TestRuntimeFactory::resetRuntime();
RuntimePaths::resetProjectRoot();
ModuleRegistry::reset();
} }
protected function tearDown(): void protected function tearDown(): void
{ {
RuntimePaths::resetApplicationRoot(); TestRuntimeFactory::resetRuntime();
RuntimePaths::resetProjectRoot();
ModuleRegistry::reset();
} }
public function testModulesAreDeclaredInExpectedOrderForFixtureApplication(): void public function testModulesAreDeclaredInExpectedOrderForFixtureApplication(): void
{ {
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application'); TestRuntimeFactory::resetRuntime();
$moduleClasses = array_map( $moduleClasses = array_map(
static fn (ModuleInterface $module): string => $module::class, static fn (ModuleInterface $module): string => $module::class,
@@ -47,7 +44,7 @@ final class ModuleRegistryTest extends TestCase
public function testModuleClassNamesExposeTheActiveApplicationComposition(): void public function testModuleClassNamesExposeTheActiveApplicationComposition(): void
{ {
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application'); TestRuntimeFactory::resetRuntime();
self::assertSame([ self::assertSame([
'Netig\Netslim\Kernel\Runtime\KernelModule', 'Netig\Netslim\Kernel\Runtime\KernelModule',
@@ -62,14 +59,14 @@ final class ModuleRegistryTest extends TestCase
public function testApplicationManifestIsResolvedFromTheActiveApplicationRoot(): void public function testApplicationManifestIsResolvedFromTheActiveApplicationRoot(): void
{ {
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application'); TestRuntimeFactory::resetRuntime();
self::assertFileExists(RuntimePaths::getApplicationConfigPath('modules.php')); self::assertFileExists(RuntimePaths::getApplicationConfigPath('modules.php'));
} }
public function testEveryModuleExposesResolvableMetadata(): void public function testEveryModuleExposesResolvableMetadata(): void
{ {
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application'); TestRuntimeFactory::resetRuntime();
foreach (ModuleRegistry::modules() as $module) { foreach (ModuleRegistry::modules() as $module) {
self::assertNotSame([], $module->definitions(), $module::class . ' should expose DI definitions.'); self::assertNotSame([], $module->definitions(), $module::class . ' should expose DI definitions.');

View File

@@ -6,21 +6,18 @@ namespace Tests\Kernel;
use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry; use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry;
use Netig\Netslim\Kernel\Runtime\Module\ProvidesSchemaInterface; use Netig\Netslim\Kernel\Runtime\Module\ProvidesSchemaInterface;
use Netig\Netslim\Kernel\Runtime\RuntimePaths;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Tests\Support\TestRuntimeFactory;
final class ModuleSchemaTest extends TestCase final class ModuleSchemaTest extends TestCase
{ {
protected function setUp(): void protected function setUp(): void
{ {
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application'); TestRuntimeFactory::resetRuntime();
ModuleRegistry::reset();
} }
protected function tearDown(): void protected function tearDown(): void
{ {
RuntimePaths::resetApplicationRoot();
RuntimePaths::resetProjectRoot();
ModuleRegistry::reset(); ModuleRegistry::reset();
} }

View File

@@ -5,14 +5,15 @@ declare(strict_types=1);
namespace Tests\Kernel; namespace Tests\Kernel;
use Netig\Netslim\Kernel\Persistence\Infrastructure\Provisioner; use Netig\Netslim\Kernel\Persistence\Infrastructure\Provisioner;
use PDO;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Tests\Support\TestDatabaseFactory;
use Tests\Support\TestRuntimeFactory;
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] #[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
final class ProvisionerTest extends TestCase final class ProvisionerTest extends TestCase
{ {
private PDO $db; private \PDO $db;
private string $lockPath; private string $lockPath;
@@ -20,13 +21,11 @@ final class ProvisionerTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
$this->db = new PDO('sqlite::memory:', options: [ TestRuntimeFactory::resetRuntime();
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, $this->db = TestDatabaseFactory::createInMemory();
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$this->db->sqliteCreateFunction('strip_tags', 'strip_tags', 1); $this->db->sqliteCreateFunction('strip_tags', 'strip_tags', 1);
$this->lockPath = dirname(__DIR__, 2) . '/database/.provision.lock'; $this->lockPath = TestRuntimeFactory::path('database/.provision.lock');
@unlink($this->lockPath); @unlink($this->lockPath);
$this->envBackup = [ $this->envBackup = [

View File

@@ -6,23 +6,20 @@ namespace Tests\Kernel;
use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry; use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry;
use Netig\Netslim\Kernel\Runtime\Routing\Routes; use Netig\Netslim\Kernel\Runtime\Routing\Routes;
use Netig\Netslim\Kernel\Runtime\RuntimePaths;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Slim\Factory\AppFactory; use Slim\Factory\AppFactory;
use Tests\Support\TestRuntimeFactory;
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] #[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
final class RoutesTest extends TestCase final class RoutesTest extends TestCase
{ {
protected function setUp(): void protected function setUp(): void
{ {
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application'); TestRuntimeFactory::resetRuntime();
ModuleRegistry::reset();
} }
protected function tearDown(): void protected function tearDown(): void
{ {
RuntimePaths::resetApplicationRoot();
RuntimePaths::resetProjectRoot();
ModuleRegistry::reset(); ModuleRegistry::reset();
} }

View File

@@ -6,40 +6,40 @@ namespace Tests\Kernel;
use Netig\Netslim\Kernel\Runtime\RuntimePaths; use Netig\Netslim\Kernel\Runtime\RuntimePaths;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Tests\Support\TestRuntimeFactory;
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] #[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
final class RuntimePathsTest extends TestCase final class RuntimePathsTest extends TestCase
{ {
protected function tearDown(): void protected function tearDown(): void
{ {
RuntimePaths::resetApplicationRoot(); TestRuntimeFactory::resetRuntime();
RuntimePaths::resetProjectRoot();
} }
public function testGetProjectRootReturnsRepositoryRoot(): void public function testGetProjectRootReturnsIsolatedTestProjectRoot(): void
{ {
self::assertSame(dirname(__DIR__, 2), RuntimePaths::getProjectRoot()); self::assertSame(TestRuntimeFactory::projectRoot(), RuntimePaths::getProjectRoot());
} }
public function testGetConfigPathReturnsRootConfigDirectoryAndFilePath(): void public function testGetConfigPathReturnsRootConfigDirectoryAndFilePath(): void
{ {
self::assertSame(dirname(__DIR__, 2) . '/config', RuntimePaths::getConfigPath()); self::assertSame(TestRuntimeFactory::path('config'), RuntimePaths::getConfigPath());
self::assertSame(dirname(__DIR__, 2) . '/config/modules.php', RuntimePaths::getConfigPath('modules.php')); self::assertSame(TestRuntimeFactory::path('config/modules.php'), RuntimePaths::getConfigPath('modules.php'));
} }
public function testApplicationRootDefaultsToProjectRoot(): void public function testApplicationRootDefaultsToProjectRoot(): void
{ {
self::assertSame(dirname(__DIR__, 2), RuntimePaths::getApplicationRoot()); self::assertSame(TestRuntimeFactory::applicationRoot(), RuntimePaths::getApplicationRoot());
self::assertSame(dirname(__DIR__, 2) . '/config', RuntimePaths::getApplicationConfigPath()); self::assertSame(TestRuntimeFactory::path('config'), RuntimePaths::getApplicationConfigPath());
} }
public function testApplicationRootCanPointToFixtureApplication(): void public function testApplicationRootCanPointToTheIsolatedFixtureApplication(): void
{ {
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application'); RuntimePaths::setApplicationRoot(TestRuntimeFactory::applicationRoot());
self::assertSame(dirname(__DIR__, 2) . '/tests/Fixtures/Application', RuntimePaths::getApplicationRoot()); self::assertSame(TestRuntimeFactory::applicationRoot(), RuntimePaths::getApplicationRoot());
self::assertSame(dirname(__DIR__, 2) . '/tests/Fixtures/Application/config/modules.php', RuntimePaths::getApplicationConfigPath('modules.php')); self::assertSame(TestRuntimeFactory::path('config/modules.php'), RuntimePaths::getApplicationConfigPath('modules.php'));
self::assertSame(dirname(__DIR__, 2) . '/tests/Fixtures/Application/templates/Kernel', RuntimePaths::getApplicationPath('templates/Kernel')); self::assertSame(TestRuntimeFactory::path('templates/Kernel'), RuntimePaths::getApplicationPath('templates/Kernel'));
} }
public function testGetTwigCacheReturnsFalseInDev(): void public function testGetTwigCacheReturnsFalseInDev(): void
@@ -57,7 +57,7 @@ final class RuntimePathsTest extends TestCase
public function testGetDatabasePathCreatesDatabaseFileWhenMissing(): void public function testGetDatabasePathCreatesDatabaseFileWhenMissing(): void
{ {
$dbFile = dirname(__DIR__, 2) . '/database/app.sqlite'; $dbFile = TestRuntimeFactory::path('database/app.sqlite');
$dbDir = dirname($dbFile); $dbDir = dirname($dbFile);
$backup = $dbFile . '.bak-test'; $backup = $dbFile . '.bak-test';

View File

@@ -5,20 +5,19 @@ declare(strict_types=1);
namespace Tests\Media; namespace Tests\Media;
use Netig\Netslim\Kernel\Persistence\Infrastructure\Migrator; use Netig\Netslim\Kernel\Persistence\Infrastructure\Migrator;
use PDO;
use PDOException; use PDOException;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Tests\Support\TestDatabaseFactory;
use Tests\Support\TestRuntimeFactory;
final class MediaSchemaIntegrationTest extends TestCase final class MediaSchemaIntegrationTest extends TestCase
{ {
private PDO $db; private \PDO $db;
protected function setUp(): void protected function setUp(): void
{ {
$this->db = new PDO('sqlite::memory:', options: [ TestRuntimeFactory::resetRuntime();
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, $this->db = TestDatabaseFactory::createInMemory();
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$this->db->sqliteCreateFunction('strip_tags', 'strip_tags', 1); $this->db->sqliteCreateFunction('strip_tags', 'strip_tags', 1);
Migrator::run($this->db); Migrator::run($this->db);

9
tests/README.md Normal file
View File

@@ -0,0 +1,9 @@
# Tests du core
La suite de tests prépare une mini application consommatrice éphémère sous un répertoire temporaire. Les chemins runtime (`database/`, `var/`, `public/media/`) ne pointent donc ni vers le dépôt du core ni vers un projet client réel.
Organisation :
- `Architecture/` : garde-fous de dépendances et de structure
- `Kernel/` : runtime, bootstrap, wiring, utilitaires transverses
- `Support/` : helpers de tests et fabrication du runtime de test
- `Fixtures/` : manifeste et templates minimaux de l'application de test

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Tests\Support;
use PDO;
/**
* Fabrique légère pour les bases SQLite de test.
*/
final class TestDatabaseFactory
{
public static function createInMemory(): PDO
{
return new PDO('sqlite::memory:', options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
public static function createFileBacked(string $name): PDO
{
$path = TestRuntimeFactory::path('database/' . $name . '.sqlite');
if (is_file($path)) {
@unlink($path);
}
return new PDO('sqlite:' . $path, options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
}

View File

@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace Tests\Support;
use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry;
use Netig\Netslim\Kernel\Runtime\RuntimePaths;
/**
* Prépare une mini application consommatrice éphémère pour les tests du core.
*
* Tous les chemins runtime persistants (database/, var/, public/media) sont
* créés sous un répertoire temporaire isolé afin que la suite de tests ne
* touche ni le dépôt courant ni un projet client réel.
*/
final class TestRuntimeFactory
{
private static ?string $projectRoot = null;
public static function boot(): void
{
if (self::$projectRoot !== null) {
self::applyRuntimeRoots();
return;
}
$baseRoot = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'netslim-core-tests';
if (!is_dir($baseRoot)) {
mkdir($baseRoot, 0777, true);
}
$projectRoot = $baseRoot . DIRECTORY_SEPARATOR . 'run-' . bin2hex(random_bytes(6));
mkdir($projectRoot, 0777, true);
self::copyDirectory(self::fixtureRoot() . '/config', $projectRoot . '/config');
self::copyDirectory(self::fixtureRoot() . '/templates', $projectRoot . '/templates');
foreach (['database', 'var/cache/twig', 'var/cache/htmlpurifier', 'var/logs', 'public/media'] as $directory) {
$path = $projectRoot . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $directory);
if (!is_dir($path)) {
mkdir($path, 0777, true);
}
}
self::$projectRoot = $projectRoot;
self::applyRuntimeRoots();
register_shutdown_function(static function (): void {
TestRuntimeFactory::cleanup();
});
}
public static function projectRoot(): string
{
self::boot();
return self::$projectRoot;
}
public static function applicationRoot(): string
{
return self::projectRoot();
}
public static function path(string $relativePath = ''): string
{
$root = self::projectRoot();
return $relativePath === ''
? $root
: $root . DIRECTORY_SEPARATOR . ltrim(str_replace('/', DIRECTORY_SEPARATOR, $relativePath), DIRECTORY_SEPARATOR);
}
public static function resetRuntime(): void
{
self::boot();
self::applyRuntimeRoots();
}
private static function applyRuntimeRoots(): void
{
if (self::$projectRoot === null) {
throw new \LogicException('Test runtime project root is not initialized.');
}
RuntimePaths::setProjectRoot(self::$projectRoot);
RuntimePaths::setApplicationRoot(self::$projectRoot);
ModuleRegistry::reset();
}
public static function cleanup(): void
{
if (self::$projectRoot === null || !is_dir(self::$projectRoot)) {
return;
}
self::deleteDirectory(self::$projectRoot);
self::$projectRoot = null;
RuntimePaths::resetProjectRoot();
RuntimePaths::resetApplicationRoot();
ModuleRegistry::reset();
}
private static function fixtureRoot(): string
{
return dirname(__DIR__) . '/Fixtures/Application';
}
private static function copyDirectory(string $source, string $destination): void
{
if (!is_dir($destination)) {
mkdir($destination, 0777, true);
}
$items = scandir($source);
if ($items === false) {
throw new \RuntimeException(sprintf('Unable to read fixture directory: %s', $source));
}
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$from = $source . DIRECTORY_SEPARATOR . $item;
$to = $destination . DIRECTORY_SEPARATOR . $item;
if (is_dir($from)) {
self::copyDirectory($from, $to);
continue;
}
copy($from, $to);
}
}
private static function deleteDirectory(string $path): void
{
$items = scandir($path);
if ($items === false) {
return;
}
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$current = $path . DIRECTORY_SEPARATOR . $item;
if (is_dir($current)) {
self::deleteDirectory($current);
continue;
}
@unlink($current);
}
@rmdir($path);
}
}

View File

@@ -4,9 +4,6 @@ declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php'; require dirname(__DIR__) . '/vendor/autoload.php';
use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry; use Tests\Support\TestRuntimeFactory;
use Netig\Netslim\Kernel\Runtime\RuntimePaths;
RuntimePaths::setApplicationRoot(dirname(__DIR__) . '/tests/Fixtures/Application'); TestRuntimeFactory::boot();
RuntimePaths::setProjectRoot(dirname(__DIR__));
ModuleRegistry::reset();