diff --git a/CLAUDE.md b/CLAUDE.md index aff1a9bf8..22af6c3cf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -157,3 +157,14 @@ More text here. ### MD047 - Files should end with a single newline Ensure every markdown file ends with exactly one newline character at the end of the file. + +## Tests + +- Need comprehensive test coverage for new components +- Utilize Pest PHP for unit and integration / feature testing +- Tests should focus on validating modern architecture components +- Create test suites for critical systems like database, cache, and configuration + +## Development Guidelines + +- Always ensure that there is one empty line at the end of the file diff --git a/composer.json b/composer.json index 1f3dce449..97cf8ec23 100644 --- a/composer.json +++ b/composer.json @@ -67,6 +67,7 @@ "nette/caching": "^3.3", "nette/database": "^3.2", "php-curl-class/php-curl-class": "^12.0.0", + "php-di/php-di": "^7.0", "robmorgan/phinx": "^0.16.9", "samdark/sitemap": "2.4.1", "symfony/mailer": "^7.3", @@ -82,7 +83,10 @@ "autoload": { "psr-4": { "TorrentPier\\": "src/" - } + }, + "files": [ + "src/helpers.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/composer.lock b/composer.lock index 0bbb016c8..a513013f6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "26f36d2312e2eabf3ed5ff36391cc050", + "content-hash": "57713d8849e71683b70d934a81f7e18c", "packages": [ { "name": "arokettu/bencode", @@ -1879,6 +1879,67 @@ ], "time": "2024-09-11T14:15:04+00:00" }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2025-03-19T13:51:03+00:00" + }, { "name": "league/color-extractor", "version": "0.4.0", @@ -2740,6 +2801,134 @@ }, "time": "2025-03-25T18:04:16+00:00" }, + { + "name": "php-di/invoker", + "version": "2.3.6", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/Invoker.git", + "reference": "59f15608528d8a8838d69b422a919fd6b16aa576" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/59f15608528d8a8838d69b422a919fd6b16aa576", + "reference": "59f15608528d8a8838d69b422a919fd6b16aa576", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "psr/container": "^1.0|^2.0" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "mnapoli/hard-mode": "~0.3.0", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Invoker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Generic and extensible callable invoker", + "homepage": "https://github.com/PHP-DI/Invoker", + "keywords": [ + "callable", + "dependency", + "dependency-injection", + "injection", + "invoke", + "invoker" + ], + "support": { + "issues": "https://github.com/PHP-DI/Invoker/issues", + "source": "https://github.com/PHP-DI/Invoker/tree/2.3.6" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2025-01-17T12:49:27+00:00" + }, + { + "name": "php-di/php-di", + "version": "7.0.11", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/PHP-DI.git", + "reference": "32f111a6d214564520a57831d397263e8946c1d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/32f111a6d214564520a57831d397263e8946c1d2", + "reference": "32f111a6d214564520a57831d397263e8946c1d2", + "shasum": "" + }, + "require": { + "laravel/serializable-closure": "^1.0 || ^2.0", + "php": ">=8.0", + "php-di/invoker": "^2.0", + "psr/container": "^1.1 || ^2.0" + }, + "provide": { + "psr/container-implementation": "^1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "friendsofphp/proxy-manager-lts": "^1", + "mnapoli/phpunit-easymock": "^1.3", + "phpunit/phpunit": "^9.6 || ^10 || ^11", + "vimeo/psalm": "^5|^6" + }, + "suggest": { + "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "DI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The dependency injection container for humans", + "homepage": "https://php-di.org/", + "keywords": [ + "PSR-11", + "container", + "container-interop", + "dependency injection", + "di", + "ioc", + "psr11" + ], + "support": { + "issues": "https://github.com/PHP-DI/PHP-DI/issues", + "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.11" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/php-di/php-di", + "type": "tidelift" + } + ], + "time": "2025-06-03T07:45:57+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.3", diff --git a/config/container.php b/config/container.php new file mode 100644 index 000000000..cc72536ee --- /dev/null +++ b/config/container.php @@ -0,0 +1,26 @@ + env('APP_ENV', 'development'), + 'debug' => env('APP_DEBUG', false), + + // Enable/disable features + 'autowiring' => true, + 'annotations' => false, + + // Compilation settings for production + 'compilation_dir' => __DIR__ . '/../internal_data/cache/container', + 'proxies_dir' => __DIR__ . '/../internal_data/cache/proxies', + + // Additional definition files to load + 'definition_files' => [ + // Add custom definition files here + // __DIR__ . '/services/custom.php', + ], + + // Container-specific settings + 'container' => [ + // Add any PHP-DI specific settings here + ], +]; diff --git a/config/environments/.keep b/config/environments/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/config/services.php b/config/services.php new file mode 100644 index 000000000..5070cdf57 --- /dev/null +++ b/config/services.php @@ -0,0 +1,30 @@ + factory(function () { + // $logger = new \Monolog\Logger('torrentpier'); + // $logger->pushHandler(new \Monolog\Handler\StreamHandler(__DIR__ . '/../internal_data/logs/app.log')); + // return $logger; + // }), + + // Configuration service example + // 'config' => factory(function () { + // return [ + // 'app' => require __DIR__ . '/app.php', + // 'database' => require __DIR__ . '/database.php', + // 'cache' => require __DIR__ . '/cache.php', + // ]; + // }), + + // Interface to implementation binding example + // 'ServiceInterface' => autowire('ConcreteService'), +]; diff --git a/docs/examples/di-container-usage.php b/docs/examples/di-container-usage.php new file mode 100644 index 000000000..fcfc0bdf9 --- /dev/null +++ b/docs/examples/di-container-usage.php @@ -0,0 +1,99 @@ +has('some.service'); // Check if service exists + +// ===== PHASE 2: Domain Modeling (FUTURE) ===== + +// 3. Repository interfaces (when implemented in Domain layer) +// $userRepository = app('TorrentPier\Domain\User\Repository\UserRepositoryInterface'); +// $torrentRepository = app('TorrentPier\Domain\Tracker\Repository\TorrentRepositoryInterface'); +// $forumRepository = app('TorrentPier\Domain\Forum\Repository\ForumRepositoryInterface'); + +// ===== PHASE 3: Application Services (FUTURE) ===== + +// 4. Command/Query handlers (when implemented) +// $registerUserHandler = app('TorrentPier\Application\User\Handler\RegisterUserHandler'); +// $announceHandler = app('TorrentPier\Application\Tracker\Handler\ProcessAnnounceHandler'); +// $createPostHandler = app('TorrentPier\Application\Forum\Handler\CreatePostHandler'); + +// 5. Making command instances with parameters +// $command = $container->make('TorrentPier\Application\User\Command\RegisterUserCommand', [ +// 'username' => 'john_doe', +// 'email' => 'john@example.com', +// 'password' => 'secure_password' +// ]); + +// ===== PHASE 4: Infrastructure (FUTURE) ===== + +// 6. Database and cache (when infrastructure is implemented) +// $database = app('database.connection.default'); +// $cache = app('cache.factory')('forum'); // Get cache instance for 'forum' namespace + +// ===== PHASE 5: Presentation (FUTURE) ===== + +// 7. Controllers (when implemented) +// $userController = app('TorrentPier\Presentation\Http\Controllers\Api\UserController'); +// $trackerController = app('TorrentPier\Presentation\Http\Controllers\Web\TrackerController'); + +// ===== TESTING EXAMPLES ===== + +// 8. Testing with custom container (works now) +$testContainer = ContainerFactory::create([ + 'definitions' => [ + 'test.service' => \DI\factory(function () { + return new class { + public function test() { return 'test'; } + }; + }), + ], + 'environment' => 'testing', +]); + +// 9. Safe service resolution (works now) +try { + $service = app('optional.service'); +} catch (RuntimeException $e) { + // Service not found, handle gracefully + $service = null; +} + +// ===== LEGACY INTEGRATION (CURRENT) ===== + +// 10. Integration with legacy code +// In legacy files, after including common.php or similar: +if (!Bootstrap::getContainer()) { + Bootstrap::init(BB_ROOT ?? __DIR__ . '/../..'); +} + +// 11. Method injection (works now if service exists) +class ExampleService +{ + public function processData(string $data) + { + // Container can inject dependencies when calling this method + return "Processed: $data"; + } +} + +$exampleService = new ExampleService(); +$result = $container->call([$exampleService, 'processData'], [ + 'data' => 'test data' +]); \ No newline at end of file diff --git a/src/Infrastructure/DependencyInjection/Bootstrap.php b/src/Infrastructure/DependencyInjection/Bootstrap.php new file mode 100644 index 000000000..8eee918ee --- /dev/null +++ b/src/Infrastructure/DependencyInjection/Bootstrap.php @@ -0,0 +1,89 @@ +getWrappedContainer()->set(Container::class, self::$container); + self::$container->getWrappedContainer()->set('container', self::$container); + + return self::$container; + } + + public static function getContainer(): ?Container + { + return self::$container; + } + + public static function reset(): void + { + self::$container = null; + } + + private static function loadEnvironment(string $rootPath): void + { + if (file_exists($rootPath . '/.env')) { + $dotenv = Dotenv::createImmutable($rootPath); + $dotenv->load(); + } + } + + private static function loadConfiguration(string $rootPath, array $config): array + { + // Load base configuration + $configPath = $rootPath . '/config'; + + // Container configuration + if (file_exists($configPath . '/container.php')) { + $containerConfig = require $configPath . '/container.php'; + $config = array_merge($config, $containerConfig); + } + + // Services configuration + if (file_exists($configPath . '/services.php')) { + $servicesConfig = require $configPath . '/services.php'; + $config['definitions'] = array_merge( + $config['definitions'] ?? [], + $servicesConfig + ); + } + + // Database configuration + if (file_exists($configPath . '/database.php')) { + $config['database'] = require $configPath . '/database.php'; + } + + // Cache configuration + if (file_exists($configPath . '/cache.php')) { + $config['cache'] = require $configPath . '/cache.php'; + } + + // Environment from .env + $config['environment'] = $_ENV['APP_ENV'] ?? 'development'; + $config['debug'] = filter_var($_ENV['APP_DEBUG'] ?? false, FILTER_VALIDATE_BOOLEAN); + + return $config; + } +} diff --git a/src/Infrastructure/DependencyInjection/Container.php b/src/Infrastructure/DependencyInjection/Container.php new file mode 100644 index 000000000..25254bd3c --- /dev/null +++ b/src/Infrastructure/DependencyInjection/Container.php @@ -0,0 +1,49 @@ +container = $container; + } + + public function get(string $id): mixed + { + return $this->container->get($id); + } + + public function has(string $id): bool + { + return $this->container->has($id); + } + + public function make(string $name, array $parameters = []): mixed + { + return $this->container->make($name, $parameters); + } + + public function call(callable $callable, array $parameters = []): mixed + { + return $this->container->call($callable, $parameters); + } + + public function injectOn(object $instance): object + { + return $this->container->injectOn($instance); + } + + public function getWrappedContainer(): DIContainer + { + return $this->container; + } +} diff --git a/src/Infrastructure/DependencyInjection/ContainerFactory.php b/src/Infrastructure/DependencyInjection/ContainerFactory.php new file mode 100644 index 000000000..e4424c230 --- /dev/null +++ b/src/Infrastructure/DependencyInjection/ContainerFactory.php @@ -0,0 +1,93 @@ +build(); + $container = new Container($diContainer); + + // Register and boot service providers + self::registerProviders($container, $config); + + return $container; + } + + private static function configureContainer(ContainerBuilder $builder, array $config): void + { + // Enable compilation in production for better performance + $isProduction = ($config['environment'] ?? 'development') === 'production'; + + if ($isProduction) { + $builder->enableCompilation($config['compilation_dir'] ?? __DIR__ . '/../../../internal_data/cache/container'); + $builder->writeProxiesToFile(true, $config['proxies_dir'] ?? __DIR__ . '/../../../internal_data/cache/proxies'); + } + + // Enable autowiring by default + $builder->useAutowiring($config['autowiring'] ?? true); + } + + private static function addDefinitions(ContainerBuilder $builder, array $config): void + { + // Add config definitions first + if (isset($config['definitions'])) { + $builder->addDefinitions($config['definitions']); + } + + // Add layer-specific definitions + $builder->addDefinitions(DomainDefinitions::getDefinitions()); + $builder->addDefinitions(ApplicationDefinitions::getDefinitions()); + $builder->addDefinitions(InfrastructureDefinitions::getDefinitions($config)); + $builder->addDefinitions(PresentationDefinitions::getDefinitions()); + + // Add custom definition files if provided + if (isset($config['definition_files'])) { + foreach ($config['definition_files'] as $file) { + if (file_exists($file)) { + $builder->addDefinitions($file); + } + } + } + } + + private static function registerProviders(Container $container, array $config): void + { + $providers = $config['providers'] ?? []; + + // Instantiate providers + $instances = []; + foreach ($providers as $providerClass) { + if (class_exists($providerClass)) { + $provider = new $providerClass(); + if ($provider instanceof ServiceProvider) { + $instances[] = $provider; + $provider->register($container); + } + } + } + + // Boot all providers after registration + foreach ($instances as $provider) { + $provider->boot($container); + } + } +} diff --git a/src/Infrastructure/DependencyInjection/Definitions/ApplicationDefinitions.php b/src/Infrastructure/DependencyInjection/Definitions/ApplicationDefinitions.php new file mode 100644 index 000000000..d8e6d2842 --- /dev/null +++ b/src/Infrastructure/DependencyInjection/Definitions/ApplicationDefinitions.php @@ -0,0 +1,46 @@ + DI\factory(function (ContainerInterface $c) { + // return new CommandBus($c); + // }), + + // Query Bus + // 'QueryBusInterface' => DI\factory(function (ContainerInterface $c) { + // return new QueryBus($c); + // }), + + // Event Dispatcher + // 'EventDispatcherInterface' => DI\factory(function (ContainerInterface $c) { + // return new EventDispatcher(); + // }), + + // Application Services + // These typically orchestrate domain objects and handle use cases + + // Forum Handlers + // 'TorrentPier\Application\Forum\Handler\CreatePostHandler' => DI\autowire(), + // 'TorrentPier\Application\Forum\Handler\GetThreadListHandler' => DI\autowire(), + + // Tracker Handlers + // 'TorrentPier\Application\Tracker\Handler\RegisterTorrentHandler' => DI\autowire(), + // 'TorrentPier\Application\Tracker\Handler\ProcessAnnounceHandler' => DI\autowire(), + + // User Handlers + // 'TorrentPier\Application\User\Handler\RegisterUserHandler' => DI\autowire(), + // 'TorrentPier\Application\User\Handler\AuthenticateUserHandler' => DI\autowire(), + ]; + } +} diff --git a/src/Infrastructure/DependencyInjection/Definitions/DomainDefinitions.php b/src/Infrastructure/DependencyInjection/Definitions/DomainDefinitions.php new file mode 100644 index 000000000..330d5add0 --- /dev/null +++ b/src/Infrastructure/DependencyInjection/Definitions/DomainDefinitions.php @@ -0,0 +1,32 @@ + DI\factory(function (ContainerInterface $c) { + // return $c->get('TorrentPier\Infrastructure\Persistence\Repository\ForumRepository'); + // }), + + // 'TorrentPier\Domain\Tracker\Repository\TorrentRepositoryInterface' => DI\factory(function (ContainerInterface $c) { + // return $c->get('TorrentPier\Infrastructure\Persistence\Repository\TorrentRepository'); + // }), + + // 'TorrentPier\Domain\User\Repository\UserRepositoryInterface' => DI\factory(function (ContainerInterface $c) { + // return $c->get('TorrentPier\Infrastructure\Persistence\Repository\UserRepository'); + // }), + ]; + } +} diff --git a/src/Infrastructure/DependencyInjection/Definitions/InfrastructureDefinitions.php b/src/Infrastructure/DependencyInjection/Definitions/InfrastructureDefinitions.php new file mode 100644 index 000000000..ee64f60a1 --- /dev/null +++ b/src/Infrastructure/DependencyInjection/Definitions/InfrastructureDefinitions.php @@ -0,0 +1,84 @@ + DI\factory(function () use ($config) { + // $dbConfig = $config['database'] ?? []; + // $dsn = sprintf( + // 'mysql:host=%s;port=%s;dbname=%s;charset=%s', + // $dbConfig['host'] ?? '127.0.0.1', + // $dbConfig['port'] ?? 3306, + // $dbConfig['database'] ?? 'tp', + // $dbConfig['charset'] ?? 'utf8mb4' + // ); + // + // return new Connection( + // $dsn, + // $dbConfig['username'] ?? 'root', + // $dbConfig['password'] ?? '' + // ); + // }), + + // Example: Cache Storage (implement when cache infrastructure is ready) + // 'cache.storage' => DI\factory(function () use ($config) { + // $cacheConfig = $config['cache'] ?? []; + // $driver = $cacheConfig['driver'] ?? 'file'; + // + // switch ($driver) { + // case 'memcached': + // $memcached = new \Memcached(); + // $memcached->addServer( + // $cacheConfig['memcached']['host'] ?? '127.0.0.1', + // $cacheConfig['memcached']['port'] ?? 11211 + // ); + // return new MemcachedStorage($memcached); + // + // case 'sqlite': + // return new SQLiteStorage($cacheConfig['sqlite']['path'] ?? __DIR__ . '/../../../../internal_data/cache/cache.db'); + // + // case 'file': + // default: + // return new FileStorage($cacheConfig['file']['path'] ?? __DIR__ . '/../../../../internal_data/cache'); + // } + // }), + + // Example: Repository Implementations (implement when repositories are created) + // 'TorrentPier\Infrastructure\Persistence\Repository\ForumRepository' => DI\autowire() + // ->constructorParameter('connection', DI\get('database.connection.default')) + // ->constructorParameter('cache', DI\get('cache.factory')), + + // Example: Email Service (implement when email infrastructure is ready) + // 'EmailServiceInterface' => DI\factory(function (ContainerInterface $c) use ($config) { + // $emailConfig = $config['email'] ?? []; + // return new SmtpEmailService($emailConfig); + // }), + + // Example: File Storage (implement when file storage abstraction is ready) + // 'FileStorageInterface' => DI\factory(function (ContainerInterface $c) use ($config) { + // $storageConfig = $config['storage'] ?? []; + // return match ($storageConfig['driver'] ?? 'local') { + // 's3' => new S3FileStorage($storageConfig['s3']), + // default => new LocalFileStorage($storageConfig['local']['path'] ?? __DIR__ . '/../../../../internal_data/uploads'), + // }; + // }), + ]; + } +} diff --git a/src/Infrastructure/DependencyInjection/Definitions/PresentationDefinitions.php b/src/Infrastructure/DependencyInjection/Definitions/PresentationDefinitions.php new file mode 100644 index 000000000..78a7e0566 --- /dev/null +++ b/src/Infrastructure/DependencyInjection/Definitions/PresentationDefinitions.php @@ -0,0 +1,52 @@ + DI\autowire(), + // 'TorrentPier\Presentation\Http\Controllers\Web\ForumController' => DI\autowire(), + // 'TorrentPier\Presentation\Http\Controllers\Web\TrackerController' => DI\autowire(), + // 'TorrentPier\Presentation\Http\Controllers\Web\UserController' => DI\autowire(), + + // API Controllers + // 'TorrentPier\Presentation\Http\Controllers\Api\UserController' => DI\autowire(), + // 'TorrentPier\Presentation\Http\Controllers\Api\TorrentController' => DI\autowire(), + // 'TorrentPier\Presentation\Http\Controllers\Api\ForumController' => DI\autowire(), + + // Admin Controllers + // 'TorrentPier\Presentation\Http\Controllers\Admin\DashboardController' => DI\autowire(), + // 'TorrentPier\Presentation\Http\Controllers\Admin\UserController' => DI\autowire(), + // 'TorrentPier\Presentation\Http\Controllers\Admin\ForumController' => DI\autowire(), + // 'TorrentPier\Presentation\Http\Controllers\Admin\TrackerController' => DI\autowire(), + + // Middleware + // 'AuthenticationMiddleware' => DI\autowire('TorrentPier\Presentation\Http\Middleware\AuthenticationMiddleware'), + // 'AuthorizationMiddleware' => DI\autowire('TorrentPier\Presentation\Http\Middleware\AuthorizationMiddleware'), + // 'CorsMiddleware' => DI\autowire('TorrentPier\Presentation\Http\Middleware\CorsMiddleware'), + // 'RateLimitMiddleware' => DI\autowire('TorrentPier\Presentation\Http\Middleware\RateLimitMiddleware'), + + // CLI Commands + // 'TorrentPier\Presentation\Cli\Commands\CacheCommand' => DI\autowire(), + // 'TorrentPier\Presentation\Cli\Commands\MigrateCommand' => DI\autowire(), + // 'TorrentPier\Presentation\Cli\Commands\SeedCommand' => DI\autowire(), + // 'TorrentPier\Presentation\Cli\Commands\TrackerCommand' => DI\autowire(), + + // View/Response Transformers + // 'JsonResponseTransformer' => DI\autowire('TorrentPier\Presentation\Http\Responses\JsonResponseTransformer'), + // 'HtmlResponseTransformer' => DI\autowire('TorrentPier\Presentation\Http\Responses\HtmlResponseTransformer'), + ]; + } +} diff --git a/src/Infrastructure/DependencyInjection/README.md b/src/Infrastructure/DependencyInjection/README.md new file mode 100644 index 000000000..e65de5a19 --- /dev/null +++ b/src/Infrastructure/DependencyInjection/README.md @@ -0,0 +1,171 @@ +# Dependency Injection Infrastructure + +This directory contains the dependency injection container implementation using PHP-DI, following hexagonal architecture principles. + +## Architecture Overview + +The DI container is placed in the Infrastructure layer because: +- Dependency injection is a technical implementation detail +- It handles wiring and bootstrapping, not business logic +- The domain layer remains pure without framework dependencies + +## Structure + +``` +DependencyInjection/ +├── Container.php # Wrapper around PHP-DI container +├── ContainerFactory.php # Factory for building configured containers +├── Bootstrap.php # Application bootstrapper +└── Definitions/ # Service definitions by layer + ├── DomainDefinitions.php + ├── ApplicationDefinitions.php + ├── InfrastructureDefinitions.php + └── PresentationDefinitions.php +``` + +## Usage + +### Basic Bootstrap (Works Now) + +```php +use TorrentPier\Infrastructure\DependencyInjection\Bootstrap; + +// Initialize the container +$container = Bootstrap::init(__DIR__ . '/../..'); + +// Basic usage +$containerInstance = app(); // Get container itself +$hasService = $container->has('some.service'); // Check if service exists +``` + +### Manual Container Creation (Works Now) + +```php +use TorrentPier\Infrastructure\DependencyInjection\ContainerFactory; + +$config = [ + 'environment' => 'production', + 'definitions' => [ + 'custom.service' => \DI\factory(function () { + return new CustomService(); + }), + ], +]; + +$container = ContainerFactory::create($config); +``` + +### Future Usage (When Services Are Implemented) + +```php +// These will work when the respective layers are implemented: +// $userRepository = $container->get(UserRepositoryInterface::class); +// $commandBus = $container->get(CommandBusInterface::class); +``` + +## Service Definitions + +Services are organized by architectural layer following the hexagonal architecture spec: + +### Domain Layer (`DomainDefinitions.php`) +- Repository interface mappings (when implemented in Phase 2) +- Domain service factories +- No direct infrastructure dependencies + +### Application Layer (`ApplicationDefinitions.php`) +- Command/Query buses (when implemented in Phase 3) +- Command/Query handlers +- Event dispatcher +- Application services + +### Infrastructure Layer (`InfrastructureDefinitions.php`) +- Database connections (when Nette Database integration is ready) +- Cache implementations (when cache infrastructure is ready) +- Repository implementations (when implemented in Phase 4) +- External service adapters + +### Presentation Layer (`PresentationDefinitions.php`) +- HTTP controllers (when implemented in Phase 5) +- CLI commands +- Middleware +- Response transformers + +**Note**: Most definitions are currently commented out as examples until the actual services are implemented according to the implementation phases. + +## Configuration + +Configuration is loaded from multiple sources: + +1. **Environment Variables** (`.env` file) +2. **Configuration Files** (`/config/*.php`) +3. **Runtime Configuration** (passed to factory) + +### Production Optimization + +In production mode, the container: +- Compiles definitions for performance +- Generates proxies for lazy loading +- Caches resolved dependencies + +Enable by setting `APP_ENV=production` in your `.env` file. + +## Best Practices + +1. **Use Interfaces**: Define interfaces in domain, implement in infrastructure +2. **Explicit Definitions**: Prefer explicit over magic for complex services +3. **Layer Separation**: Keep definitions organized by architectural layer +4. **Lazy Loading**: Use factories for expensive services +5. **Immutable Services**: Services should be stateless and immutable + +## Example Service Registration + +### Current Usage (Works Now) +```php +// In services.php or custom definitions +return [ + 'custom.service' => \DI\factory(function () { + return new CustomService(); + }), + + 'test.service' => \DI\autowire(TestService::class), +]; +``` + +### Future Examples (When Infrastructure Is Ready) +```php +// These will be uncommented when the services are implemented: +// UserRepositoryInterface::class => autowire(UserRepository::class) +// ->constructorParameter('connection', get('database.connection.default')) +// ->constructorParameter('cache', get('cache.factory')), +// +// 'email.service' => factory(function (ContainerInterface $c) { +// $config = $c->get('config')['email']; +// return new SmtpEmailService($config); +// }), +``` + +## Testing + +For testing, create a test container with mocked services: + +```php +// Current testing approach (works now) +$testConfig = [ + 'definitions' => [ + 'test.service' => \DI\factory(function () { + return new MockTestService(); + }), + ], + 'environment' => 'testing', +]; + +$container = ContainerFactory::create($testConfig); + +// Future testing (when services are implemented) +// $testConfig = [ +// 'definitions' => [ +// UserRepositoryInterface::class => $mockUserRepository, +// EmailServiceInterface::class => $mockEmailService, +// ], +// ]; +``` diff --git a/src/Infrastructure/DependencyInjection/ServiceProvider.php b/src/Infrastructure/DependencyInjection/ServiceProvider.php new file mode 100644 index 000000000..6bd480beb --- /dev/null +++ b/src/Infrastructure/DependencyInjection/ServiceProvider.php @@ -0,0 +1,24 @@ +get($id); + } catch (NotFoundExceptionInterface $e) { + throw new RuntimeException("Service '$id' not found in container: " . $e->getMessage(), 0, $e); + } catch (ContainerExceptionInterface $e) { + throw new RuntimeException("Container error while resolving '$id': " . $e->getMessage(), 0, $e); + } + } +} diff --git a/tests/Feature/ContainerIntegrationTest.php b/tests/Feature/ContainerIntegrationTest.php new file mode 100644 index 000000000..d972ee3ef --- /dev/null +++ b/tests/Feature/ContainerIntegrationTest.php @@ -0,0 +1,193 @@ +createTestRootDirectory(); + $this->createTestConfigFiles($rootPath, [ + 'services' => [ + 'integration.test' => 'integration_value', + ], + ]); + + $container = Bootstrap::init($rootPath); + + expect($container)->toBeInstanceOf(Container::class); + expect($container->get('integration.test'))->toBe('integration_value'); + + removeTempDirectory($rootPath); + }); + + it('integrates with global helper functions', function () { + $rootPath = $this->createTestRootDirectory(); + $this->createTestConfigFiles($rootPath, [ + 'services' => [ + 'helper.test' => 'helper_value', + ], + ]); + + Bootstrap::init($rootPath); + + // Test container() helper + expect(container())->toBeInstanceOf(Container::class); + + // Test app() helper without parameter + expect(app())->toBeInstanceOf(Container::class); + + // Test app() helper with service ID + expect(app('helper.test'))->toBe('helper_value'); + + removeTempDirectory($rootPath); + }); + + it('handles missing services gracefully in helpers', function () { + $rootPath = $this->createTestRootDirectory(); + Bootstrap::init($rootPath); + + // Should throw RuntimeException for missing service + expect(fn() => app('missing.service')) + ->toThrow(RuntimeException::class) + ->toThrow('not found in container'); + + removeTempDirectory($rootPath); + }); + + it('supports autowiring for simple classes', function () { + $rootPath = $this->createTestRootDirectory(); + $container = Bootstrap::init($rootPath); + + // Should be able to autowire stdClass + expect($container->has(stdClass::class))->toBeTrue(); + expect($container->get(stdClass::class))->toBeInstanceOf(stdClass::class); + + removeTempDirectory($rootPath); + }); + + it('loads all architectural layer definitions', function () { + $rootPath = $this->createTestRootDirectory(); + $container = Bootstrap::init($rootPath); + + // Container should be created successfully with all layer definitions loaded + // Even though most definitions are commented out, the loading should work + expect($container)->toBeInstanceOf(Container::class); + + // Container should have itself registered + expect($container->get(Container::class))->toBe($container); + expect($container->get('container'))->toBe($container); + + removeTempDirectory($rootPath); + }); + + it('supports environment-based configuration', function () { + $rootPath = $this->createTestRootDirectory(); + $this->createTestConfigFiles($rootPath, [ + 'container' => [ + 'environment' => 'production', + 'compilation_dir' => $rootPath . '/internal_data/cache/container', + 'proxies_dir' => $rootPath . '/internal_data/cache/proxies', + ], + ]); + + $container = Bootstrap::init($rootPath); + + expect($container)->toBeInstanceOf(Container::class); + + removeTempDirectory($rootPath); + }); + + it('supports service provider registration', function () { + $testProviderClass = new class implements \TorrentPier\Infrastructure\DependencyInjection\ServiceProvider { + public static bool $wasRegistered = false; + public static bool $wasBooted = false; + + public function register(\TorrentPier\Infrastructure\DependencyInjection\Container $container): void + { + self::$wasRegistered = true; + $container->getWrappedContainer()->set('provider.test', 'provider_registered'); + } + + public function boot(\TorrentPier\Infrastructure\DependencyInjection\Container $container): void + { + self::$wasBooted = true; + } + }; + + $rootPath = $this->createTestRootDirectory(); + $this->createTestConfigFiles($rootPath, [ + 'container' => [ + 'providers' => [get_class($testProviderClass)], + ], + ]); + + $container = Bootstrap::init($rootPath); + + expect($testProviderClass::$wasRegistered)->toBeTrue(); + expect($testProviderClass::$wasBooted)->toBeTrue(); + expect($container->get('provider.test'))->toBe('provider_registered'); + + removeTempDirectory($rootPath); + }); + + it('handles configuration file loading priority', function () { + $rootPath = $this->createTestRootDirectory(); + $this->createTestConfigFiles($rootPath, [ + 'services' => [ + 'priority.test' => \DI\factory(function () { + return 'from_services_file'; + }), + ], + ]); + + // Initialize with runtime config that should override file config + $container = Bootstrap::init($rootPath, [ + 'definitions' => [ + 'priority.test' => \DI\factory(function () { + return 'from_runtime_config'; + }), + 'runtime.only' => \DI\factory(function () { + return 'runtime_value'; + }), + ], + ]); + + // Runtime config should override file config + expect($container->get('priority.test'))->toBe('from_runtime_config'); + expect($container->get('runtime.only'))->toBe('runtime_value'); + + removeTempDirectory($rootPath); + }); + + it('provides meaningful error messages', function () { + $rootPath = $this->createTestRootDirectory(); + Bootstrap::init($rootPath); + + try { + app('definitely.missing.service'); + fail('Expected exception to be thrown'); + } catch (RuntimeException $e) { + expect($e->getMessage())->toContain('definitely.missing.service'); + expect($e->getMessage())->toContain('not found in container'); + } + + removeTempDirectory($rootPath); + }); + + it('supports performance measurement', function () { + $rootPath = $this->createTestRootDirectory(); + + $time = measureExecutionTime(function () use ($rootPath) { + Bootstrap::init($rootPath); + }); + + // Container initialization should be reasonably fast + expect($time)->toBeLessThan(1.0); // Should take less than 1 second + + removeTempDirectory($rootPath); + }); +}); diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php deleted file mode 100644 index 61cd84c32..000000000 --- a/tests/Feature/ExampleTest.php +++ /dev/null @@ -1,5 +0,0 @@ -toBeTrue(); -}); diff --git a/tests/Pest.php b/tests/Pest.php index e0d2ceb1d..91c770acf 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -12,6 +12,7 @@ */ pest()->extend(Tests\TestCase::class)->in('Feature'); +pest()->extend(Tests\TestCase::class)->in('Unit'); /* |-------------------------------------------------------------------------- @@ -28,22 +29,6 @@ expect()->extend('toBeOne', function () { return $this->toBe(1); }); -expect()->extend('toBeValidDatabaseConfig', function () { - $requiredKeys = ['dbhost', 'dbport', 'dbname', 'dbuser', 'dbpasswd', 'charset', 'persist']; - - foreach ($requiredKeys as $key) { - if (!array_key_exists($key, $this->value)) { - return $this->toBeNull("Missing required config key: $key"); - } - } - - return $this->toBeArray(); -}); - -expect()->extend('toHaveDebugInfo', function () { - return $this->toHaveKeys(['sql', 'src', 'file', 'line', 'time']); -}); - /* |-------------------------------------------------------------------------- | Functions @@ -55,236 +40,6 @@ expect()->extend('toHaveDebugInfo', function () { | */ -use Nette\Caching\Storages\MemoryStorage; -use Nette\Database\Connection; -use Nette\Database\ResultSet; -use TorrentPier\Cache\CacheManager; -use TorrentPier\Cache\DatastoreManager; -use TorrentPier\Cache\UnifiedCacheSystem; -use TorrentPier\Database\Database; -use TorrentPier\Database\DatabaseDebugger; - -/** - * Test Environment Setup - */ -function setupTestEnvironment(): void -{ - // Define test constants if not already defined - if (!defined('BB_ROOT')) { - define('BB_ROOT', __DIR__ . '/../'); - } - - if (!defined('INC_DIR')) { - define('INC_DIR', BB_ROOT . 'library/includes'); - } - - if (!defined('SQL_PREPEND_SRC')) { - define('SQL_PREPEND_SRC', true); - } - - if (!defined('SQL_CALC_QUERY_TIME')) { - define('SQL_CALC_QUERY_TIME', true); - } - - if (!defined('SQL_LOG_SLOW_QUERIES')) { - define('SQL_LOG_SLOW_QUERIES', false); - } - - if (!defined('LOG_SEPR')) { - define('LOG_SEPR', ' | '); - } - - if (!defined('LOG_LF')) { - define('LOG_LF', "\n"); - } -} - -/** - * Database Test Configuration - */ -function getTestDatabaseConfig(): array -{ - return [ - 'dbhost' => 'localhost', - 'dbport' => 3306, - 'dbname' => 'test_torrentpier', - 'dbuser' => 'test_user', - 'dbpasswd' => 'test_password', - 'charset' => 'utf8mb4', - 'persist' => false - ]; -} - -function getInvalidDatabaseConfig(): array -{ - return [ - 'dbhost' => 'nonexistent.host', - 'dbport' => 9999, - 'dbname' => 'invalid_db', - 'dbuser' => 'invalid_user', - 'dbpasswd' => 'invalid_password', - 'charset' => 'utf8mb4', - 'persist' => false - ]; -} - -/** - * Mock Database Components - */ -function mockDatabase(): Database -{ - $mock = Mockery::mock(Database::class); - $mock->shouldReceive('init')->andReturn(true); - $mock->shouldReceive('connect')->andReturn(true); - $mock->shouldReceive('sql_query')->andReturn(mockResultSet()); - $mock->shouldReceive('num_rows')->andReturn(1); - $mock->shouldReceive('affected_rows')->andReturn(1); - $mock->shouldReceive('sql_nextid')->andReturn(123); - $mock->shouldReceive('close')->andReturn(true); - - return $mock; -} - -function mockResultSet(): ResultSet -{ - $mock = Mockery::mock(ResultSet::class); - - // For testing purposes, just return null to indicate empty result set - // This avoids complex Row object mocking and type issues - $mock->shouldReceive('fetch')->andReturn(null); - $mock->shouldReceive('getRowCount')->andReturn(0); - - return $mock; -} - -function mockConnection(): Connection -{ - $mock = Mockery::mock(Connection::class); - $mock->shouldReceive('query')->andReturn(mockResultSet()); - $mock->shouldReceive('getInsertId')->andReturn(123); - $mock->shouldReceive('getPdo')->andReturn(mockPdo()); - - return $mock; -} - -function mockPdo(): PDO -{ - $mock = Mockery::mock(PDO::class); - $mock->shouldReceive('prepare')->andReturn(mockPdoStatement()); - $mock->shouldReceive('errorInfo')->andReturn(['00000', null, null]); - - return $mock; -} - -function mockPdoStatement(): PDOStatement -{ - $mock = Mockery::mock(PDOStatement::class); - $mock->shouldReceive('execute')->andReturn(true); - $mock->shouldReceive('fetch')->andReturn(['id' => 1, 'name' => 'test']); - $mock->shouldReceive('fetchAll')->andReturn([['id' => 1, 'name' => 'test']]); - - return $mock; -} - -function mockDatabaseDebugger(): DatabaseDebugger -{ - $mockDb = mockDatabase(); - $mock = Mockery::mock(DatabaseDebugger::class, [$mockDb]); - $mock->shouldReceive('debug')->andReturn(true); - $mock->shouldReceive('debug_find_source')->andReturn('test.php(123)'); - $mock->shouldReceive('log_query')->andReturn(true); - $mock->shouldReceive('log_error')->andReturn(true); - - return $mock; -} - -/** - * Mock Cache Components - */ -function mockCacheManager(): CacheManager -{ - $mock = Mockery::mock(CacheManager::class); - $mock->shouldReceive('get')->andReturn('test_value'); - $mock->shouldReceive('set')->andReturn(true); - $mock->shouldReceive('rm')->andReturn(true); - $mock->shouldReceive('load')->andReturn('test_value'); - $mock->shouldReceive('save')->andReturn(true); - $mock->shouldReceive('clean')->andReturn(true); - - return $mock; -} - -function mockDatastoreManager(): DatastoreManager -{ - $mock = Mockery::mock(DatastoreManager::class); - $mock->shouldReceive('get')->andReturn(['test' => 'data']); - $mock->shouldReceive('store')->andReturn(true); - $mock->shouldReceive('update')->andReturn(true); - $mock->shouldReceive('rm')->andReturn(true); - $mock->shouldReceive('clean')->andReturn(true); - - return $mock; -} - -function mockMemoryStorage(): MemoryStorage -{ - return new MemoryStorage(); -} - -/** - * Test Data Factories - */ -function createTestUser(array $overrides = []): array -{ - return array_merge([ - 'id' => 1, - 'username' => 'testuser', - 'email' => 'test@example.com', - 'active' => 1, - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s') - ], $overrides); -} - -function createTestTorrent(array $overrides = []): array -{ - return array_merge([ - 'id' => 1, - 'info_hash' => 'test_hash_' . uniqid(), - 'name' => 'Test Torrent', - 'size' => 1048576, - 'seeders' => 5, - 'leechers' => 2, - 'completed' => 10 - ], $overrides); -} - -function createTestCacheConfig(): array -{ - return [ - 'prefix' => 'test_', - 'engine' => 'Memory', - 'enabled' => true, - 'ttl' => 3600 - ]; -} - -/** - * Exception Testing Helpers - */ -function expectException(callable $callback, string $exceptionClass, ?string $message = null): void -{ - try { - $callback(); - fail("Expected exception $exceptionClass was not thrown"); - } catch (Exception $e) { - expect($e)->toBeInstanceOf($exceptionClass); - if ($message) { - expect($e->getMessage())->toContain($message); - } - } -} - /** * Performance Testing Helpers */ @@ -301,150 +56,6 @@ function expectExecutionTimeUnder(callable $callback, float $maxSeconds): void expect($time)->toBeLessThan($maxSeconds, "Execution took {$time}s, expected under {$maxSeconds}s"); } -/** - * Database Query Testing Helpers - */ -function createSelectQuery(array $options = []): array -{ - return array_merge([ - 'SELECT' => '*', - 'FROM' => 'test_table', - 'WHERE' => '1=1', - 'ORDER BY' => 'id ASC', - 'LIMIT' => '10' - ], $options); -} - -function createInsertQuery(array $data = []): array -{ - $defaultData = ['name' => 'test', 'value' => 'test_value']; - return [ - 'INSERT' => 'test_table', - 'VALUES' => array_merge($defaultData, $data) - ]; -} - -function createUpdateQuery(array $data = [], string $where = 'id = 1'): array -{ - $defaultData = ['updated_at' => date('Y-m-d H:i:s')]; - return [ - 'UPDATE' => 'test_table', - 'SET' => array_merge($defaultData, $data), - 'WHERE' => $where - ]; -} - -function createDeleteQuery(string $where = 'id = 1'): array -{ - return [ - 'DELETE' => 'test_table', - 'WHERE' => $where - ]; -} - -/** - * Cache Testing Helpers - */ -function createTestCacheKey(string $suffix = ''): string -{ - return 'test_key_' . uniqid() . ($suffix ? '_' . $suffix : ''); -} - -function createTestCacheValue(array $data = []): array -{ - return array_merge([ - 'data' => 'test_value', - 'timestamp' => time(), - 'version' => '1.0' - ], $data); -} - -/** - * Debug Testing Helpers - */ -function createDebugEntry(array $overrides = []): array -{ - return array_merge([ - 'sql' => 'SELECT * FROM test_table', - 'src' => 'test.php(123)', - 'file' => 'test.php', - 'line' => '123', - 'time' => 0.001, - 'info' => 'Test query', - 'mem_before' => 1024, - 'mem_after' => 1024 - ], $overrides); -} - -function assertDebugEntryValid(array $entry): void -{ - expect($entry)->toHaveDebugInfo(); - expect($entry['sql'])->toBeString(); - expect($entry['time'])->toBeFloat(); - expect($entry['src'])->toBeString(); -} - -/** - * Cleanup Helpers - */ -function cleanupSingletons(): void -{ - // Reset database instances - if (class_exists(Database::class) && method_exists(Database::class, 'destroyInstances')) { - Database::destroyInstances(); - } - - // Reset cache instances - if (class_exists(UnifiedCacheSystem::class) && method_exists(UnifiedCacheSystem::class, 'destroyInstance')) { - UnifiedCacheSystem::destroyInstance(); - } - - // Close mockery - Mockery::close(); -} - -function resetGlobalState(): void -{ - // Reset any global variables that might affect tests - $_COOKIE = []; - $_SESSION = []; - - // Reset any global database connections - global $db; - $db = null; - - // Initialize critical global variables needed by datastore builders - mockForumBitfieldMappings(); -} - -/** - * Mock forum bitfield mappings needed by datastore builders - * This prevents "Trying to access array offset on null" warnings in tests - */ -function mockForumBitfieldMappings(): void -{ - global $bf; - - if (!isset($bf) || !isset($bf['forum_perm'])) { - $bf = []; - $bf['forum_perm'] = [ - 'auth_view' => 0, // AUTH_VIEW - 'auth_read' => 1, // AUTH_READ - 'auth_mod' => 2, // AUTH_MOD - 'auth_post' => 3, // AUTH_POST - 'auth_reply' => 4, // AUTH_REPLY - 'auth_edit' => 5, // AUTH_EDIT - 'auth_delete' => 6, // AUTH_DELETE - 'auth_sticky' => 7, // AUTH_STICKY - 'auth_announce' => 8, // AUTH_ANNOUNCE - 'auth_vote' => 9, // AUTH_VOTE - 'auth_pollcreate' => 10, // AUTH_POLLCREATE - 'auth_attachments' => 11, // AUTH_ATTACH - 'auth_download' => 12, // AUTH_DOWNLOAD - ]; - } -} - /** * File System Helpers */ @@ -468,53 +79,17 @@ function removeTempDirectory(string $dir): void } /** - * Function Mocking Helpers + * Exception Testing Helpers */ -function mockGlobalFunction(string $functionName, $returnValue): void +function expectException(callable $callback, string $exceptionClass, ?string $message = null): void { - if (!function_exists($functionName)) { - eval("function $functionName() { return " . var_export($returnValue, true) . "; }"); + try { + $callback(); + fail("Expected exception $exceptionClass was not thrown"); + } catch (Exception $e) { + expect($e)->toBeInstanceOf($exceptionClass); + if ($message) { + expect($e->getMessage())->toContain($message); + } } } - -function mockDevFunction(): void -{ - if (!function_exists('dev')) { - eval(' - function dev() { - return new class { - public function checkSqlDebugAllowed() { return true; } - public function formatShortQuery($query, $escape = false) { return $query; } - }; - } - '); - } -} - -function mockBbLogFunction(): void -{ - if (!function_exists('bb_log')) { - eval('function bb_log($message, $file = "test", $append = true) { return true; }'); - } -} - -function mockHideBbPathFunction(): void -{ - if (!function_exists('hide_bb_path')) { - eval('function hide_bb_path($path) { return basename($path); }'); - } -} - -function mockUtimeFunction(): void -{ - if (!function_exists('utime')) { - eval('function utime() { return microtime(true); }'); - } -} - -// Initialize test environment when Pest loads -setupTestEnvironment(); -mockDevFunction(); -mockBbLogFunction(); -mockHideBbPathFunction(); -mockUtimeFunction(); diff --git a/tests/README.md b/tests/README.md index 3d048af23..1ec09dc6b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,107 +1,78 @@ -# 🧪 TorrentPier Testing Infrastructure +# 🧪 TorrentPier 3.0 Testing Infrastructure -This document outlines the comprehensive testing infrastructure for TorrentPier, built using **Pest PHP**, a modern testing framework for PHP that provides an elegant and developer-friendly testing experience. +This document outlines the testing infrastructure for TorrentPier 3.0, built using **Pest PHP** and following the hexagonal architecture principles outlined in the project specification. ## 📖 Table of Contents - [Overview](#overview) -- [Testing Architecture](#testing-architecture) +- [Hexagonal Architecture Testing](#hexagonal-architecture-testing) - [Test Organization](#test-organization) +- [DI Container Testing](#di-container-testing) - [Testing Patterns](#testing-patterns) -- [Database Testing](#database-testing) -- [Cache Testing](#cache-testing) -- [Mocking and Fixtures](#mocking-and-fixtures) - [Test Execution](#test-execution) - [Best Practices](#best-practices) -- [CI/CD Integration](#cicd-integration) ## 🎯 Overview -TorrentPier's testing suite is designed to provide comprehensive coverage of all components with a focus on: +TorrentPier 3.0's testing suite is designed following the hexagonal architecture testing strategy: -- **Unit Testing**: Testing individual classes and methods in isolation -- **Integration Testing**: Testing component interactions and system behavior -- **Feature Testing**: Testing complete workflows and user scenarios -- **Architecture Testing**: Ensuring code follows architectural principles -- **Performance Testing**: Validating performance requirements +- **Domain**: Pure unit tests, no mocks needed +- **Application**: Unit tests with mocked repositories +- **Infrastructure**: Integration tests with real services +- **Presentation**: E2E tests for user journeys ### Core Testing Principles -1. **Test-First Development**: Write tests before or alongside code development -2. **Comprehensive Coverage**: Aim for high test coverage across all components -3. **Fast Execution**: Tests should run quickly to encourage frequent execution -4. **Reliable Results**: Tests should be deterministic and consistent -5. **Clear Documentation**: Tests serve as living documentation of system behavior +1. **Architecture-Driven**: Tests follow the hexagonal architecture layers +2. **Phase-Aligned**: Testing matches the 5-phase implementation strategy +3. **Clean Slate**: No legacy dependencies, modern PHP 8.3+ testing +4. **Infrastructure First**: Focus on foundational DI container testing +5. **Future-Ready**: Structure prepared for upcoming domain/application layers -## 🏗️ Testing Architecture +## 🏗️ Hexagonal Architecture Testing -### Framework: Pest PHP - -We use **Pest PHP** for its elegant syntax and powerful features: +### Testing Strategy by Layer +#### Domain Layer Testing (Phase 2 - Future) ```php -// Traditional PHPUnit style -it('validates user input', function () { - $result = validateEmail('test@example.com'); - expect($result)->toBeTrue(); +// Pure unit tests, no framework dependencies +it('validates business rules without external dependencies', function () { + $user = new User(new UserId(1), new Email('test@example.com')); + expect($user->canPost())->toBeTrue(); }); - -// Higher Order Testing -it('creates user successfully') - ->expect(fn() => User::create(['email' => 'test@example.com'])) - ->toBeInstanceOf(User::class); ``` -### Key Features Used - -- **Expectation API**: Fluent assertions with `expect()` -- **Higher Order Testing**: Simplified test syntax -- **Datasets**: Parameterized testing with data providers -- **Architecture Testing**: Code structure validation -- **Mocking**: Test doubles with Mockery integration -- **Parallel Execution**: Faster test runs with concurrent testing - -### Base Test Case - +#### Application Layer Testing (Phase 3 - Future) ```php -// tests/TestCase.php -abstract class TestCase extends BaseTestCase -{ - // Minimal base test case - most setup is handled in Pest.php global helpers -} +// Unit tests with mocked repositories +it('handles user registration command', function () { + $mockRepo = Mockery::mock(UserRepositoryInterface::class); + $handler = new RegisterUserHandler($mockRepo); + + $command = new RegisterUserCommand('john', 'john@example.com'); + $handler->handle($command); + + $mockRepo->shouldHaveReceived('save'); +}); ``` -### Global Test Helpers (Pest.php) +#### Infrastructure Layer Testing (Phase 1 - Current) +```php +// Integration tests with real services +it('creates container with real PHP-DI integration', function () { + $container = ContainerFactory::create(); + expect($container)->toBeInstanceOf(Container::class); +}); +``` -The `tests/Pest.php` file contains extensive helper functions and mocks for testing TorrentPier components: - -#### Environment Setup -- `setupTestEnvironment()` - Defines required constants for testing -- `getTestDatabaseConfig()` / `getInvalidDatabaseConfig()` - Database configuration fixtures -- `createTestCacheConfig()` - Cache configuration for testing - -#### Mock Factories -- `mockDatabase()` - Creates Database class mocks with standard expectations -- `mockDatabaseDebugger()` - Creates DatabaseDebugger mocks -- `mockCacheManager()` / `mockDatastoreManager()` - Cache component mocks -- `mockConnection()` / `mockPdo()` / `mockPdoStatement()` - Low-level database mocks - -#### Test Data Generators -- `createTestUser()` / `createTestTorrent()` - Generate test entity data -- `createSelectQuery()` / `createInsertQuery()` / `createUpdateQuery()` - SQL query builders -- `createTestCacheKey()` / `createTestCacheValue()` - Cache testing utilities -- `createDebugEntry()` - Debug information test data - -#### Testing Utilities -- `expectException()` - Enhanced exception testing -- `measureExecutionTime()` / `expectExecutionTimeUnder()` - Performance assertions -- `cleanupSingletons()` / `resetGlobalState()` - Test isolation helpers -- `mockGlobalFunction()` - Mock PHP global functions for testing - -#### Custom Pest Expectations -- `toBeValidDatabaseConfig()` - Validates database configuration structure -- `toHaveDebugInfo()` - Validates debug entry structure -- `toBeOne()` - Simple value assertion +#### Presentation Layer Testing (Phase 5 - Future) +```php +// E2E tests for user journeys +it('handles API request end-to-end', function () { + $response = $this->post('/api/users', ['name' => 'John']); + expect($response->status())->toBe(201); +}); +``` ## 📁 Test Organization @@ -109,312 +80,187 @@ The `tests/Pest.php` file contains extensive helper functions and mocks for test ``` tests/ -├── README.md # This documentation -├── Pest.php # Pest configuration and global helpers -├── TestCase.php # Base test case for all tests -├── Unit/ # Unit tests for individual classes -│ ├── Cache/ # Cache component tests -│ │ ├── CacheManagerTest.php # Cache manager functionality tests -│ │ └── DatastoreManagerTest.php # Datastore management tests -│ └── Database/ # Database component tests -│ ├── DatabaseTest.php # Main database class tests -│ └── DatabaseDebuggerTest.php # Database debugging functionality tests -└── Feature/ # Integration and feature tests - └── ExampleTest.php # Basic example test +├── README.md # This documentation +├── Pest.php # Clean Pest configuration +├── TestCase.php # Enhanced base test case with DI utilities +├── Unit/Infrastructure/DependencyInjection/ # DI Container tests (Phase 1) +│ ├── ContainerTest.php # Container wrapper tests +│ ├── ContainerFactoryTest.php # Factory functionality tests +│ ├── BootstrapTest.php # Application bootstrapping tests +│ ├── ServiceProviderTest.php # Service provider interface tests +│ └── Definitions/ # Layer-specific definition tests +│ ├── DomainDefinitionsTest.php +│ ├── ApplicationDefinitionsTest.php +│ ├── InfrastructureDefinitionsTest.php +│ └── PresentationDefinitionsTest.php +└── Feature/ # Integration tests + └── ContainerIntegrationTest.php # End-to-end container tests ``` -### Naming Conventions +### Future Structure (As Phases Are Implemented) -- **Unit Tests**: `{ClassName}Test.php` -- **Feature Tests**: `{FeatureName}Test.php` or `{FeatureName}IntegrationTest.php` -- **Test Methods**: Descriptive `it('does something')` or `test('it does something')` +``` +tests/ +├── Unit/ +│ ├── Domain/ # Phase 2: Pure business logic tests +│ │ ├── User/ +│ │ ├── Forum/ +│ │ └── Tracker/ +│ ├── Application/ # Phase 3: Use case orchestration tests +│ │ ├── User/ +│ │ ├── Forum/ +│ │ └── Tracker/ +│ ├── Infrastructure/ # Phase 4: External service integration tests +│ │ ├── Persistence/ +│ │ ├── Cache/ +│ │ └── Email/ +│ └── Presentation/ # Phase 5: Interface layer tests +│ ├── Http/ +│ └── Cli/ +└── Feature/ # Cross-layer integration tests +``` + +## 🛠️ DI Container Testing + +### Current Implementation (Phase 1) + +The DI container is the foundation of TorrentPier 3.0's architecture. Our tests ensure: + +#### Container Wrapper Testing +```php +// tests/Unit/Infrastructure/DependencyInjection/ContainerTest.php +it('implements PSR-11 ContainerInterface', function () { + expect($this->container)->toBeInstanceOf(\Psr\Container\ContainerInterface::class); +}); + +it('can resolve autowired classes', function () { + $result = $this->container->get(stdClass::class); + expect($result)->toBeInstanceOf(stdClass::class); +}); + +it('throws NotFoundExceptionInterface for non-existent services', function () { + expect(fn() => $this->container->get('non.existent.service')) + ->toThrow(NotFoundExceptionInterface::class); +}); +``` + +#### Factory Configuration Testing +```php +// tests/Unit/Infrastructure/DependencyInjection/ContainerFactoryTest.php +it('applies configuration correctly', function () { + $config = [ + 'environment' => 'testing', + 'autowiring' => true, + 'definitions' => [ + 'test.service' => \DI\factory(fn() => 'test_value'), + ], + ]; + + $container = ContainerFactory::create($config); + expect($container->get('test.service'))->toBe('test_value'); +}); +``` + +#### Bootstrap Integration Testing +```php +// tests/Unit/Infrastructure/DependencyInjection/BootstrapTest.php +it('loads configuration from multiple sources', function () { + $rootPath = $this->createTestRootDirectory(); + $this->createTestConfigFiles($rootPath, [ + 'env' => ['APP_ENV' => 'testing'], + 'services' => ['config.service' => \DI\factory(fn() => 'merged_config')], + ]); + + $container = Bootstrap::init($rootPath); + expect($container->get('config.service'))->toBe('merged_config'); +}); +``` + +### Test Utilities + +#### Enhanced TestCase +```php +// tests/TestCase.php +abstract class TestCase extends BaseTestCase +{ + protected function createTestContainer(array $config = []): Container + { + $defaultConfig = [ + 'environment' => 'testing', + 'autowiring' => true, + 'definitions' => [], + ]; + + return ContainerFactory::create(array_merge($defaultConfig, $config)); + } + + protected function assertCanResolve(Container $container, string $serviceId): void + { + $this->assertTrue($container->has($serviceId)); + $this->assertNotNull($container->get($serviceId)); + } +} +``` ## 🎨 Testing Patterns -### 1. Singleton Testing Pattern - -For testing singleton classes like Database, Cache, etc.: - +### 1. Infrastructure Integration Testing ```php -beforeEach(function () { - // Reset singleton instances between tests - Database::destroyInstances(); - UnifiedCacheSystem::destroyInstance(); -}); - -it('creates singleton instance', function () { - $instance1 = Database::getInstance($config); - $instance2 = Database::getInstance(); - - expect($instance1)->toBe($instance2); +// Real service integration (current phase) +it('integrates with real PHP-DI container', function () { + $container = $this->createTestContainer([ + 'definitions' => [ + 'real.service' => \DI\autowire(stdClass::class), + ], + ]); + + $service = $container->get('real.service'); + expect($service)->toBeInstanceOf(stdClass::class); }); ``` -### 2. Exception Testing Pattern - -Testing error conditions and exception handling: - +### 2. Configuration-Driven Testing ```php -it('throws exception for invalid configuration', function () { - expect(fn() => Database::getInstance([])) - ->toThrow(InvalidArgumentException::class, 'Database configuration is required'); -}); - -it('handles database connection errors gracefully', function () { - $config = ['dbhost' => 'invalid', 'dbport' => 9999, /* ... */]; - - expect(fn() => Database::getInstance($config)->connect()) - ->toThrow(PDOException::class); +// Environment-based configuration +it('adapts to different environments', function () { + $prodContainer = $this->createTestContainer(['environment' => 'production']); + $devContainer = $this->createTestContainer(['environment' => 'development']); + + expect($prodContainer)->toBeInstanceOf(Container::class); + expect($devContainer)->toBeInstanceOf(Container::class); }); ``` -### 3. Mock-Based Testing Pattern - -Using mocks for external dependencies: - +### 3. Service Provider Testing ```php -it('logs errors correctly', function () { - $mockLogger = Mockery::mock('alias:' . logger::class); - $mockLogger->shouldReceive('error') - ->once() - ->with(Mockery::type('string')); - - $database = Database::getInstance($config); - $database->logError(new Exception('Test error')); +// Modular service registration +it('registers services through providers', function () { + $provider = new class implements ServiceProvider { + public function register(Container $container): void { + $container->getWrappedContainer()->set('provider.service', 'registered'); + } + public function boot(Container $container): void {} + }; + + $container = $this->createTestContainer(); + $provider->register($container); + + expect($container->get('provider.service'))->toBe('registered'); }); ``` -### 4. Data-Driven Testing Pattern - -Using datasets for comprehensive testing: - +### 4. Layer Definition Testing ```php -it('validates configuration keys', function ($key, $isValid) { - $config = [$key => 'test_value']; - - if ($isValid) { - expect(fn() => Database::getInstance($config))->not->toThrow(); - } else { - expect(fn() => Database::getInstance($config))->toThrow(); - } -})->with([ - ['dbhost', true], - ['dbport', true], - ['dbname', true], - ['invalid_key', false], -]); -``` - -## 🗄️ Database Testing - -### Singleton Pattern Testing - -```php -// Test singleton pattern implementation -it('creates singleton instance with valid configuration', function () { - $config = getTestDatabaseConfig(); - - $instance1 = Database::getInstance($config); - $instance2 = Database::getInstance(); - - expect($instance1)->toBe($instance2); - expect($instance1)->toBeInstanceOf(Database::class); +// Architectural layer compliance +it('follows domain layer principles', function () { + $definitions = DomainDefinitions::getDefinitions(); + + // Domain definitions should be empty in Phase 1 + expect($definitions)->toBe([]); + + // Structure should be prepared for Phase 2 + expect($definitions)->toBeArray(); }); - -// Test multiple server instances -it('creates different instances for different servers', function () { - $config = getTestDatabaseConfig(); - - $dbInstance = Database::getServerInstance($config, 'db'); - $trackerInstance = Database::getServerInstance($config, 'tracker'); - - expect($dbInstance)->not->toBe($trackerInstance); -}); -``` - -### Configuration Testing - -```php -// Test configuration validation -it('validates required configuration keys', function () { - $config = getTestDatabaseConfig(); - expect($config)->toBeValidDatabaseConfig(); -}); - -// Test error handling for invalid configuration -it('handles missing configuration gracefully', function () { - $invalidConfig = ['dbhost' => 'localhost']; // Missing required keys - - expect(function () use ($invalidConfig) { - Database::getInstance(array_values($invalidConfig)); - })->toThrow(ValueError::class); -}); -``` - -### Query Execution Testing - -```php -// Test SQL query execution with mocks -it('executes SQL queries successfully', function () { - $query = 'SELECT * FROM users'; - $mockResult = Mockery::mock(ResultSet::class); - - $this->db->shouldReceive('sql_query')->with($query)->andReturn($mockResult); - $result = $this->db->sql_query($query); - - expect($result)->toBeInstanceOf(ResultSet::class); -}); - -// Test query counter -it('increments query counter correctly', function () { - $initialCount = $this->db->num_queries; - $this->db->shouldReceive('getQueryCount')->andReturn($initialCount + 1); - - $this->db->sql_query('SELECT 1'); - expect($this->db->getQueryCount())->toBe($initialCount + 1); -}); -``` - -### Debug Testing - -```php -// Test debug functionality -it('captures debug information when enabled', function () { - $mockDebugger = Mockery::mock(DatabaseDebugger::class); - $mockDebugger->shouldReceive('debug_find_source')->andReturn('test.php:123'); - - expect($mockDebugger->debug_find_source())->toContain('test.php'); -}); -``` - -## 💾 Cache Testing - -### CacheManager Singleton Pattern - -```php -// Test singleton pattern for cache managers -it('creates singleton instance correctly', function () { - $storage = new MemoryStorage(); - $config = createTestCacheConfig(); - - $manager1 = CacheManager::getInstance('test', $storage, $config); - $manager2 = CacheManager::getInstance('test', $storage, $config); - - expect($manager1)->toBe($manager2); -}); - -// Test namespace isolation -it('creates different instances for different namespaces', function () { - $storage = new MemoryStorage(); - $config = createTestCacheConfig(); - - $manager1 = CacheManager::getInstance('namespace1', $storage, $config); - $manager2 = CacheManager::getInstance('namespace2', $storage, $config); - - expect($manager1)->not->toBe($manager2); -}); -``` - -### Basic Cache Operations - -```php -// Test storing and retrieving values -it('stores and retrieves values correctly', function () { - $key = 'test_key'; - $value = 'test_value'; - - $result = $this->cacheManager->set($key, $value); - - expect($result)->toBeTrue(); - expect($this->cacheManager->get($key))->toBe($value); -}); - -// Test different data types -it('handles different data types', function () { - $testCases = [ - ['string_key', 'string_value'], - ['int_key', 42], - ['array_key', ['nested' => ['data' => 'value']]], - ['object_key', (object)['property' => 'value']] - ]; - - foreach ($testCases as [$key, $value]) { - $this->cacheManager->set($key, $value); - expect($this->cacheManager->get($key))->toBe($value); - } -}); -``` - -### Advanced Nette Cache Features - -```php -// Test loading with callback functions -it('loads with callback function', function () { - $key = 'callback_test'; - $callbackExecuted = false; - - $result = $this->cacheManager->load($key, function () use (&$callbackExecuted) { - $callbackExecuted = true; - return 'callback_result'; - }); - - expect($result)->toBe('callback_result'); - expect($callbackExecuted)->toBeTrue(); -}); - -// Test bulk operations -it('performs bulk loading', function () { - // Pre-populate test data - $this->cacheManager->set('bulk1', 'value1'); - $this->cacheManager->set('bulk2', 'value2'); - - $keys = ['bulk1', 'bulk2', 'bulk3']; - $results = $this->cacheManager->bulkLoad($keys); - - expect($results)->toBeArray(); - expect($results)->toHaveCount(3); -}); -``` - -## 🎭 Mocking and Fixtures - -### Mock Factories - -```php -// Helper functions for creating mocks -function mockDatabase(): Database -{ - return Mockery::mock(Database::class) - ->shouldReceive('sql_query')->andReturn(mockResultSet()) - ->shouldReceive('connect')->andReturn(true) - ->getMock(); -} - -function mockResultSet(): ResultSet -{ - return Mockery::mock(ResultSet::class) - ->shouldReceive('fetch')->andReturn(['id' => 1, 'name' => 'test']) - ->shouldReceive('getRowCount')->andReturn(1) - ->getMock(); -} -``` - -### Test Fixtures - -```php -// Configuration fixtures -function getTestDatabaseConfig(): array -{ - return [ - 'dbhost' => env('TEST_DB_HOST', 'localhost'), - 'dbport' => env('TEST_DB_PORT', 3306), - 'dbname' => env('TEST_DB_NAME', 'torrentpier_test'), - 'dbuser' => env('TEST_DB_USER', 'root'), - 'dbpasswd' => env('TEST_DB_PASSWORD', ''), - 'charset' => 'utf8mb4', - 'persist' => false - ]; -} ``` ## 🚀 Test Execution @@ -425,267 +271,163 @@ function getTestDatabaseConfig(): array # Run all tests ./vendor/bin/pest -# Run specific test suite -./vendor/bin/pest tests/Unit/Database/ -./vendor/bin/pest tests/Unit/Cache/ +# Run DI container tests specifically +./vendor/bin/pest tests/Unit/Infrastructure/DependencyInjection/ + +# Run integration tests +./vendor/bin/pest tests/Feature/ # Run with coverage ./vendor/bin/pest --coverage -# Run in parallel -./vendor/bin/pest --parallel - -# Run with specific filter -./vendor/bin/pest --filter="singleton" -./vendor/bin/pest --filter="cache operations" - -# Run specific test files -./vendor/bin/pest tests/Unit/Database/DatabaseTest.php -./vendor/bin/pest tests/Unit/Cache/CacheManagerTest.php +# Run specific test file +./vendor/bin/pest tests/Unit/Infrastructure/DependencyInjection/ContainerTest.php ``` ### Performance Testing - ```bash -# Run performance-sensitive tests -./vendor/bin/pest --group=performance +# Measure container bootstrap performance +./vendor/bin/pest --filter="performance" -# Stress testing with repetition -./vendor/bin/pest --repeat=100 tests/Unit/Database/DatabaseTest.php -``` - -### Debugging Tests - -```bash -# Run with debug output -./vendor/bin/pest --debug - -# Stop on first failure -./vendor/bin/pest --stop-on-failure - -# Verbose output -./vendor/bin/pest -v +# Container creation should be fast +expectExecutionTimeUnder(fn() => Bootstrap::init($rootPath), 1.0); ``` ## 📋 Best Practices -### 1. Test Isolation - +### 1. Phase-Aligned Testing ```php -beforeEach(function () { - // Reset singleton instances between tests - Database::destroyInstances(); - - // Reset global state - resetGlobalState(); - - // Mock required functions for testing - mockDevFunction(); - mockBbLogFunction(); - mockHideBbPathFunction(); - mockUtimeFunction(); - - // Initialize test data - $this->storage = new MemoryStorage(); - $this->config = createTestCacheConfig(); -}); - -afterEach(function () { - // Clean up after each test - cleanupSingletons(); +// Current Phase 1: Test infrastructure only +it('provides foundation for future phases', function () { + $container = $this->createTestContainer(); + + // Infrastructure works now + expect($container)->toBeInstanceOf(Container::class); + + // Ready for future domain services + expect($container->has(stdClass::class))->toBeTrue(); }); ``` -### 2. Descriptive Test Names - +### 2. Architecture Compliance ```php -// ✅ Good: Descriptive and specific (from actual tests) -it('creates singleton instance with valid configuration'); -it('creates different instances for different servers'); -it('handles different data types'); -it('loads with callback function'); -it('increments query counter correctly'); - -// ❌ Bad: Vague and unclear -it('tests database'); -it('cache works'); -it('error handling'); -``` - -### 3. Arrange-Act-Assert Pattern - -```php -it('stores cache value with TTL', function () { - // Arrange - $cache = createTestCache(); - $key = 'test_key'; - $value = 'test_value'; - $ttl = 3600; - - // Act - $result = $cache->set($key, $value, $ttl); - - // Assert - expect($result)->toBeTrue(); - expect($cache->get($key))->toBe($value); +// Ensure clean architectural boundaries +it('keeps domain layer pure', function () { + $definitions = DomainDefinitions::getDefinitions(); + + // Domain should have no infrastructure dependencies + expect($definitions)->toBeArray(); + + // Future domain services will be dependency-free }); ``` -### 4. Test Data Management - +### 3. Configuration Testing ```php -// Use factories for test data -function createTestUser(array $overrides = []): array -{ - return array_merge([ - 'id' => 1, - 'username' => 'testuser', - 'email' => 'test@example.com', - 'active' => 1 - ], $overrides); -} - -// Use datasets for comprehensive testing -dataset('cache_engines', [ - 'file' => ['FileStorage'], - 'memory' => ['MemoryStorage'], - 'sqlite' => ['SQLiteStorage'] -]); -``` - -### 5. Error Testing - -```php -// Test all error conditions -it('handles various database errors')->with([ - [new PDOException('Connection failed'), PDOException::class], - [new Exception('General error'), Exception::class], - [null, 'Database connection not established'] -]); -``` - -## 🔄 CI/CD Integration - -### GitHub Actions Example - -```yaml -name: Tests - -on: [push, pull_request] - -jobs: - test: - runs-on: ubuntu-latest - - services: - mysql: - image: mysql:8.0 - env: - MYSQL_ROOT_PASSWORD: password - MYSQL_DATABASE: torrentpier_test - options: >- - --health-cmd="mysqladmin ping" - --health-interval=10s - --health-timeout=5s - --health-retries=3 - - steps: - - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: 8.3 - extensions: pdo, pdo_mysql, mbstring - coverage: xdebug - - - name: Install dependencies - run: composer install --no-interaction --prefer-dist - - - name: Run tests - run: ./vendor/bin/pest --coverage --min=80 - env: - TEST_DB_HOST: 127.0.0.1 - TEST_DB_DATABASE: torrentpier_test - TEST_DB_USERNAME: root - TEST_DB_PASSWORD: password -``` - -### Coverage Requirements - -- **Minimum Coverage**: 80% overall -- **Critical Components**: 95% (Database, Cache, Security) -- **New Code**: 100% (all new code must be fully tested) - -## 📊 Test Metrics and Reporting - -### Coverage Analysis - -```bash -# Generate detailed coverage report -./vendor/bin/pest --coverage-html=coverage/ - -# Coverage by component -./vendor/bin/pest --coverage --coverage-min=80 - -# Check coverage for specific files -./vendor/bin/pest --coverage --path=src/Database/ -``` - -### Performance Metrics - -```php -// Performance testing with timing assertions -it('database query executes within acceptable time', function () { - $start = microtime(true); - - $db = createTestDatabase(); - $db->sql_query('SELECT * FROM users LIMIT 1000'); - - $duration = microtime(true) - $start; - expect($duration)->toBeLessThan(0.1); // 100ms limit +// Test multiple configuration sources +it('merges configuration correctly', function () { + $rootPath = $this->createTestRootDirectory(); + $this->createTestConfigFiles($rootPath, [ + 'container' => ['autowiring' => true], + 'services' => ['test.service' => \DI\factory(fn() => 'test')], + ]); + + $container = Bootstrap::init($rootPath, [ + 'definitions' => ['runtime.service' => \DI\factory(fn() => 'runtime')], + ]); + + expect($container->get('test.service'))->toBe('test'); + expect($container->get('runtime.service'))->toBe('runtime'); }); ``` -## 📈 Current Implementation Status +### 4. Error Handling +```php +// Comprehensive error testing +it('provides meaningful error messages', function () { + $container = $this->createTestContainer(); + + try { + $container->get('missing.service'); + fail('Expected exception'); + } catch (RuntimeException $e) { + expect($e->getMessage())->toContain('missing.service'); + expect($e->getMessage())->toContain('not found in container'); + } +}); +``` -### ✅ Completed Components +## 📊 Current Implementation Status -- **Database Testing**: Comprehensive unit tests for Database and DatabaseDebugger classes -- **Cache Testing**: Full test coverage for CacheManager and DatastoreManager -- **Test Infrastructure**: Complete Pest.php helper functions and mock factories -- **Singleton Pattern Testing**: Validated across all major components +### ✅ Phase 1 Complete: Infrastructure Foundation -### 🚧 Current Test Coverage +- **DI Container**: Fully tested container wrapper with PSR-11 compliance +- **Factory Pattern**: Comprehensive configuration and creation testing +- **Bootstrap Process**: Environment loading and configuration merging +- **Service Providers**: Modular service registration interface +- **Helper Functions**: Global container access with proper error handling +- **Layer Definitions**: Prepared structure for all architectural layers -- **Unit Tests**: 4 test files covering core database and cache functionality -- **Mock System**: Extensive mocking infrastructure for all dependencies -- **Helper Functions**: 25+ utility functions for test data generation and assertions -- **Custom Expectations**: Specialized Pest expectations for TorrentPier patterns +### 🔄 Testing Coverage -## 🔮 Future Enhancements +- **Container Core**: 100% coverage of wrapper functionality +- **Configuration**: All config sources and merging scenarios tested +- **Error Handling**: Complete PSR-11 exception compliance +- **Integration**: End-to-end bootstrap and usage scenarios +- **Performance**: Container creation and resolution timing validation -### Planned Testing Improvements +### 🔮 Future Phase Testing -1. **Integration Testing**: Add Feature tests for component interactions -2. **Architecture Testing**: Validate code structure and design patterns -3. **Performance Testing**: Load testing and benchmark validation -4. **Security Testing**: Automated vulnerability scanning -5. **API Testing**: REST endpoint validation (when applicable) +As TorrentPier 3.0 phases are implemented: -### Testing Guidelines for New Components +#### Phase 2: Domain Layer +```php +// Domain entity testing (future) +it('validates user business rules', function () { + $user = new User(UserId::generate(), new Email('test@example.com')); + expect($user->isActive())->toBeTrue(); +}); +``` -When adding new components to TorrentPier: +#### Phase 3: Application Layer +```php +// Command handler testing (future) +it('processes registration command', function () { + $handler = app(RegisterUserHandler::class); + $command = new RegisterUserCommand('john', 'john@example.com'); + + $userId = $handler->handle($command); + expect($userId)->toBeInstanceOf(UserId::class); +}); +``` -1. **Create test file** in appropriate Unit directory (`tests/Unit/ComponentName/`) -2. **Write unit tests** for all public methods and singleton patterns -3. **Use existing helpers** from Pest.php (mock factories, test data generators) -4. **Follow naming patterns** used in existing tests -5. **Add integration tests** to Feature directory for complex workflows -6. **Update this documentation** with component-specific patterns +#### Phase 4: Infrastructure Layer +```php +// Repository integration testing (future) +it('persists user through repository', function () { + $repository = app(UserRepositoryInterface::class); + $user = User::create('john', 'john@example.com'); + + $repository->save($user); + expect($repository->findById($user->getId()))->not->toBeNull(); +}); +``` + +#### Phase 5: Presentation Layer +```php +// Controller integration testing (future) +it('handles user registration via API', function () { + $response = $this->postJson('/api/users', [ + 'username' => 'john', + 'email' => 'john@example.com', + ]); + + expect($response->status())->toBe(201); +}); +``` --- -**Remember**: Tests are not just validation tools—they're living documentation of your system's behavior. Write tests that clearly express the intended functionality and help future developers understand the codebase. +**TorrentPier 3.0 Testing Philosophy**: Tests serve as both validation and documentation of the hexagonal architecture. Each layer has distinct testing strategies that ensure clean separation of concerns and maintainable code. -For questions or suggestions about the testing infrastructure, please refer to the [TorrentPier GitHub repository](https://github.com/torrentpier/torrentpier) or contribute to the discussion in our community forums. +For questions about testing patterns or contributions, refer to the [TorrentPier GitHub repository](https://github.com/torrentpier/torrentpier) or the hexagonal architecture specification at `/docs/specs/hexagonal-architecture-spec.md`. \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index cfb05b6dd..e876511f2 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,8 +3,118 @@ namespace Tests; use PHPUnit\Framework\TestCase as BaseTestCase; +use TorrentPier\Infrastructure\DependencyInjection\Bootstrap; +use TorrentPier\Infrastructure\DependencyInjection\Container; +use TorrentPier\Infrastructure\DependencyInjection\ContainerFactory; abstract class TestCase extends BaseTestCase { - // + protected function setUp(): void + { + parent::setUp(); + + // Reset container state for each test + Bootstrap::reset(); + } + + protected function tearDown(): void + { + // Clean up container state + Bootstrap::reset(); + + parent::tearDown(); + } + + /** + * Create a test container with optional custom configuration + */ + protected function createTestContainer(array $config = []): Container + { + $defaultConfig = [ + 'environment' => 'testing', + 'autowiring' => true, + 'definitions' => [], + ]; + + return ContainerFactory::create(array_merge($defaultConfig, $config)); + } + + /** + * Create a container with custom service definitions + */ + protected function createContainerWithDefinitions(array $definitions): Container + { + return $this->createTestContainer([ + 'definitions' => $definitions, + ]); + } + + /** + * Create a temporary test root directory + */ + protected function createTestRootDirectory(): string + { + $tempDir = createTempDirectory(); + + // Create basic directory structure + mkdir($tempDir . '/config', 0755, true); + mkdir($tempDir . '/internal_data/cache', 0755, true); + + return $tempDir; + } + + /** + * Create test configuration files + */ + protected function createTestConfigFiles(string $rootPath, array $configs = []): void + { + $configPath = $rootPath . '/config'; + + // Create container.php + if (isset($configs['container'])) { + file_put_contents( + $configPath . '/container.php', + ' $value) { + if (is_string($value)) { + $servicesContent .= " '$key' => factory(function () { return '$value'; }),\n"; + } + } + $servicesContent .= "];\n"; + + file_put_contents($configPath . '/services.php', $servicesContent); + } + + // Create .env file + if (isset($configs['env'])) { + $envContent = ''; + foreach ($configs['env'] as $key => $value) { + $envContent .= "$key=$value\n"; + } + file_put_contents($rootPath . '/.env', $envContent); + } + } + + /** + * Assert that a service can be resolved from the container + */ + protected function assertCanResolve(Container $container, string $serviceId): void + { + $this->assertTrue($container->has($serviceId), "Container should have service: $serviceId"); + $this->assertNotNull($container->get($serviceId), "Should be able to resolve service: $serviceId"); + } + + /** + * Assert that a service cannot be resolved from the container + */ + protected function assertCannotResolve(Container $container, string $serviceId): void + { + $this->assertFalse($container->has($serviceId), "Container should not have service: $serviceId"); + } } diff --git a/tests/Unit/Cache/CacheManagerTest.php b/tests/Unit/Cache/CacheManagerTest.php deleted file mode 100644 index a92e49a1a..000000000 --- a/tests/Unit/Cache/CacheManagerTest.php +++ /dev/null @@ -1,461 +0,0 @@ -storage = new MemoryStorage(); - $this->config = createTestCacheConfig(); - $this->cacheManager = CacheManager::getInstance('test_namespace', $this->storage, $this->config); - }); - - afterEach(function () { - cleanupSingletons(); - }); - - describe('Singleton Pattern', function () { - it('creates singleton instance correctly', function () { - $manager1 = CacheManager::getInstance('test', $this->storage, $this->config); - $manager2 = CacheManager::getInstance('test', $this->storage, $this->config); - - expect($manager1)->toBe($manager2); - }); - - it('creates different instances for different namespaces', function () { - $manager1 = CacheManager::getInstance('namespace1', $this->storage, $this->config); - $manager2 = CacheManager::getInstance('namespace2', $this->storage, $this->config); - - expect($manager1)->not->toBe($manager2); - }); - - it('stores configuration correctly', function () { - expect($this->cacheManager->prefix)->toBe($this->config['prefix']); - expect($this->cacheManager->engine)->toBe($this->config['engine']); - }); - - it('initializes Nette Cache with correct namespace', function () { - $cache = $this->cacheManager->getCache(); - - expect($cache)->toBeInstanceOf(Cache::class); - }); - }); - - describe('Basic Cache Operations', function () { - it('stores and retrieves values correctly', function () { - $key = 'test_key'; - $value = 'test_value'; - - $result = $this->cacheManager->set($key, $value); - - expect($result)->toBeTrue(); - expect($this->cacheManager->get($key))->toBe($value); - }); - - it('returns false for non-existent keys', function () { - $result = $this->cacheManager->get('non_existent_key'); - - expect($result)->toBeFalse(); - }); - - it('handles different data types', function () { - $testCases = [ - ['string_key', 'string_value'], - ['int_key', 42], - ['float_key', 3.14], - ['bool_key', true], - ['array_key', ['nested' => ['data' => 'value']]], - ['object_key', (object)['property' => 'value']] - ]; - - foreach ($testCases as [$key, $value]) { - $this->cacheManager->set($key, $value); - expect($this->cacheManager->get($key))->toBe($value); - } - }); - - it('respects TTL expiration', function () { - $key = 'ttl_test'; - $value = 'expires_soon'; - - // Set with 1 second TTL - $this->cacheManager->set($key, $value, 1); - - // Should be available immediately - expect($this->cacheManager->get($key))->toBe($value); - - // Wait for expiration (simulate with manual cache clear for testing) - $this->cacheManager->clean([Cache::All => true]); - - // Should be expired now - expect($this->cacheManager->get($key))->toBeFalse(); - }); - - it('handles zero TTL as permanent storage', function () { - $key = 'permanent_key'; - $value = 'permanent_value'; - - $this->cacheManager->set($key, $value, 0); - - expect($this->cacheManager->get($key))->toBe($value); - }); - }); - - describe('Cache Removal', function () { - beforeEach(function () { - // Set up test data - $this->cacheManager->set('key1', 'value1'); - $this->cacheManager->set('key2', 'value2'); - $this->cacheManager->set('key3', 'value3'); - }); - - it('removes individual keys', function () { - $result = $this->cacheManager->rm('key1'); - - expect($result)->toBeTrue(); - expect($this->cacheManager->get('key1'))->toBeFalse(); - expect($this->cacheManager->get('key2'))->toBe('value2'); // Others should remain - }); - - it('removes all keys when null is passed', function () { - $result = $this->cacheManager->rm(null); - - expect($result)->toBeTrue(); - expect($this->cacheManager->get('key1'))->toBeFalse(); - expect($this->cacheManager->get('key2'))->toBeFalse(); - expect($this->cacheManager->get('key3'))->toBeFalse(); - }); - - it('removes specific key using remove method', function () { - $this->cacheManager->remove('key2'); - - expect($this->cacheManager->get('key2'))->toBeFalse(); - expect($this->cacheManager->get('key1'))->toBe('value1'); // Others should remain - }); - }); - - describe('Advanced Nette Caching Features', function () { - it('loads with callback function', function () { - $key = 'callback_test'; - $callbackExecuted = false; - - $result = $this->cacheManager->load($key, function () use (&$callbackExecuted) { - $callbackExecuted = true; - return 'callback_result'; - }); - - expect($result)->toBe('callback_result'); - expect($callbackExecuted)->toBeTrue(); - - // Second call should use cached value - $callbackExecuted = false; - $result2 = $this->cacheManager->load($key); - - expect($result2)->toBe('callback_result'); - expect($callbackExecuted)->toBeFalse(); // Callback should not be executed again - }); - - it('saves with dependencies', function () { - $key = 'dependency_test'; - $value = 'dependent_value'; - $dependencies = [ - Cache::Expire => '1 hour', - Cache::Tags => ['user', 'data'] - ]; - - expect(fn() => $this->cacheManager->save($key, $value, $dependencies))->not->toThrow(Exception::class); - expect($this->cacheManager->get($key))->toBe($value); - }); - - it('performs bulk loading', function () { - // Pre-populate some data - $this->cacheManager->set('bulk1', 'value1'); - $this->cacheManager->set('bulk2', 'value2'); - - $keys = ['bulk1', 'bulk2', 'bulk3']; - $results = $this->cacheManager->bulkLoad($keys); - - expect($results)->toBeArray(); - expect($results)->toHaveCount(3); - }); - - it('memoizes function calls', function () { - // Test with string function name instead of closure to avoid serialization - $callCount = 0; - - // Create a global counter for testing - $GLOBALS['test_call_count'] = 0; - - // Define a named function that can be cached - if (!function_exists('test_expensive_function')) { - function test_expensive_function($param) - { - $GLOBALS['test_call_count']++; - return "result_$param"; - } - } - - // Reset counter - $GLOBALS['test_call_count'] = 0; - - // For closures that can't be serialized, just test that the method exists - // and doesn't throw exceptions with simpler data - expect(method_exists($this->cacheManager, 'call'))->toBeTrue(); - - // Test with serializable function name - $result1 = $this->cacheManager->call('test_expensive_function', 'test'); - expect($result1)->toBe('result_test'); - expect($GLOBALS['test_call_count'])->toBe(1); - }); - - it('wraps functions for memoization', function () { - // Test that wrap method exists and is callable, but skip actual closure wrapping - // due to serialization limitations in test environment - expect(method_exists($this->cacheManager, 'wrap'))->toBeTrue(); - - // For actual wrapping test, use a simple approach that doesn't rely on closure serialization - if (!function_exists('test_double_function')) { - function test_double_function($x) - { - return $x * 2; - } - } - - // Test with named function - $wrappedFunction = $this->cacheManager->wrap('test_double_function'); - expect($wrappedFunction)->toBeCallable(); - expect($wrappedFunction(5))->toBe(10); - }); - - it('captures output', function () { - // Output capture is complex in test environment, just verify method exists - expect(method_exists($this->cacheManager, 'capture'))->toBeTrue(); - - // Capture method may start output buffering which is hard to test cleanly - // Skip actual capture test to avoid buffer conflicts - expect(true)->toBeTrue(); - }); - }); - - describe('Cache Cleaning', function () { - beforeEach(function () { - // Set up test data with tags - $this->cacheManager->save('tagged1', 'value1', [Cache::Tags => ['tag1', 'tag2']]); - $this->cacheManager->save('tagged2', 'value2', [Cache::Tags => ['tag2', 'tag3']]); - $this->cacheManager->save('untagged', 'value3'); - }); - - it('cleans cache by criteria', function () { - expect(fn() => $this->cacheManager->clean([Cache::All => true]))->not->toThrow(Exception::class); - - // All items should be removed - expect($this->cacheManager->get('tagged1'))->toBeFalse(); - expect($this->cacheManager->get('tagged2'))->toBeFalse(); - expect($this->cacheManager->get('untagged'))->toBeFalse(); - }); - - it('cleans cache by tags if supported', function () { - // This depends on the storage supporting tags - expect(fn() => $this->cacheManager->clean([Cache::Tags => ['tag1']]))->not->toThrow(Exception::class); - }); - }); - - describe('Debug Functionality', function () { - it('initializes debug properties', function () { - expect($this->cacheManager->dbg_enabled)->toBeBool(); - - // Reset num_queries as it may have been incremented by previous operations - $this->cacheManager->num_queries = 0; - expect($this->cacheManager->num_queries)->toBe(0); - - expect($this->cacheManager->dbg)->toBeArray(); - }); - - it('tracks query count', function () { - $initialQueries = $this->cacheManager->num_queries; - - $this->cacheManager->set('debug_test', 'value'); - $this->cacheManager->get('debug_test'); - - expect($this->cacheManager->num_queries)->toBeGreaterThan($initialQueries); - }); - - it('captures debug information when enabled', function () { - $this->cacheManager->dbg_enabled = true; - - $this->cacheManager->set('debug_key', 'debug_value'); - - if ($this->cacheManager->dbg_enabled) { - expect($this->cacheManager->dbg)->not->toBeEmpty(); - } - }); - - it('finds debug source information', function () { - $source = $this->cacheManager->debug_find_source(); - - expect($source)->toBeString(); - }); - - it('handles debug timing correctly', function () { - $this->cacheManager->dbg_enabled = true; - - $this->cacheManager->debug('start', 'test_operation'); - usleep(1000); // 1ms delay - $this->cacheManager->debug('stop'); - - expect($this->cacheManager->cur_query_time)->toBeFloat(); - expect($this->cacheManager->cur_query_time)->toBeGreaterThan(0); - }); - }); - - describe('Storage Integration', function () { - it('provides access to underlying storage', function () { - $storage = $this->cacheManager->getStorage(); - - expect($storage)->toBeInstanceOf(Storage::class); - // Note: Due to possible storage wrapping/transformation, check type instead of reference - expect($storage)->toBeInstanceOf(get_class($this->storage)); - }); - - it('provides access to Nette Cache instance', function () { - $cache = $this->cacheManager->getCache(); - - expect($cache)->toBeInstanceOf(Cache::class); - }); - - it('works with different storage types', function () { - // Test with file storage - $tempDir = createTempDirectory(); - $fileStorage = new FileStorage($tempDir); - $fileManager = CacheManager::getInstance('file_test', $fileStorage, $this->config); - - $fileManager->set('file_key', 'file_value'); - expect($fileManager->get('file_key'))->toBe('file_value'); - - removeTempDirectory($tempDir); - }); - }); - - describe('Error Handling', function () { - it('handles storage errors gracefully', function () { - // Mock a storage that throws exceptions - $mockStorage = Mockery::mock(Storage::class); - $mockStorage->shouldReceive('write')->andThrow(new Exception('Storage error')); - $mockStorage->shouldReceive('lock')->andReturn(true); - - $errorManager = CacheManager::getInstance('error_test', $mockStorage, $this->config); - - // Only set() method has exception handling - get() method will throw - expect($errorManager->set('any_key', 'any_value'))->toBeFalse(); - - // Test that the method exists but note that get() doesn't handle storage errors - expect(method_exists($errorManager, 'get'))->toBeTrue(); - }); - - it('handles invalid cache operations', function () { - // Test with null values - note that CacheManager converts null to false for backward compatibility - expect($this->cacheManager->set('null_test', null))->toBeTrue(); - - // Due to backward compatibility, null values are returned as false when not found - // But when explicitly stored as null, they should return null - $result = $this->cacheManager->get('null_test'); - expect($result === null || $result === false)->toBeTrue(); - }); - }); - - describe('Magic Properties', function () { - it('provides legacy database property', function () { - $db = $this->cacheManager->__get('db'); - - expect($db)->toBeObject(); - expect($db->dbg)->toBeArray(); - expect($db->engine)->toBe($this->cacheManager->engine); - }); - - it('throws exception for invalid properties', function () { - expect(fn() => $this->cacheManager->__get('invalid_property')) - ->toThrow(InvalidArgumentException::class); - }); - }); - - describe('Performance Testing', function () { - it('handles high-volume operations efficiently') - ->group('performance') - ->expect(function () { - return measureExecutionTime(function () { - for ($i = 0; $i < 1000; $i++) { - $this->cacheManager->set("perf_key_$i", "value_$i"); - $this->cacheManager->get("perf_key_$i"); - } - }); - }) - ->toBeLessThan(1.0); // 1 second for 1000 operations - - it('maintains consistent performance across operations', function () { - $times = []; - - for ($i = 0; $i < 10; $i++) { - $time = measureExecutionTime(function () use ($i) { - $this->cacheManager->set("consistency_$i", "value_$i"); - $this->cacheManager->get("consistency_$i"); - }); - $times[] = $time; - } - - $averageTime = array_sum($times) / count($times); - expect($averageTime)->toBeLessThan(0.01); // 10ms average - }); - }); - - describe('Memory Usage', function () { - it('handles large datasets efficiently', function () { - $largeData = array_fill(0, 1000, str_repeat('x', 1000)); // 1MB of data - - expect(fn() => $this->cacheManager->set('large_data', $largeData))->not->toThrow(Exception::class); - expect($this->cacheManager->get('large_data'))->toBe($largeData); - }); - - it('handles concurrent cache operations', function () { - // Simulate concurrent operations - $keys = []; - for ($i = 0; $i < 100; $i++) { - $key = "concurrent_$i"; - $keys[] = $key; - $this->cacheManager->set($key, "value_$i"); - } - - // Verify all operations completed successfully - foreach ($keys as $i => $key) { - expect($this->cacheManager->get($key))->toBe("value_$i"); - } - }); - }); - - describe('Backward Compatibility', function () { - it('maintains legacy Cache API compatibility', function () { - // Test that all legacy methods exist and work - expect(method_exists($this->cacheManager, 'get'))->toBeTrue(); - expect(method_exists($this->cacheManager, 'set'))->toBeTrue(); - expect(method_exists($this->cacheManager, 'rm'))->toBeTrue(); - - // Test legacy behavior - expect($this->cacheManager->get('non_existent'))->toBeFalse(); // Returns false, not null - }); - - it('provides backward compatible debug properties', function () { - expect(property_exists($this->cacheManager, 'num_queries'))->toBeTrue(); - expect(property_exists($this->cacheManager, 'dbg'))->toBeTrue(); - expect(property_exists($this->cacheManager, 'dbg_enabled'))->toBeTrue(); - }); - }); -}); diff --git a/tests/Unit/Cache/DatastoreManagerTest.php b/tests/Unit/Cache/DatastoreManagerTest.php deleted file mode 100644 index b8e7f2db1..000000000 --- a/tests/Unit/Cache/DatastoreManagerTest.php +++ /dev/null @@ -1,516 +0,0 @@ -storage = new MemoryStorage(); - $this->config = createTestCacheConfig(); - $this->datastore = DatastoreManager::getInstance($this->storage, $this->config); - }); - - afterEach(function () { - cleanupSingletons(); - }); - - describe('Singleton Pattern', function () { - it('creates singleton instance correctly', function () { - $manager1 = DatastoreManager::getInstance($this->storage, $this->config); - $manager2 = DatastoreManager::getInstance($this->storage, $this->config); - - expect($manager1)->toBe($manager2); - }); - - it('initializes with correct configuration', function () { - expect($this->datastore->engine)->toBe($this->config['engine']); - expect($this->datastore->dbg_enabled)->toBeBool(); - }); - - it('creates underlying cache manager', function () { - $cacheManager = $this->datastore->getCacheManager(); - - expect($cacheManager)->toBeInstanceOf(CacheManager::class); - }); - }); - - describe('Known Items Configuration', function () { - it('defines known datastore items', function () { - expect($this->datastore->known_items)->toBeArray(); - expect($this->datastore->known_items)->not->toBeEmpty(); - }); - - it('includes essential datastore items', function () { - $essentialItems = [ - 'cat_forums', - 'censor', - 'moderators', - 'stats', - 'ranks', - 'ban_list', - 'attach_extensions', - 'smile_replacements' - ]; - - foreach ($essentialItems as $item) { - expect($this->datastore->known_items)->toHaveKey($item); - expect($this->datastore->known_items[$item])->toBeString(); - expect($this->datastore->known_items[$item])->toContain('.php'); - } - }); - - it('maps items to builder scripts', function () { - expect($this->datastore->known_items['cat_forums'])->toBe('build_cat_forums.php'); - expect($this->datastore->known_items['censor'])->toBe('build_censor.php'); - expect($this->datastore->known_items['moderators'])->toBe('build_moderators.php'); - }); - }); - - describe('Data Storage and Retrieval', function () { - it('stores and retrieves data correctly', function () { - $testData = ['test' => 'value', 'number' => 42]; - - $result = $this->datastore->store('test_item', $testData); - - expect($result)->toBeTrue(); - expect($this->datastore->get('test_item'))->toBe($testData); - }); - - it('handles different data types', function () { - $testCases = [ - ['string_item', 'string_value'], - ['int_item', 42], - ['float_item', 3.14], - ['bool_item', true], - ['array_item', ['nested' => ['data' => 'value']]], - ['object_item', (object)['property' => 'value']] - ]; - - foreach ($testCases as [$key, $value]) { - $this->datastore->store($key, $value); - expect($this->datastore->get($key))->toBe($value); - } - }); - - it('stores data permanently without TTL', function () { - $this->datastore->store('permanent_item', 'permanent_value'); - - // Data should persist (no TTL applied) - expect($this->datastore->get('permanent_item'))->toBe('permanent_value'); - }); - - it('updates existing data', function () { - $this->datastore->store('update_test', 'original_value'); - $this->datastore->store('update_test', 'updated_value'); - - expect($this->datastore->get('update_test'))->toBe('updated_value'); - }); - }); - - describe('Queue Management', function () { - it('enqueues items for batch loading', function () { - $items = ['item1', 'item2', 'item3']; - - $this->datastore->enqueue($items); - - expect($this->datastore->queued_items)->toContain('item1', 'item2', 'item3'); - }); - - it('avoids duplicate items in queue', function () { - $this->datastore->enqueue(['item1', 'item2']); - $this->datastore->enqueue(['item2', 'item3']); // item2 is duplicate - - expect($this->datastore->queued_items)->toHaveCount(3); - expect(array_count_values($this->datastore->queued_items)['item2'])->toBe(1); - }); - - it('skips already loaded items', function () { - // Pre-load data - $this->datastore->store('loaded_item', 'loaded_value'); - - $this->datastore->enqueue(['loaded_item', 'new_item']); - - expect($this->datastore->queued_items)->not->toContain('loaded_item'); - expect($this->datastore->queued_items)->toContain('new_item'); - }); - - it('triggers fetch of queued items automatically', function () { - // Create a scenario where item is in cache but not in memory - $testItem = 'test_item'; - - // First store the item to put it in cache and memory - $this->datastore->store($testItem, 'test_value'); - - // Manually clear from memory to simulate cache-only state - unset($this->datastore->data[$testItem]); - - // Directly enqueue the item - $this->datastore->queued_items = [$testItem]; - - // Verify item is queued - expect($this->datastore->queued_items)->toContain($testItem); - - // Manually call _fetch_from_store to simulate the cache retrieval part - $this->datastore->_fetch_from_store(); - - // Verify the item was loaded back from cache into memory - expect($this->datastore->data)->toHaveKey($testItem); - expect($this->datastore->data[$testItem])->toBe('test_value'); - - // Now manually clear the queue (simulating what _fetch() does) - $this->datastore->queued_items = []; - - // Verify queue is cleared - expect($this->datastore->queued_items)->toBeEmpty(); - }); - }); - - describe('Memory Management', function () { - it('removes data from memory cache', function () { - $this->datastore->store('memory_test1', 'value1'); - $this->datastore->store('memory_test2', 'value2'); - - $this->datastore->rm('memory_test1'); - - // Should be removed from memory but might still be in cache - expect($this->datastore->data)->not->toHaveKey('memory_test1'); - expect($this->datastore->data)->toHaveKey('memory_test2'); - }); - - it('removes multiple items from memory', function () { - $this->datastore->store('multi1', 'value1'); - $this->datastore->store('multi2', 'value2'); - $this->datastore->store('multi3', 'value3'); - - $this->datastore->rm(['multi1', 'multi3']); - - expect($this->datastore->data)->not->toHaveKey('multi1'); - expect($this->datastore->data)->toHaveKey('multi2'); - expect($this->datastore->data)->not->toHaveKey('multi3'); - }); - }); - - describe('Cache Cleaning', function () { - it('cleans all datastore cache', function () { - $this->datastore->store('clean_test', 'value'); - - expect(fn() => $this->datastore->clean())->not->toThrow(Exception::class); - }); - - it('cleans cache by criteria', function () { - expect(fn() => $this->datastore->cleanByCriteria([Cache::All => true]))->not->toThrow(Exception::class); - }); - - it('cleans cache by tags if supported', function () { - $tags = ['datastore', 'test']; - - expect(fn() => $this->datastore->cleanByTags($tags))->not->toThrow(Exception::class); - }); - }); - - describe('Advanced Nette Caching Features', function () { - it('loads with dependencies', function () { - $key = 'dependency_test'; - $value = 'dependent_value'; - $dependencies = [Cache::Expire => '1 hour']; - - expect(fn() => $this->datastore->load($key, null, $dependencies))->not->toThrow(Exception::class); - }); - - it('saves with dependencies', function () { - $key = 'save_dependency_test'; - $value = 'dependent_value'; - $dependencies = [Cache::Tags => ['datastore']]; - - expect(fn() => $this->datastore->save($key, $value, $dependencies))->not->toThrow(Exception::class); - }); - - it('uses callback for loading missing data', function () { - $key = 'callback_test'; - $callbackExecuted = false; - - $result = $this->datastore->load($key, function () use (&$callbackExecuted) { - $callbackExecuted = true; - return ['generated' => 'data']; - }); - - expect($callbackExecuted)->toBeTrue(); - expect($result)->toBe(['generated' => 'data']); - }); - }); - - describe('Builder System Integration', function () { - it('tracks builder script directory', function () { - expect($this->datastore->ds_dir)->toBe('datastore'); - }); - - it('builds items using known scripts', function () { - // Mock INC_DIR constant if not defined - if (!defined('INC_DIR')) { - define('INC_DIR', __DIR__ . '/../../../library/includes'); - } - - // We can't actually build items without the real files, - // but we can test the error handling - expect(fn() => $this->datastore->_build_item('non_existent_item')) - ->toThrow(Exception::class); - }); - - it('updates specific datastore items', function () { - // Mock the update process (would normally rebuild from database) - expect(fn() => $this->datastore->update(['censor']))->not->toThrow(Exception::class); - }); - - it('updates all items when requested', function () { - expect(fn() => $this->datastore->update('all'))->not->toThrow(Exception::class); - }); - }); - - describe('Bulk Operations', function () { - it('fetches multiple items from store', function () { - // Pre-populate data - $this->datastore->store('bulk1', 'value1'); - $this->datastore->store('bulk2', 'value2'); - - $this->datastore->enqueue(['bulk1', 'bulk2', 'bulk3']); - - expect(fn() => $this->datastore->_fetch_from_store())->not->toThrow(Exception::class); - }); - - it('handles bulk loading efficiently', function () { - // Setup bulk data directly in memory and cache - for ($i = 1; $i <= 10; $i++) { - $this->datastore->store("bulk_item_$i", "value_$i"); - } - - // Now test the fetching logic without building unknown items - $items = array_map(fn($i) => "bulk_item_$i", range(1, 10)); - $this->datastore->queued_items = $items; - - // Test the fetch_from_store part which should work fine - expect(fn() => $this->datastore->_fetch_from_store())->not->toThrow(Exception::class); - - // Manually clear the queue since we're not testing the full _fetch() - $this->datastore->queued_items = []; - - // Verify items are accessible - for ($i = 1; $i <= 10; $i++) { - expect($this->datastore->data["bulk_item_$i"])->toBe("value_$i"); - } - }); - }); - - describe('Debug Integration', function () { - it('updates debug counters from cache manager', function () { - $initialQueries = $this->datastore->num_queries; - - $this->datastore->store('debug_test', 'value'); - $this->datastore->get('debug_test'); - - expect($this->datastore->num_queries)->toBeGreaterThan($initialQueries); - }); - - it('tracks timing information', function () { - $initialTime = $this->datastore->sql_timetotal; - - $this->datastore->store('timing_test', 'value'); - - expect($this->datastore->sql_timetotal)->toBeGreaterThanOrEqual($initialTime); - }); - - it('maintains debug arrays', function () { - expect($this->datastore->dbg)->toBeArray(); - expect($this->datastore->dbg_id)->toBeInt(); - }); - }); - - describe('Source Debugging', function () { - it('finds debug caller information', function () { - $caller = $this->datastore->_debug_find_caller('enqueue'); - - expect($caller)->toBeString(); - // Caller might return "caller not found" in test environment - expect($caller)->toBeString(); - if (!str_contains($caller, 'not found')) { - expect($caller)->toContain('('); - expect($caller)->toContain(')'); - } - }); - - it('handles missing caller gracefully', function () { - $caller = $this->datastore->_debug_find_caller('non_existent_function'); - - expect($caller)->toBe('caller not found'); - }); - }); - - describe('Magic Methods', function () { - it('delegates property access to cache manager', function () { - // Test accessing cache manager properties - expect($this->datastore->prefix)->toBeString(); - expect($this->datastore->used)->toBeTrue(); - }); - - it('delegates method calls to cache manager', function () { - // Test calling cache manager methods - expect(fn() => $this->datastore->bulkLoad(['test1', 'test2']))->not->toThrow(Exception::class); - }); - - it('provides legacy database property', function () { - $db = $this->datastore->__get('db'); - - expect($db)->toBeObject(); - expect($db->dbg)->toBeArray(); - expect($db->engine)->toBe($this->datastore->engine); - }); - - it('throws exception for invalid property access', function () { - expect(fn() => $this->datastore->__get('invalid_property')) - ->toThrow(InvalidArgumentException::class); - }); - - it('throws exception for invalid method calls', function () { - expect(fn() => $this->datastore->__call('invalid_method', [])) - ->toThrow(BadMethodCallException::class); - }); - }); - - describe('Tag Support Detection', function () { - it('detects tag support in storage', function () { - $supportsTagsBefore = $this->datastore->supportsTags(); - - expect($supportsTagsBefore)->toBeBool(); - }); - - it('returns engine information', function () { - $engine = $this->datastore->getEngine(); - - expect($engine)->toBe($this->config['engine']); - }); - }); - - describe('Performance Testing', function () { - it('handles high-volume datastore operations efficiently') - ->group('performance') - ->expect(function () { - return measureExecutionTime(function () { - for ($i = 0; $i < 100; $i++) { - $this->datastore->store("perf_item_$i", ['data' => "value_$i"]); - $this->datastore->get("perf_item_$i"); - } - }); - }) - ->toBeLessThan(0.5); // 500ms for 100 operations - - it('efficiently handles bulk enqueue operations', function () { - $items = array_map(fn($i) => "bulk_perf_$i", range(1, 1000)); - - $time = measureExecutionTime(function () use ($items) { - $this->datastore->enqueue($items); - }); - - expect($time)->toBeLessThan(0.1); // 100ms for 1000 items - }); - }); - - describe('Error Handling', function () { - it('handles missing builder scripts', function () { - // Test with non-existent item - expect(fn() => $this->datastore->_build_item('non_existent')) - ->toThrow(Exception::class); - }); - - it('handles empty queue operations', function () { - $this->datastore->queued_items = []; - - // Should handle empty queue gracefully - just test that queue is empty - expect($this->datastore->queued_items)->toBeEmpty(); - }); - }); - - describe('Memory Optimization', function () { - it('manages memory efficiently with large datasets', function () { - // Create large dataset - $largeData = array_fill(0, 1000, str_repeat('x', 1000)); // ~1MB - - expect(fn() => $this->datastore->store('large_dataset', $largeData))->not->toThrow(Exception::class); - expect($this->datastore->get('large_dataset'))->toBe($largeData); - }); - - it('handles concurrent datastore operations', function () { - // Simulate concurrent operations - $operations = []; - for ($i = 0; $i < 50; $i++) { - $operations[] = function () use ($i) { - $this->datastore->store("concurrent_$i", ['id' => $i, 'data' => "value_$i"]); - return $this->datastore->get("concurrent_$i"); - }; - } - - // Execute operations - foreach ($operations as $i => $operation) { - $result = $operation(); - expect($result['id'])->toBe($i); - } - }); - }); - - describe('Backward Compatibility', function () { - it('maintains legacy Datastore API compatibility', function () { - // Test that all legacy methods exist and work - expect(method_exists($this->datastore, 'get'))->toBeTrue(); - expect(method_exists($this->datastore, 'store'))->toBeTrue(); - expect(method_exists($this->datastore, 'update'))->toBeTrue(); - expect(method_exists($this->datastore, 'rm'))->toBeTrue(); - expect(method_exists($this->datastore, 'clean'))->toBeTrue(); - }); - - it('provides backward compatible properties', function () { - expect(property_exists($this->datastore, 'data'))->toBeTrue(); - expect(property_exists($this->datastore, 'queued_items'))->toBeTrue(); - expect(property_exists($this->datastore, 'known_items'))->toBeTrue(); - expect(property_exists($this->datastore, 'ds_dir'))->toBeTrue(); - }); - - it('maintains reference semantics for get method', function () { - $testData = ['modifiable' => 'data']; - $this->datastore->store('reference_test', $testData); - - $retrieved = &$this->datastore->get('reference_test'); - $retrieved['modifiable'] = 'modified'; - - // Should maintain reference semantics - expect($this->datastore->data['reference_test']['modifiable'])->toBe('modified'); - }); - }); - - describe('Integration Features', function () { - it('integrates with cache manager debug features', function () { - $cacheManager = $this->datastore->getCacheManager(); - - expect($this->datastore->dbg_enabled)->toBe($cacheManager->dbg_enabled); - }); - - it('synchronizes debug counters properly', function () { - $initialQueries = $this->datastore->num_queries; - - // Perform operations through datastore - $this->datastore->store('sync_test', 'value'); - $this->datastore->get('sync_test'); - - // Counters should be synchronized - $cacheManager = $this->datastore->getCacheManager(); - expect($this->datastore->num_queries)->toBe($cacheManager->num_queries); - }); - }); -}); diff --git a/tests/Unit/Database/AffectedRowsTest.php b/tests/Unit/Database/AffectedRowsTest.php deleted file mode 100644 index 75fa8391f..000000000 --- a/tests/Unit/Database/AffectedRowsTest.php +++ /dev/null @@ -1,90 +0,0 @@ -makePartial(); - - expect(method_exists($db, 'affected_rows'))->toBeTrue(); - - // Mock the method to return a value - $db->shouldReceive('affected_rows')->andReturn(1); - - $result = $db->affected_rows(); - expect($result)->toBeInt(); - expect($result)->toBe(1); - }); - - it('affected_rows method returns 0 initially', function () { - $db = Mockery::mock(Database::class)->makePartial(); - $db->shouldReceive('affected_rows')->andReturn(0); - - expect($db->affected_rows())->toBe(0); - }); - - it('affected_rows can track INSERT operations', function () { - $db = Mockery::mock(Database::class)->makePartial(); - - // Mock that INSERT affects 1 row - $db->shouldReceive('affected_rows')->andReturn(1); - - expect($db->affected_rows())->toBe(1); - }); - - it('affected_rows can track UPDATE operations', function () { - $db = Mockery::mock(Database::class)->makePartial(); - - // Mock that UPDATE affects 3 rows - $db->shouldReceive('affected_rows')->andReturn(3); - - expect($db->affected_rows())->toBe(3); - }); - - it('affected_rows can track DELETE operations', function () { - $db = Mockery::mock(Database::class)->makePartial(); - - // Mock that DELETE affects 2 rows - $db->shouldReceive('affected_rows')->andReturn(2); - - expect($db->affected_rows())->toBe(2); - }); - - it('affected_rows returns 0 when no rows affected', function () { - $db = Mockery::mock(Database::class)->makePartial(); - - // Mock operation that affects no rows - $db->shouldReceive('affected_rows')->andReturn(0); - - expect($db->affected_rows())->toBe(0); - }); - - it('validates Database class has last_affected_rows property', function () { - // Test that the Database class structure supports affected_rows tracking - $reflection = new ReflectionClass(Database::class); - - expect($reflection->hasProperty('last_affected_rows'))->toBeTrue(); - expect($reflection->hasMethod('affected_rows'))->toBeTrue(); - }); - - it('validates fix is present in source code', function () { - // Simple source code validation to ensure fix is in place - $databaseSource = file_get_contents(__DIR__ . '/../../../src/Database/Database.php'); - - // Check that our fix is present: getRowCount() usage - expect($databaseSource)->toContain('getRowCount()'); - - // Check that the last_affected_rows property exists - expect($databaseSource)->toContain('last_affected_rows'); - }); -}); \ No newline at end of file diff --git a/tests/Unit/Database/DatabaseDebuggerTest.php b/tests/Unit/Database/DatabaseDebuggerTest.php deleted file mode 100644 index 258157452..000000000 --- a/tests/Unit/Database/DatabaseDebuggerTest.php +++ /dev/null @@ -1,572 +0,0 @@ -db = Database::getInstance(getTestDatabaseConfig()); - $this->db->connection = mockConnection(); - $this->debugger = $this->db->debugger; - }); - - afterEach(function () { - cleanupSingletons(); - }); - - describe('Initialization', function () { - it('initializes with database reference', function () { - // Test that debugger is properly constructed with database reference - expect($this->debugger)->toBeInstanceOf(DatabaseDebugger::class); - - // Test that it has necessary public properties/methods - expect(property_exists($this->debugger, 'dbg_enabled'))->toBe(true); - expect(property_exists($this->debugger, 'dbg'))->toBe(true); - }); - - it('sets up debug configuration', function () { - expect($this->debugger->dbg_enabled)->toBeBool(); - expect($this->debugger->do_explain)->toBeBool(); - expect($this->debugger->slow_time)->toBeFloat(); - }); - - it('initializes debug arrays', function () { - expect($this->debugger->dbg)->toBeArray(); - expect($this->debugger->dbg_id)->toBe(0); - expect($this->debugger->legacy_queries)->toBeArray(); - }); - - it('sets up timing properties', function () { - expect($this->debugger->sql_starttime)->toBeFloat(); - expect($this->debugger->cur_query_time)->toBeFloat(); - }); - }); - - describe('Debug Configuration', function () { - it('enables debug based on dev settings', function () { - // Test that debug configuration is working - $originalEnabled = $this->debugger->dbg_enabled; - - // Test that the debugger has debug configuration - expect($this->debugger->dbg_enabled)->toBeBool(); - expect(isset($this->debugger->dbg_enabled))->toBe(true); - }); - - it('enables explain based on cookie', function () { - $_COOKIE['explain'] = '1'; - - // Test that explain functionality can be configured - expect(property_exists($this->debugger, 'do_explain'))->toBe(true); - expect($this->debugger->do_explain)->toBeBool(); - - unset($_COOKIE['explain']); - }); - - it('respects slow query time constants', function () { - if (!defined('SQL_SLOW_QUERY_TIME')) { - define('SQL_SLOW_QUERY_TIME', 5.0); - } - - $debugger = new DatabaseDebugger($this->db); - - expect($debugger->slow_time)->toBe(5.0); - }); - }); - - describe('Debug Information Collection', function () { - beforeEach(function () { - $this->debugger->dbg_enabled = true; - $this->db->cur_query = 'SELECT * FROM test_table'; - }); - - it('captures debug info on start', function () { - $this->debugger->debug('start'); - - expect($this->debugger->dbg[0])->toHaveKey('sql'); - expect($this->debugger->dbg[0])->toHaveKey('src'); - expect($this->debugger->dbg[0])->toHaveKey('file'); - expect($this->debugger->dbg[0])->toHaveKey('line'); - expect($this->debugger->dbg[0]['sql'])->toContain('SELECT * FROM test_table'); - }); - - it('captures timing info on stop', function () { - $this->debugger->debug('start'); - usleep(1000); // 1ms delay - $this->debugger->debug('stop'); - - expect($this->debugger->dbg[0])->toHaveKey('time'); - expect($this->debugger->dbg[0]['time'])->toBeFloat(); - expect($this->debugger->dbg[0]['time'])->toBeGreaterThan(0); - }); - - it('captures memory usage if available', function () { - // Mock sys function - if (!function_exists('sys')) { - eval('function sys($what) { return $what === "mem" ? 1024 : 0; }'); - } - - $this->debugger->debug('start'); - $this->debugger->debug('stop'); - - expect($this->debugger->dbg[0])->toHaveKey('mem_before'); - expect($this->debugger->dbg[0])->toHaveKey('mem_after'); - }); - - it('increments debug ID after each query', function () { - $initialId = $this->debugger->dbg_id; - - $this->debugger->debug('start'); - $this->debugger->debug('stop'); - - expect($this->debugger->dbg_id)->toBe($initialId + 1); - }); - - it('handles multiple debug entries', function () { - // First query - $this->db->cur_query = 'SELECT 1'; - $this->debugger->debug('start'); - $this->debugger->debug('stop'); - - // Second query - $this->db->cur_query = 'SELECT 2'; - $this->debugger->debug('start'); - $this->debugger->debug('stop'); - - expect($this->debugger->dbg)->toHaveCount(2); - expect($this->debugger->dbg[0]['sql'])->toContain('SELECT 1'); - expect($this->debugger->dbg[1]['sql'])->toContain('SELECT 2'); - }); - }); - - describe('Source Detection', function () { - it('finds debug source information', function () { - $source = $this->debugger->debug_find_source(); - - expect($source)->toBeString(); - expect($source)->toContain('('); - expect($source)->toContain(')'); - }); - - it('extracts file path only when requested', function () { - $file = $this->debugger->debug_find_source('file'); - - expect($file)->toBeString(); - expect($file)->toContain('.php'); - }); - - it('extracts line number only when requested', function () { - $line = $this->debugger->debug_find_source('line'); - - expect($line)->toBeString(); - expect(is_numeric($line) || $line === '?')->toBeTrue(); - }); - - it('returns "src disabled" when SQL_PREPEND_SRC is false', function () { - if (defined('SQL_PREPEND_SRC')) { - // Create new constant for this test - eval('define("TEST_SQL_PREPEND_SRC", false);'); - } - - // This test would need modification of the actual method to test properly - // For now, we'll test the positive case - $source = $this->debugger->debug_find_source(); - expect($source)->not->toBe('src disabled'); - }); - - it('skips Database-related files in stack trace', function () { - $source = $this->debugger->debug_find_source(); - - // Should not contain Database.php or DatabaseDebugger.php in the result - expect($source)->not->toContain('Database.php'); - expect($source)->not->toContain('DatabaseDebugger.php'); - }); - }); - - describe('Nette Explorer Detection', function () { - it('detects Nette Explorer in call stack', function () { - // Create a mock trace that includes Nette Database classes - $trace = [ - ['class' => 'Nette\\Database\\Table\\Selection', 'function' => 'select'], - ['class' => 'TorrentPier\\Database\\DebugSelection', 'function' => 'where'], - ['file' => '/path/to/DatabaseTest.php', 'function' => 'testMethod'] - ]; - - $result = $this->debugger->detectNetteExplorerInTrace($trace); - - expect($result)->toBeTrue(); - }); - - it('detects Nette Explorer by SQL syntax patterns', function () { - $netteSQL = 'SELECT `id`, `name` FROM `users` WHERE (`active` = 1)'; - - $result = $this->debugger->detectNetteExplorerBySqlSyntax($netteSQL); - - expect($result)->toBeTrue(); - }); - - it('does not detect regular SQL as Nette Explorer', function () { - $regularSQL = 'SELECT id, name FROM users WHERE active = 1'; - - $result = $this->debugger->detectNetteExplorerBySqlSyntax($regularSQL); - - expect($result)->toBeFalse(); - }); - - it('marks queries as Nette Explorer when detected', function () { - $this->debugger->markAsNetteExplorerQuery(); - - expect($this->debugger->is_nette_explorer_query)->toBeTrue(); - }); - - it('resets Nette Explorer flag after query completion', function () { - $this->debugger->markAsNetteExplorerQuery(); - $this->debugger->resetNetteExplorerFlag(); - - expect($this->debugger->is_nette_explorer_query)->toBeFalse(); - }); - - it('adds Nette Explorer marker to debug info', function () { - $this->debugger->dbg_enabled = true; - $this->debugger->markAsNetteExplorerQuery(); - - $this->db->cur_query = 'SELECT `id` FROM `users`'; - $this->debugger->debug('start'); - $this->debugger->debug('stop'); - - $debugEntry = $this->debugger->dbg[0]; - expect($debugEntry['is_nette_explorer'])->toBeTrue(); - expect($debugEntry['info'])->toContain('[Nette Explorer]'); - }); - }); - - describe('Query Logging', function () { - beforeEach(function () { - $this->db->DBS['log_counter'] = 0; - $this->db->DBS['log_file'] = 'test_queries'; - }); - - it('prepares for query logging', function () { - $this->debugger->log_next_query(3, 'custom_log'); - - expect($this->db->DBS['log_counter'])->toBe(3); - expect($this->db->DBS['log_file'])->toBe('custom_log'); - }); - - it('logs queries when enabled', function () { - $this->debugger->log_next_query(1); - $this->db->inited = true; - $this->db->cur_query = 'SELECT 1'; - $this->debugger->cur_query_time = 0.001; - $this->debugger->sql_starttime = microtime(true); - - // Should not throw - expect(fn() => $this->debugger->log_query())->not->toThrow(Exception::class); - }); - - it('logs slow queries when they exceed threshold', function () { - $this->debugger->slow_time = 0.001; // Very low threshold - $this->debugger->cur_query_time = 0.002; // Exceeds threshold - $this->db->cur_query = 'SELECT SLEEP(1)'; - - expect(fn() => $this->debugger->log_slow_query())->not->toThrow(Exception::class); - }); - - it('respects slow query cache setting', function () { - // Mock CACHE function - if (!function_exists('CACHE')) { - eval(' - function CACHE($name) { - return new class { - public function get($key) { return true; } // Indicates not to log - }; - } - '); - } - - $this->debugger->slow_time = 0.001; - $this->debugger->cur_query_time = 0.002; - - // Should not log due to cache setting - expect(fn() => $this->debugger->log_slow_query())->not->toThrow(Exception::class); - }); - }); - - describe('Error Logging', function () { - it('logs exceptions with detailed information', function () { - $exception = new Exception('Test database error', 1064); - - expect(fn() => $this->debugger->log_error($exception))->not->toThrow(Exception::class); - }); - - it('logs PDO exceptions with specific details', function () { - $pdoException = new PDOException('Connection failed'); - $pdoException->errorInfo = ['42000', 1045, 'Access denied']; - - expect(fn() => $this->debugger->log_error($pdoException))->not->toThrow(Exception::class); - }); - - it('logs comprehensive context information', function () { - $this->db->cur_query = 'SELECT * FROM nonexistent_table'; - $this->db->selected_db = 'test_db'; - $this->db->db_server = 'test_server'; - - $exception = new Exception('Table does not exist'); - - expect(fn() => $this->debugger->log_error($exception))->not->toThrow(Exception::class); - }); - - it('handles empty or no-error states gracefully', function () { - // Mock sql_error to return no error - $this->db->connection = mockConnection(); - - expect(fn() => $this->debugger->log_error())->not->toThrow(Exception::class); - }); - - it('checks connection status during error logging', function () { - $this->db->connection = null; // No connection - - $exception = new Exception('No connection'); - - expect(fn() => $this->debugger->log_error($exception))->not->toThrow(Exception::class); - }); - }); - - describe('Legacy Query Tracking', function () { - it('logs legacy queries that needed compatibility fixes', function () { - $problematicQuery = 'SELECT t.*, f.* FROM table t, forum f'; - $error = 'Found duplicate columns'; - - $this->debugger->logLegacyQuery($problematicQuery, $error); - - expect($this->debugger->legacy_queries)->not->toBeEmpty(); - expect($this->debugger->legacy_queries[0]['query'])->toBe($problematicQuery); - expect($this->debugger->legacy_queries[0]['error'])->toBe($error); - }); - - it('marks debug entries as legacy when logging', function () { - $this->debugger->dbg_enabled = true; - - // Create a debug entry first - $this->db->cur_query = 'SELECT t.*, f.*'; - $this->debugger->debug('start'); - $this->debugger->debug('stop'); - - // Now log it as legacy - $this->debugger->logLegacyQuery('SELECT t.*, f.*', 'Duplicate columns'); - - $debugEntry = $this->debugger->dbg[0]; - expect($debugEntry['is_legacy_query'])->toBeTrue(); - expect($debugEntry['info'])->toContain('LEGACY COMPATIBILITY FIX APPLIED'); - }); - - it('records detailed legacy query information', function () { - $query = 'SELECT * FROM old_table'; - $error = 'Compatibility issue'; - - $this->debugger->logLegacyQuery($query, $error); - - $entry = $this->debugger->legacy_queries[0]; - expect($entry)->toHaveKey('query'); - expect($entry)->toHaveKey('error'); - expect($entry)->toHaveKey('source'); - expect($entry)->toHaveKey('file'); - expect($entry)->toHaveKey('line'); - expect($entry)->toHaveKey('time'); - }); - }); - - describe('Explain Functionality', function () { - beforeEach(function () { - $this->debugger->do_explain = true; - $this->db->cur_query = 'SELECT * FROM users WHERE active = 1'; - }); - - it('starts explain capture for SELECT queries', function () { - expect(fn() => $this->debugger->explain('start'))->not->toThrow(Exception::class); - }); - - it('converts UPDATE queries to SELECT for explain', function () { - $this->db->cur_query = 'UPDATE users SET status = 1 WHERE id = 5'; - - expect(fn() => $this->debugger->explain('start'))->not->toThrow(Exception::class); - }); - - it('converts DELETE queries to SELECT for explain', function () { - $this->db->cur_query = 'DELETE FROM users WHERE status = 0'; - - expect(fn() => $this->debugger->explain('start'))->not->toThrow(Exception::class); - }); - - it('generates explain output on stop', function () { - $this->debugger->explain_hold = ''; - $this->debugger->dbg_enabled = true; - - // Create debug entry - $this->debugger->debug('start'); - $this->debugger->debug('stop'); - - // Test that explain functionality works without throwing exceptions - expect(fn() => $this->debugger->explain('stop'))->not->toThrow(Exception::class); - - // Verify that explain_out is a string (the explain functionality ran) - expect($this->debugger->explain_out)->toBeString(); - - // If there's any output, it should contain some HTML structure - if (!empty($this->debugger->explain_out)) { - expect($this->debugger->explain_out)->toContain(' 'users', - 'type' => 'ALL', - 'rows' => '1000' - ]; - - // Test that the explain method exists and can process row data - if (method_exists($this->debugger, 'explain')) { - expect(fn() => $this->debugger->explain('add_explain_row', false, $row)) - ->not->toThrow(Exception::class); - } else { - // If method doesn't exist, just verify our data structure - expect($row)->toHaveKey('table'); - expect($row)->toHaveKey('type'); - expect($row)->toHaveKey('rows'); - } - }); - }); - - describe('Performance Optimization', function () { - it('marks slow queries for ignoring when expected', function () { - // Test that the method exists and can be called without throwing - expect(fn() => $this->debugger->expect_slow_query(60, 5))->not->toThrow(Exception::class); - }); - - it('respects priority levels for slow query marking', function () { - // Test that the method handles multiple calls correctly - expect(fn() => $this->debugger->expect_slow_query(30, 10))->not->toThrow(Exception::class); - expect(fn() => $this->debugger->expect_slow_query(60, 5))->not->toThrow(Exception::class); - }); - }); - - describe('Debug Statistics', function () { - it('provides debug statistics', function () { - // Generate some actual debug data to test stats - $this->debugger->dbg_enabled = true; - - // Create some debug entries - $this->db->cur_query = 'SELECT 1'; - $this->debugger->debug('start'); - usleep(1000); - $this->debugger->debug('stop'); - - // Test that the stats method exists and returns expected structure - $result = method_exists($this->debugger, 'getDebugStats') || - !empty($this->debugger->dbg); - - expect($result)->toBe(true); - }); - - it('clears debug data when requested', function () { - // Add some debug data first - $this->debugger->dbg = [createDebugEntry()]; - $this->debugger->legacy_queries = [['query' => 'test']]; - $this->debugger->dbg_id = 5; - - // Test that clear methods exist and work - if (method_exists($this->debugger, 'clearDebugData')) { - $this->debugger->clearDebugData(); - expect($this->debugger->dbg)->toBeEmpty(); - } else { - // Manual cleanup for testing - $this->debugger->dbg = []; - $this->debugger->legacy_queries = []; - $this->debugger->dbg_id = 0; - - expect($this->debugger->dbg)->toBeEmpty(); - expect($this->debugger->legacy_queries)->toBeEmpty(); - expect($this->debugger->dbg_id)->toBe(0); - } - }); - }); - - describe('Timing Accuracy', function () { - it('measures query execution time accurately', function () { - $this->debugger->debug('start'); - $startTime = $this->debugger->sql_starttime; - - usleep(2000); // 2ms delay - - $this->debugger->debug('stop'); - - expect($this->debugger->cur_query_time)->toBeGreaterThan(0.001); - expect($this->debugger->cur_query_time)->toBeLessThan(0.1); - }); - - it('accumulates total SQL time correctly', function () { - $initialTotal = $this->db->sql_timetotal; - - $this->debugger->debug('start'); - usleep(1000); - $this->debugger->debug('stop'); - - expect($this->db->sql_timetotal)->toBeGreaterThan($initialTotal); - }); - - it('updates DBS statistics correctly', function () { - $initialDBS = $this->db->DBS['sql_timetotal']; - - $this->debugger->debug('start'); - usleep(1000); - $this->debugger->debug('stop'); - - expect($this->db->DBS['sql_timetotal'])->toBeGreaterThan($initialDBS); - }); - }); - - describe('Edge Cases', function () { - it('handles debugging when query is null', function () { - $this->db->cur_query = null; - $this->debugger->dbg_enabled = true; - - expect(fn() => $this->debugger->debug('start'))->not->toThrow(Exception::class); - expect(fn() => $this->debugger->debug('stop'))->not->toThrow(Exception::class); - }); - - it('handles debugging when connection is null', function () { - $this->db->connection = null; - - expect(fn() => $this->debugger->log_error(new Exception('Test')))->not->toThrow(Exception::class); - }); - - it('handles missing global functions gracefully', function () { - // Test when bb_log function doesn't exist - if (function_exists('bb_log')) { - // We can't really undefine it, but we can test error handling - expect(fn() => $this->debugger->log_query())->not->toThrow(Exception::class); - } - }); - - it('handles empty debug arrays', function () { - // Reset to empty state - $this->debugger->dbg = []; - $this->debugger->dbg_id = 0; - - // Test handling of empty arrays - expect($this->debugger->dbg)->toBeEmpty(); - expect($this->debugger->dbg_id)->toBe(0); - - // Test that debug operations still work with empty state - expect(fn() => $this->debugger->debug('start'))->not->toThrow(Exception::class); - }); - }); -}); diff --git a/tests/Unit/Database/DatabaseTest.php b/tests/Unit/Database/DatabaseTest.php deleted file mode 100644 index 58e240f11..000000000 --- a/tests/Unit/Database/DatabaseTest.php +++ /dev/null @@ -1,730 +0,0 @@ -toBe($instance2); - expect($instance1)->toBeInstanceOf(Database::class); - }); - - it('creates different instances for different servers', function () { - $config = getTestDatabaseConfig(); - - $dbInstance = Database::getServerInstance($config, 'db'); - $trackerInstance = Database::getServerInstance($config, 'tracker'); - - expect($dbInstance)->not->toBe($trackerInstance); - expect($dbInstance)->toBeInstanceOf(Database::class); - expect($trackerInstance)->toBeInstanceOf(Database::class); - }); - - it('sets first server instance as default', function () { - $config = getTestDatabaseConfig(); - - $trackerInstance = Database::getServerInstance($config, 'tracker'); - $defaultInstance = Database::getInstance(); - - expect($defaultInstance)->toBe($trackerInstance); - }); - - it('stores configuration correctly', function () { - $config = getTestDatabaseConfig(); - $db = Database::getInstance($config); - - expect($db->cfg)->toBe($config); - expect($db->db_server)->toBe('db'); - expect($db->cfg_keys)->toContain('dbhost', 'dbport', 'dbname'); - }); - - it('initializes debugger on construction', function () { - $config = getTestDatabaseConfig(); - $db = Database::getInstance($config); - - expect($db->debugger)->toBeInstanceOf(DatabaseDebugger::class); - }); - }); - - describe('Configuration Validation', function () { - it('validates required configuration keys', function () { - $requiredKeys = ['dbhost', 'dbport', 'dbname', 'dbuser', 'dbpasswd', 'charset', 'persist']; - $config = getTestDatabaseConfig(); - - foreach ($requiredKeys as $key) { - expect($config)->toHaveKey($key); - } - }); - - it('validates configuration has correct structure', function () { - $config = getTestDatabaseConfig(); - expect($config)->toBeValidDatabaseConfig(); - }); - - it('handles missing configuration gracefully', function () { - $invalidConfig = ['dbhost' => 'localhost']; // Missing required keys - - expect(function () use ($invalidConfig) { - Database::getInstance(array_values($invalidConfig)); - })->toThrow(ValueError::class); - }); - }); - - describe('Connection Management', function () { - it('initializes connection state correctly', function () { - $config = getTestDatabaseConfig(); - $db = Database::getInstance($config); - - expect($db->connection)->toBeNull(); - expect($db->inited)->toBeFalse(); - expect($db->num_queries)->toBe(0); - }); - - it('tracks initialization state', function () { - // Create a mock that doesn't try to connect to real database - $mockConnection = Mockery::mock(Connection::class); - $mockConnection->shouldReceive('connect')->andReturn(true); - - $this->db = Mockery::mock(Database::class)->makePartial(); - $this->db->shouldReceive('init')->andReturnNull(); - $this->db->shouldReceive('connect')->andReturnNull(); - - $this->db->init(); // void method, just call it - expect(true)->toBeTrue(); // Just verify it completes without error - }); - - it('only initializes once', function () { - $this->db = Mockery::mock(Database::class)->makePartial(); - $this->db->shouldReceive('init')->twice()->andReturnNull(); - - // Both calls should work - $this->db->init(); - $this->db->init(); - - expect(true)->toBeTrue(); // Just verify both calls complete without error - }); - - it('handles connection errors gracefully', function () { - $invalidConfig = getInvalidDatabaseConfig(); - $db = Database::getInstance($invalidConfig); - - // Connection should fail with invalid config - expect(fn() => $db->connect())->toThrow(Exception::class); - }); - }); - - describe('Query Execution', function () { - beforeEach(function () { - $this->db = Mockery::mock(Database::class)->makePartial(); - $this->db->shouldReceive('init')->andReturnNull(); - $this->db->num_queries = 0; - - // Mock the debugger to prevent null pointer errors - $mockDebugger = Mockery::mock(\TorrentPier\Database\DatabaseDebugger::class); - $mockDebugger->shouldReceive('debug_find_source')->andReturn('test.php:123'); - $mockDebugger->shouldReceive('debug')->andReturnNull(); - $this->db->debugger = $mockDebugger; - }); - - it('executes SQL queries successfully', function () { - $query = 'SELECT * FROM users'; - $mockResult = Mockery::mock(ResultSet::class); - - $this->db->shouldReceive('sql_query')->with($query)->andReturn($mockResult); - - $result = $this->db->sql_query($query); - - expect($result)->toBeInstanceOf(ResultSet::class); - }); - - it('handles SQL query arrays', function () { - $queryArray = createSelectQuery(); - $mockResult = Mockery::mock(ResultSet::class); - - $this->db->shouldReceive('sql_query')->with(Mockery::type('array'))->andReturn($mockResult); - - $result = $this->db->sql_query($queryArray); - - expect($result)->toBeInstanceOf(ResultSet::class); - }); - - it('increments query counter correctly', function () { - $initialCount = $this->db->num_queries; - $mockResult = Mockery::mock(ResultSet::class); - - $this->db->shouldReceive('sql_query')->andReturn($mockResult); - $this->db->shouldReceive('getQueryCount')->andReturn($initialCount + 1); - - $this->db->sql_query('SELECT 1'); - - expect($this->db->getQueryCount())->toBe($initialCount + 1); - }); - - it('prepends debug source to queries when enabled', function () { - $query = 'SELECT * FROM users'; - $mockResult = Mockery::mock(ResultSet::class); - - $this->db->shouldReceive('sql_query')->with($query)->andReturn($mockResult); - - // Mock the debug source prepending behavior - $result = $this->db->sql_query($query); - - expect($result)->toBeInstanceOf(ResultSet::class); - }); - - it('handles query execution errors', function () { - $query = 'INVALID SQL'; - - $this->db->shouldReceive('sql_query')->with($query) - ->andThrow(new Exception('SQL syntax error')); - - expect(function () use ($query) { - $this->db->sql_query($query); - })->toThrow(Exception::class); - }); - - it('executes query wrapper with error handling', function () { - $this->db->shouldReceive('query_wrap')->andReturn(true); - - $result = $this->db->query_wrap(); - - expect($result)->toBe(true); - }); - }); - - describe('Result Processing', function () { - beforeEach(function () { - $this->db = Mockery::mock(Database::class)->makePartial(); - $this->mockResult = Mockery::mock(ResultSet::class); - }); - - it('counts number of rows correctly', function () { - $this->mockResult->shouldReceive('getRowCount')->andReturn(5); - - $this->db->shouldReceive('num_rows')->with($this->mockResult)->andReturn(5); - - $count = $this->db->num_rows($this->mockResult); - - expect($count)->toBe(5); - }); - - it('tracks affected rows', function () { - $this->db->shouldReceive('affected_rows')->andReturn(5); - - $affected = $this->db->affected_rows(); - - expect($affected)->toBe(5); - }); - - it('fetches single row correctly', function () { - $mockRow = Mockery::mock(\Nette\Database\Row::class); - $mockRow->shouldReceive('toArray')->andReturn(['id' => 1, 'name' => 'test']); - - $this->mockResult->shouldReceive('fetch')->andReturn($mockRow); - - $this->db->shouldReceive('sql_fetchrow')->with($this->mockResult) - ->andReturn(['id' => 1, 'name' => 'test']); - - $row = $this->db->sql_fetchrow($this->mockResult); - - expect($row)->toBe(['id' => 1, 'name' => 'test']); - }); - - it('fetches single field from row', function () { - $this->db->shouldReceive('sql_fetchfield')->with('name', 0, $this->mockResult) - ->andReturn('test_value'); - - $value = $this->db->sql_fetchfield('name', 0, $this->mockResult); - - expect($value)->toBe('test_value'); - }); - - it('returns false for empty result', function () { - $this->mockResult->shouldReceive('fetch')->andReturn(null); - - $this->db->shouldReceive('sql_fetchrow')->with($this->mockResult)->andReturn(false); - - $row = $this->db->sql_fetchrow($this->mockResult); - - expect($row)->toBe(false); - }); - - it('fetches multiple rows as rowset', function () { - $expectedRows = [ - ['id' => 1, 'name' => 'test1'], - ['id' => 2, 'name' => 'test2'] - ]; - - $this->db->shouldReceive('sql_fetchrowset')->with($this->mockResult) - ->andReturn($expectedRows); - - $rowset = $this->db->sql_fetchrowset($this->mockResult); - - expect($rowset)->toBe($expectedRows); - expect($rowset)->toHaveCount(2); - }); - - it('fetches rowset with field extraction', function () { - $expectedValues = ['test1', 'test2']; - - $this->db->shouldReceive('sql_fetchrowset')->with($this->mockResult, 'name') - ->andReturn($expectedValues); - - $values = $this->db->sql_fetchrowset($this->mockResult, 'name'); - - expect($values)->toBe($expectedValues); - expect($values)->toHaveCount(2); - }); - }); - - describe('SQL Building', function () { - beforeEach(function () { - $this->db = Mockery::mock(Database::class)->makePartial(); - }); - - it('builds SELECT queries correctly', function () { - $sqlArray = [ - 'SELECT' => ['*'], - 'FROM' => ['users'], - 'WHERE' => ['active = 1'] - ]; - - $this->db->shouldReceive('build_sql')->with($sqlArray) - ->andReturn('SELECT * FROM users WHERE active = 1'); - - $sql = $this->db->build_sql($sqlArray); - - expect($sql)->toContain('SELECT *'); - expect($sql)->toContain('FROM users'); - expect($sql)->toContain('WHERE active = 1'); - }); - - it('builds INSERT queries correctly', function () { - $sqlArray = [ - 'INSERT' => 'test_table', - 'VALUES' => ['name' => 'John', 'email' => 'john@test.com'] - ]; - - $this->db->shouldReceive('build_sql')->with($sqlArray) - ->andReturn("INSERT INTO test_table (name, email) VALUES ('John', 'john@test.com')"); - - $sql = $this->db->build_sql($sqlArray); - - expect($sql)->toContain('INSERT INTO test_table'); - expect($sql)->toContain('John'); - expect($sql)->toContain('john@test.com'); - }); - - it('builds UPDATE queries correctly', function () { - $sqlArray = [ - 'UPDATE' => 'test_table', - 'SET' => ['name' => 'Jane'], - 'WHERE' => ['id = 1'] - ]; - - $this->db->shouldReceive('build_sql')->with($sqlArray) - ->andReturn("UPDATE test_table SET name = 'Jane' WHERE id = 1"); - - $sql = $this->db->build_sql($sqlArray); - - expect($sql)->toContain('UPDATE test_table'); - expect($sql)->toContain('Jane'); - }); - - it('builds DELETE queries correctly', function () { - $sqlArray = [ - 'DELETE' => 'test_table', - 'WHERE' => ['id = 1'] - ]; - - $this->db->shouldReceive('build_sql')->with($sqlArray) - ->andReturn('DELETE FROM test_table WHERE id = 1'); - - $sql = $this->db->build_sql($sqlArray); - - expect($sql)->toContain('DELETE FROM test_table'); - expect($sql)->toContain('WHERE id = 1'); - }); - - it('creates empty SQL array template', function () { - $emptyArray = $this->db->get_empty_sql_array(); - - expect($emptyArray)->toBeArray(); - expect($emptyArray)->toHaveKey('SELECT'); - expect($emptyArray)->toHaveKey('FROM'); - expect($emptyArray)->toHaveKey('WHERE'); - }); - - it('builds arrays with escaping', function () { - $data = ['name' => "O'Reilly", 'count' => 42]; - - $this->db->shouldReceive('build_array')->with('UPDATE', $data) - ->andReturn("name = 'O\\'Reilly', count = 42"); - - $result = $this->db->build_array('UPDATE', $data); - - expect($result)->toContain("O\\'Reilly"); - expect($result)->toContain('42'); - }); - }); - - describe('Data Escaping', function () { - beforeEach(function () { - $this->db = Mockery::mock(Database::class)->makePartial(); - }); - - it('escapes strings correctly', function () { - $testString = "O'Reilly & Associates"; - $expected = "O\\'Reilly & Associates"; - - $this->db->shouldReceive('escape')->with($testString)->andReturn($expected); - - $result = $this->db->escape($testString); - - expect($result)->toBe($expected); - }); - - it('escapes with type checking', function () { - $this->db->shouldReceive('escape')->with(123, true)->andReturn('123'); - $this->db->shouldReceive('escape')->with('test', true)->andReturn("'test'"); - - $intResult = $this->db->escape(123, true); - $stringResult = $this->db->escape('test', true); - - expect($intResult)->toBe('123'); - expect($stringResult)->toBe("'test'"); - }); - }); - - describe('Database Explorer Integration', function () { - beforeEach(function () { - $this->db = Mockery::mock(Database::class)->makePartial(); - $mockExplorer = Mockery::mock(Explorer::class); - $mockSelection = Mockery::mock(\Nette\Database\Table\Selection::class); - $mockExplorer->shouldReceive('table')->andReturn($mockSelection); - - $this->db->shouldReceive('getExplorer')->andReturn($mockExplorer); - }); - - it('provides table access through explorer', function () { - $mockSelection = Mockery::mock(\TorrentPier\Database\DebugSelection::class); - $this->db->shouldReceive('table')->with('users')->andReturn($mockSelection); - - $selection = $this->db->table('users'); - - expect($selection)->toBeInstanceOf(\TorrentPier\Database\DebugSelection::class); - }); - - it('initializes explorer lazily', function () { - $mockSelection = Mockery::mock(\TorrentPier\Database\DebugSelection::class); - $this->db->shouldReceive('table')->with('posts')->andReturn($mockSelection); - - // First access should initialize explorer - $selection1 = $this->db->table('posts'); - $selection2 = $this->db->table('posts'); - - expect($selection1)->toBeInstanceOf(\TorrentPier\Database\DebugSelection::class); - expect($selection2)->toBeInstanceOf(\TorrentPier\Database\DebugSelection::class); - }); - }); - - describe('Utility Methods', function () { - beforeEach(function () { - $this->db = Mockery::mock(Database::class)->makePartial(); - }); - - it('gets next insert ID', function () { - $this->db->shouldReceive('sql_nextid')->andReturn(123); - - $nextId = $this->db->sql_nextid(); - - expect($nextId)->toBe(123); - }); - - it('frees SQL result resources', function () { - $this->db->shouldReceive('sql_freeresult')->andReturnNull(); - - $this->db->sql_freeresult(); // void method, just call it - expect(true)->toBeTrue(); // Just verify it completes without error - }); - - it('closes database connection', function () { - $this->db->shouldReceive('close')->andReturnNull(); - - $this->db->close(); // void method, just call it - expect(true)->toBeTrue(); // Just verify it completes without error - }); - - it('provides database version information', function () { - $this->db->shouldReceive('get_version')->andReturn('8.0.25-MySQL'); - - $version = $this->db->get_version(); - - expect($version)->toBeString(); - expect($version)->toContain('MySQL'); - }); - - it('handles database errors', function () { - $expectedError = [ - 'code' => '42000', - 'message' => 'Syntax error or access violation' - ]; - - $this->db->shouldReceive('sql_error')->andReturn($expectedError); - - $error = $this->db->sql_error(); - - expect($error)->toHaveKey('code'); - expect($error)->toHaveKey('message'); - expect($error['code'])->toBe('42000'); - }); - }); - - describe('Locking Mechanisms', function () { - beforeEach(function () { - $this->db = Mockery::mock(Database::class)->makePartial(); - }); - - it('gets named locks', function () { - $lockName = 'test_lock'; - $timeout = 10; - - $this->db->shouldReceive('get_lock')->with($lockName, $timeout)->andReturn(1); - - $result = $this->db->get_lock($lockName, $timeout); - - expect($result)->toBe(1); - }); - - it('releases named locks', function () { - $lockName = 'test_lock'; - - $this->db->shouldReceive('release_lock')->with($lockName)->andReturn(1); - - $result = $this->db->release_lock($lockName); - - expect($result)->toBe(1); - }); - - it('checks if lock is free', function () { - $lockName = 'test_lock'; - - $this->db->shouldReceive('is_free_lock')->with($lockName)->andReturn(1); - - $result = $this->db->is_free_lock($lockName); - - expect($result)->toBe(1); - }); - - it('generates lock names correctly', function () { - $this->db->shouldReceive('get_lock_name')->with('test')->andReturn('BB_LOCK_test'); - - $lockName = $this->db->get_lock_name('test'); - - expect($lockName)->toContain('BB_LOCK_'); - expect($lockName)->toContain('test'); - }); - }); - - describe('Shutdown Handling', function () { - beforeEach(function () { - $this->db = Mockery::mock(Database::class)->makePartial(); - $this->db->shutdown = []; - }); - - it('adds shutdown queries', function () { - $query = 'UPDATE stats SET value = value + 1'; - - $this->db->shouldReceive('add_shutdown_query')->with($query)->andReturn(true); - $this->db->shouldReceive('getShutdownQueries')->andReturn([$query]); - - $this->db->add_shutdown_query($query); - - expect($this->db->getShutdownQueries())->toContain($query); - }); - - it('executes shutdown queries', function () { - $this->db->shouldReceive('add_shutdown_query')->with('SELECT 1'); - $this->db->shouldReceive('exec_shutdown_queries')->andReturn(true); - $this->db->shouldReceive('getQueryCount')->andReturn(1); - - $this->db->add_shutdown_query('SELECT 1'); - - $initialQueries = 0; - $this->db->exec_shutdown_queries(); - - expect($this->db->getQueryCount())->toBeGreaterThan($initialQueries); - }); - - it('clears shutdown queries after execution', function () { - $this->db->shouldReceive('add_shutdown_query')->with('SELECT 1'); - $this->db->shouldReceive('exec_shutdown_queries')->andReturn(true); - $this->db->shouldReceive('getShutdownQueries')->andReturn([]); - - $this->db->add_shutdown_query('SELECT 1'); - - $this->db->exec_shutdown_queries(); - - expect($this->db->getShutdownQueries())->toBeEmpty(); - }); - }); - - describe('Magic Methods', function () { - beforeEach(function () { - $this->db = Database::getInstance(getTestDatabaseConfig()); - $this->db->debugger = mockDatabaseDebugger(); - }); - - it('provides access to debugger properties via magic getter', function () { - $this->db->debugger->dbg_enabled = true; - - $value = $this->db->__get('dbg_enabled'); - - expect($value)->toBeTrue(); - }); - - it('checks property existence via magic isset', function () { - $exists = $this->db->__isset('dbg_enabled'); - - expect($exists)->toBeTrue(); - }); - - it('returns false for non-existent properties', function () { - $exists = $this->db->__isset('non_existent_property'); - - expect($exists)->toBeFalse(); - }); - - it('throws exception for invalid property access', function () { - expect(fn() => $this->db->__get('invalid_property')) - ->toThrow(InvalidArgumentException::class); - }); - }); - - describe('Performance Testing', function () { - beforeEach(function () { - $this->db = Database::getInstance(getTestDatabaseConfig()); - $this->db->connection = mockConnection(); - }); - - it('executes queries within acceptable time', function () { - expectExecutionTimeUnder(function () { - $this->db->sql_query('SELECT 1'); - }, 0.01); // 10ms - }); - - it('handles multiple concurrent queries efficiently', function () { - expectExecutionTimeUnder(function () { - for ($i = 0; $i < 100; $i++) { - $this->db->sql_query("SELECT $i"); - } - }, 0.1); // 100ms for 100 queries - }); - }); - - describe('Error Handling', function () { - beforeEach(function () { - $this->db = Mockery::mock(Database::class)->makePartial(); - }); - - it('handles connection errors gracefully', function () { - $invalidConfig = getInvalidDatabaseConfig(); - $db = Database::getInstance($invalidConfig); - - expect(fn() => $db->connect())->toThrow(Exception::class); - }); - - it('triggers error for query failures when using wrapper', function () { - // Mock sql_query to return null (indicating failure) - $this->db->shouldReceive('sql_query')->andReturn(null); - - // Mock trigger_error to throw RuntimeException instead of calling bb_die - $this->db->shouldReceive('trigger_error')->andThrow(new \RuntimeException('Database Error')); - - expect(fn() => $this->db->query('INVALID')) - ->toThrow(\RuntimeException::class); - }); - - it('logs errors appropriately', function () { - $exception = new Exception('Test error'); - - // Should not throw when logging errors - $this->db->shouldReceive('logError')->with($exception)->andReturn(true); - - expect(fn() => $this->db->logError($exception)) - ->not->toThrow(Exception::class); - }); - }); - - describe('Legacy Compatibility', function () { - it('maintains backward compatibility with SqlDb interface', function () { - $db = Database::getInstance(getTestDatabaseConfig()); - $db->connection = mockConnection(); - - // All these methods should exist and work - expect(method_exists($db, 'sql_query'))->toBeTrue(); - expect(method_exists($db, 'sql_fetchrow'))->toBeTrue(); - expect(method_exists($db, 'sql_fetchrowset'))->toBeTrue(); - expect(method_exists($db, 'fetch_row'))->toBeTrue(); - expect(method_exists($db, 'fetch_rowset'))->toBeTrue(); - expect(method_exists($db, 'affected_rows'))->toBeTrue(); - expect(method_exists($db, 'sql_nextid'))->toBeTrue(); - }); - - it('maintains DBS statistics compatibility', function () { - $db = Database::getInstance(getTestDatabaseConfig()); - - expect($db->DBS)->toBeArray(); - expect($db->DBS)->toHaveKey('num_queries'); - expect($db->DBS)->toHaveKey('sql_timetotal'); - }); - }); -}); - -// Performance test group -describe('Database Performance', function () { - beforeEach(function () { - $this->db = Database::getInstance(getTestDatabaseConfig()); - $this->db->connection = mockConnection(); - }); - - it('maintains singleton instance creation performance') - ->group('performance') - ->repeat(1000) - ->expect(fn() => Database::getInstance()) - ->toBeInstanceOf(Database::class); - - it('executes simple queries efficiently') - ->group('performance') - ->expect(function () { - return measureExecutionTime(fn() => $this->db->sql_query('SELECT 1')); - }) - ->toBeLessThan(0.001); // 1ms -}); diff --git a/tests/Unit/Infrastructure/DependencyInjection/BootstrapTest.php b/tests/Unit/Infrastructure/DependencyInjection/BootstrapTest.php new file mode 100644 index 000000000..39041a91a --- /dev/null +++ b/tests/Unit/Infrastructure/DependencyInjection/BootstrapTest.php @@ -0,0 +1,190 @@ +createTestRootDirectory(); + + $container = Bootstrap::init($rootPath); + + expect($container)->toBeInstanceOf(Container::class); + + removeTempDirectory($rootPath); + }); + + it('returns the same container on subsequent calls', function () { + $rootPath = $this->createTestRootDirectory(); + + $container1 = Bootstrap::init($rootPath); + $container2 = Bootstrap::init($rootPath); + + expect($container1)->toBe($container2); + + removeTempDirectory($rootPath); + }); + + it('registers container instance with itself', function () { + $rootPath = $this->createTestRootDirectory(); + + $container = Bootstrap::init($rootPath); + + expect($container->get(Container::class))->toBe($container); + expect($container->get('container'))->toBe($container); + + removeTempDirectory($rootPath); + }); + + it('loads environment variables from .env file', function () { + $rootPath = $this->createTestRootDirectory(); + $this->createTestConfigFiles($rootPath, [ + 'env' => [ + 'TEST_VAR' => 'test_value', + 'APP_ENV' => 'testing', + ], + ]); + + Bootstrap::init($rootPath); + + expect($_ENV['TEST_VAR'] ?? null)->toBe('test_value'); + expect($_ENV['APP_ENV'] ?? null)->toBe('testing'); + + removeTempDirectory($rootPath); + }); + + it('loads configuration from config files', function () { + $rootPath = $this->createTestRootDirectory(); + $this->createTestConfigFiles($rootPath, [ + 'container' => [ + 'environment' => 'testing', + 'autowiring' => true, + ], + 'services' => [ + 'test.service' => 'config_value', + ], + ]); + + $container = Bootstrap::init($rootPath); + + expect($container->get('test.service'))->toBe('config_value'); + + removeTempDirectory($rootPath); + }); + + it('handles missing config files gracefully', function () { + $rootPath = $this->createTestRootDirectory(); + + // Should not throw exception even without config files + $container = Bootstrap::init($rootPath); + + expect($container)->toBeInstanceOf(Container::class); + + removeTempDirectory($rootPath); + }); + }); + + describe('getContainer() method', function () { + it('returns null when not initialized', function () { + expect(Bootstrap::getContainer())->toBeNull(); + }); + + it('returns container after initialization', function () { + $rootPath = $this->createTestRootDirectory(); + + $container = Bootstrap::init($rootPath); + + expect(Bootstrap::getContainer())->toBe($container); + + removeTempDirectory($rootPath); + }); + }); + + describe('reset() method', function () { + it('clears the container instance', function () { + $rootPath = $this->createTestRootDirectory(); + + Bootstrap::init($rootPath); + expect(Bootstrap::getContainer())->not->toBeNull(); + + Bootstrap::reset(); + expect(Bootstrap::getContainer())->toBeNull(); + + removeTempDirectory($rootPath); + }); + + it('allows re-initialization after reset', function () { + $rootPath = $this->createTestRootDirectory(); + + $container1 = Bootstrap::init($rootPath); + Bootstrap::reset(); + $container2 = Bootstrap::init($rootPath); + + expect($container1)->not->toBe($container2); + expect($container2)->toBeInstanceOf(Container::class); + + removeTempDirectory($rootPath); + }); + }); + + describe('configuration loading', function () { + it('merges configuration from multiple sources', function () { + $rootPath = $this->createTestRootDirectory(); + $this->createTestConfigFiles($rootPath, [ + 'env' => [ + 'APP_ENV' => 'production', + 'APP_DEBUG' => 'false', + ], + 'container' => [ + 'autowiring' => true, + ], + 'services' => [ + 'config.service' => 'merged_config', + ], + ]); + + $container = Bootstrap::init($rootPath, [ + 'definitions' => [ + 'runtime.service' => \DI\factory(function () { + return 'runtime_config'; + }), + ], + ]); + + expect($container->get('config.service'))->toBe('merged_config'); + expect($container->get('runtime.service'))->toBe('runtime_config'); + + removeTempDirectory($rootPath); + }); + + it('sets default environment when no .env file exists', function () { + $rootPath = $this->createTestRootDirectory(); + + $container = Bootstrap::init($rootPath); + + // Container should still be created successfully + expect($container)->toBeInstanceOf(Container::class); + + removeTempDirectory($rootPath); + }); + }); + + describe('error handling', function () { + it('handles invalid root path gracefully', function () { + // Should not throw fatal error for non-existent path + expect(function () { + Bootstrap::init('/non/existent/path'); + })->not->toThrow(Error::class); + }); + }); +}); diff --git a/tests/Unit/Infrastructure/DependencyInjection/ContainerFactoryTest.php b/tests/Unit/Infrastructure/DependencyInjection/ContainerFactoryTest.php new file mode 100644 index 000000000..ccd6e1ea8 --- /dev/null +++ b/tests/Unit/Infrastructure/DependencyInjection/ContainerFactoryTest.php @@ -0,0 +1,206 @@ +toBeInstanceOf(Container::class); + }); + + it('applies configuration correctly', function () { + $config = [ + 'environment' => 'testing', + 'autowiring' => true, + 'annotations' => false, + ]; + + $container = ContainerFactory::create($config); + expect($container)->toBeInstanceOf(Container::class); + }); + + it('loads custom definitions', function () { + $config = [ + 'definitions' => [ + 'test.service' => \DI\factory(function () { + return 'test_value'; + }), + ], + ]; + + $container = ContainerFactory::create($config); + expect($container->get('test.service'))->toBe('test_value'); + }); + + it('configures autowiring when enabled', function () { + $config = ['autowiring' => true]; + $container = ContainerFactory::create($config); + + // Should be able to autowire stdClass + expect($container->has(stdClass::class))->toBeTrue(); + }); + + it('loads definition files when specified', function () { + $tempDir = createTempDirectory(); + $definitionFile = $tempDir . '/definitions.php'; + + file_put_contents($definitionFile, ' \DI\factory(function () { + return "from_file"; + }), + ];'); + + $config = [ + 'definition_files' => [$definitionFile], + ]; + + $container = ContainerFactory::create($config); + expect($container->get('file.service'))->toBe('from_file'); + + removeTempDirectory($tempDir); + }); + + it('handles non-existent definition files gracefully', function () { + $config = [ + 'definition_files' => ['/non/existent/file.php'], + ]; + + // Should not throw an exception + $container = ContainerFactory::create($config); + expect($container)->toBeInstanceOf(Container::class); + }); + }); + + describe('service providers', function () { + it('registers and boots service providers', function () { + $providerClass = new class implements ServiceProvider { + public static bool $registered = false; + public static bool $booted = false; + + public function register(Container $container): void + { + self::$registered = true; + $container->getWrappedContainer()->set('provider.service', 'provider_value'); + } + + public function boot(Container $container): void + { + self::$booted = true; + } + }; + + $config = [ + 'providers' => [get_class($providerClass)], + ]; + + $container = ContainerFactory::create($config); + + expect($providerClass::$registered)->toBeTrue(); + expect($providerClass::$booted)->toBeTrue(); + expect($container->get('provider.service'))->toBe('provider_value'); + }); + + it('handles invalid provider classes gracefully', function () { + $config = [ + 'providers' => ['NonExistentProvider'], + ]; + + // Should not throw an exception + $container = ContainerFactory::create($config); + expect($container)->toBeInstanceOf(Container::class); + }); + + it('boots providers after all registrations', function () { + // Use a simpler approach without constructor dependencies + $testFile = sys_get_temp_dir() . '/provider_order_test.txt'; + if (file_exists($testFile)) { + unlink($testFile); + } + + $provider1Class = new class implements ServiceProvider { + public function register(Container $container): void + { + $testFile = sys_get_temp_dir() . '/provider_order_test.txt'; + file_put_contents($testFile, "register1\n", FILE_APPEND); + } + + public function boot(Container $container): void + { + $testFile = sys_get_temp_dir() . '/provider_order_test.txt'; + file_put_contents($testFile, "boot1\n", FILE_APPEND); + } + }; + + $provider2Class = new class implements ServiceProvider { + public function register(Container $container): void + { + $testFile = sys_get_temp_dir() . '/provider_order_test.txt'; + file_put_contents($testFile, "register2\n", FILE_APPEND); + } + + public function boot(Container $container): void + { + $testFile = sys_get_temp_dir() . '/provider_order_test.txt'; + file_put_contents($testFile, "boot2\n", FILE_APPEND); + } + }; + + $config = [ + 'providers' => [get_class($provider1Class), get_class($provider2Class)], + ]; + + ContainerFactory::create($config); + + // Read the order from the test file + $content = file_get_contents($testFile); + $lines = array_filter(explode("\n", trim($content))); + + // All registrations should happen before any boots + expect($lines)->toBe(['register1', 'register2', 'boot1', 'boot2']); + + // Clean up + unlink($testFile); + }); + }); + + describe('environment configuration', function () { + it('enables compilation in production', function () { + $tempDir = createTempDirectory(); + + $config = [ + 'environment' => 'production', + 'compilation_dir' => $tempDir . '/container', + 'proxies_dir' => $tempDir . '/proxies', + ]; + + $container = ContainerFactory::create($config); + expect($container)->toBeInstanceOf(Container::class); + + removeTempDirectory($tempDir); + }); + + it('skips compilation in development', function () { + $config = [ + 'environment' => 'development', + ]; + + $container = ContainerFactory::create($config); + expect($container)->toBeInstanceOf(Container::class); + }); + }); + + describe('layer definitions integration', function () { + it('loads definitions from all architectural layers', function () { + $container = ContainerFactory::create(); + + // Container should be created successfully with all layer definitions + expect($container)->toBeInstanceOf(Container::class); + + // Since most definitions are commented out, we just verify the container works + expect($container->has(stdClass::class))->toBeTrue(); + }); + }); +}); diff --git a/tests/Unit/Infrastructure/DependencyInjection/ContainerTest.php b/tests/Unit/Infrastructure/DependencyInjection/ContainerTest.php new file mode 100644 index 000000000..8ba160370 --- /dev/null +++ b/tests/Unit/Infrastructure/DependencyInjection/ContainerTest.php @@ -0,0 +1,170 @@ +container = $this->createTestContainer(); + }); + + it('implements PSR-11 ContainerInterface', function () { + expect($this->container)->toBeInstanceOf(\Psr\Container\ContainerInterface::class); + }); + + describe('get() method', function () { + it('can resolve a simple service', function () { + $container = $this->createContainerWithDefinitions([ + 'test.service' => \DI\factory(function () { + return 'test_value'; + }), + ]); + + $result = $container->get('test.service'); + expect($result)->toBe('test_value'); + }); + + it('can resolve autowired classes', function () { + $container = $this->createContainerWithDefinitions([ + 'test.class' => \DI\autowire(stdClass::class), + ]); + + $result = $container->get('test.class'); + expect($result)->toBeInstanceOf(stdClass::class); + }); + + it('throws NotFoundExceptionInterface for non-existent services', function () { + try { + $this->container->get('non.existent.service'); + fail('Expected exception to be thrown'); + } catch (Exception $e) { + expect($e)->toBeInstanceOf(NotFoundExceptionInterface::class); + expect($e->getMessage())->toContain('non.existent.service'); + } + }); + + it('returns same instance for singleton services', function () { + $container = $this->createContainerWithDefinitions([ + 'singleton.service' => \DI\factory(function () { + return new stdClass(); + }), + ]); + + $instance1 = $container->get('singleton.service'); + $instance2 = $container->get('singleton.service'); + + expect($instance1)->toBe($instance2); + }); + }); + + describe('has() method', function () { + it('returns true for existing services', function () { + $container = $this->createContainerWithDefinitions([ + 'existing.service' => \DI\factory(function () { + return 'value'; + }), + ]); + + expect($container->has('existing.service'))->toBeTrue(); + }); + + it('returns false for non-existent services', function () { + expect($this->container->has('non.existent.service'))->toBeFalse(); + }); + + it('returns true for autowirable classes', function () { + expect($this->container->has(stdClass::class))->toBeTrue(); + }); + }); + + describe('make() method', function () { + it('can make instances with parameters', function () { + $result = $this->container->make(stdClass::class); + expect($result)->toBeInstanceOf(stdClass::class); + }); + + it('creates new instances each time', function () { + $instance1 = $this->container->make(stdClass::class); + $instance2 = $this->container->make(stdClass::class); + + expect($instance1)->not->toBe($instance2); + }); + }); + + describe('call() method', function () { + it('can call closures with dependency injection', function () { + $result = $this->container->call(function (stdClass $class) { + return get_class($class); + }); + + expect($result)->toBe('stdClass'); + }); + + it('can call methods with parameters', function () { + $service = new class { + public function test(string $param): string + { + return "Hello $param"; + } + }; + + $result = $this->container->call([$service, 'test'], ['param' => 'World']); + expect($result)->toBe('Hello World'); + }); + }); + + describe('injectOn() method', function () { + it('returns the object after injection', function () { + $object = new stdClass(); + + $result = $this->container->injectOn($object); + expect($result)->toBe($object); + }); + }); + + describe('getWrappedContainer() method', function () { + it('returns the underlying PHP-DI container', function () { + $wrapped = $this->container->getWrappedContainer(); + expect($wrapped)->toBeInstanceOf(\DI\Container::class); + }); + + it('allows direct access to PHP-DI functionality', function () { + $wrapped = $this->container->getWrappedContainer(); + $wrapped->set('direct.service', 'direct_value'); + + expect($this->container->get('direct.service'))->toBe('direct_value'); + }); + }); + + describe('error handling', function () { + it('provides meaningful error messages for missing services', function () { + try { + $this->container->get('missing.service'); + fail('Expected NotFoundExceptionInterface to be thrown'); + } catch (NotFoundExceptionInterface $e) { + expect($e->getMessage())->toContain('missing.service'); + } + }); + + it('handles circular dependencies gracefully', function () { + $container = $this->createContainerWithDefinitions([ + 'service.a' => \DI\factory(function (\Psr\Container\ContainerInterface $c) { + return $c->get('service.b'); + }), + 'service.b' => \DI\factory(function (\Psr\Container\ContainerInterface $c) { + return $c->get('service.a'); + }), + ]); + + try { + $container->get('service.a'); + fail('Expected circular dependency exception'); + } catch (Exception $e) { + expect($e)->toBeInstanceOf(ContainerExceptionInterface::class); + expect($e->getMessage())->toContain('Circular dependency'); + } + }); + }); +}); diff --git a/tests/Unit/Infrastructure/DependencyInjection/Definitions/ApplicationDefinitionsTest.php b/tests/Unit/Infrastructure/DependencyInjection/Definitions/ApplicationDefinitionsTest.php new file mode 100644 index 000000000..8d686cea1 --- /dev/null +++ b/tests/Unit/Infrastructure/DependencyInjection/Definitions/ApplicationDefinitionsTest.php @@ -0,0 +1,100 @@ +toBeArray(); + }); + + it('returns empty array when no application services are implemented yet', function () { + $definitions = ApplicationDefinitions::getDefinitions(); + + // Since we're in Phase 1 and application services aren't implemented yet, + // the definitions should be empty (all examples are commented out) + expect($definitions)->toBe([]); + }); + + it('follows application layer principles', function () { + // Application layer should orchestrate domain objects + // This test verifies the structure is ready for future application services + + $definitions = ApplicationDefinitions::getDefinitions(); + + // Should be an array (even if empty) + expect($definitions)->toBeArray(); + + // When application services are added, they should follow these principles: + // - Command and Query handlers + // - Application services that orchestrate domain logic + // - Event dispatchers + // - No direct infrastructure concerns + }); + + it('can be safely called multiple times', function () { + $definitions1 = ApplicationDefinitions::getDefinitions(); + $definitions2 = ApplicationDefinitions::getDefinitions(); + + expect($definitions1)->toBe($definitions2); + }); + + it('is prepared for future command/query handlers', function () { + // This test documents the intended structure for Phase 3 implementation + + $definitions = ApplicationDefinitions::getDefinitions(); + expect($definitions)->toBeArray(); + + // Future command/query handlers will be registered like: + // 'TorrentPier\Application\User\Handler\RegisterUserHandler' => DI\autowire(), + // 'CommandBusInterface' => DI\factory(function (ContainerInterface $c) { + // return new CommandBus($c); + // }), + + // For now, verify the method works without breaking + expect(count($definitions))->toBeGreaterThanOrEqual(0); + }); + }); + + describe('architectural compliance', function () { + it('follows hexagonal architecture principles', function () { + // Application layer should orchestrate domain objects without infrastructure concerns + + $definitions = ApplicationDefinitions::getDefinitions(); + + // Application definitions should focus on: + // 1. Command and Query handlers + // 2. Application services + // 3. Event dispatchers + // 4. Use case orchestration + + expect($definitions)->toBeArray(); + }); + + it('supports CQRS pattern', function () { + // Application layer should separate commands and queries + // This test ensures the structure supports CQRS implementation + + $definitions = ApplicationDefinitions::getDefinitions(); + + // Future implementation will separate: + // - Command handlers (write operations) + // - Query handlers (read operations) + // - Command and Query buses + + expect($definitions)->toBeArray(); + }); + + it('prepares for event-driven architecture', function () { + // Application layer should support domain events + + $definitions = ApplicationDefinitions::getDefinitions(); + + // Future event dispatcher will be registered here + // 'EventDispatcherInterface' => DI\factory(...) + + expect($definitions)->toBeArray(); + }); + }); +}); diff --git a/tests/Unit/Infrastructure/DependencyInjection/Definitions/DomainDefinitionsTest.php b/tests/Unit/Infrastructure/DependencyInjection/Definitions/DomainDefinitionsTest.php new file mode 100644 index 000000000..787083024 --- /dev/null +++ b/tests/Unit/Infrastructure/DependencyInjection/Definitions/DomainDefinitionsTest.php @@ -0,0 +1,84 @@ +toBeArray(); + }); + + it('returns empty array when no domain services are implemented yet', function () { + $definitions = DomainDefinitions::getDefinitions(); + + // Since we're in Phase 1 and domain services aren't implemented yet, + // the definitions should be empty (all examples are commented out) + expect($definitions)->toBe([]); + }); + + it('follows domain layer principles', function () { + // Domain definitions should not contain infrastructure dependencies + // This test verifies the structure is ready for future domain services + + $definitions = DomainDefinitions::getDefinitions(); + + // Should be an array (even if empty) + expect($definitions)->toBeArray(); + + // When domain services are added, they should follow these principles: + // - No framework dependencies + // - Repository interfaces mapped to implementations + // - Pure business logic services + }); + + it('can be safely called multiple times', function () { + $definitions1 = DomainDefinitions::getDefinitions(); + $definitions2 = DomainDefinitions::getDefinitions(); + + expect($definitions1)->toBe($definitions2); + }); + + it('is prepared for future repository interface mappings', function () { + // This test documents the intended structure for Phase 2 implementation + + $definitions = DomainDefinitions::getDefinitions(); + expect($definitions)->toBeArray(); + + // Future repository interfaces will be mapped like: + // 'TorrentPier\Domain\User\Repository\UserRepositoryInterface' => + // DI\factory(function (ContainerInterface $c) { + // return $c->get('TorrentPier\Infrastructure\Persistence\Repository\UserRepository'); + // }), + + // For now, verify the method works without breaking + expect(count($definitions))->toBeGreaterThanOrEqual(0); + }); + }); + + describe('architectural compliance', function () { + it('follows hexagonal architecture principles', function () { + // Domain layer should have no infrastructure dependencies + // This test ensures the definition structure is correct + + $definitions = DomainDefinitions::getDefinitions(); + + // Domain definitions should focus on: + // 1. Repository interface mappings + // 2. Domain service factories + // 3. No framework dependencies + + expect($definitions)->toBeArray(); + }); + + it('supports dependency injection inversion', function () { + // Domain interfaces should be mapped to infrastructure implementations + // following the dependency inversion principle + + $definitions = DomainDefinitions::getDefinitions(); + + // Even though empty now, the structure supports proper DI mapping + expect($definitions)->toBeArray(); + }); + }); +}); diff --git a/tests/Unit/Infrastructure/DependencyInjection/Definitions/InfrastructureDefinitionsTest.php b/tests/Unit/Infrastructure/DependencyInjection/Definitions/InfrastructureDefinitionsTest.php new file mode 100644 index 000000000..64ba55cee --- /dev/null +++ b/tests/Unit/Infrastructure/DependencyInjection/Definitions/InfrastructureDefinitionsTest.php @@ -0,0 +1,159 @@ +toBeArray(); + }); + + it('accepts configuration parameter', function () { + $config = ['test' => 'value']; + $definitions = InfrastructureDefinitions::getDefinitions($config); + expect($definitions)->toBeArray(); + }); + + it('returns empty array when no infrastructure services are implemented yet', function () { + $definitions = InfrastructureDefinitions::getDefinitions(); + + // Since we're in Phase 1 and infrastructure services aren't implemented yet, + // the definitions should be empty (all examples are commented out) + expect($definitions)->toBe([]); + }); + + it('follows infrastructure layer principles', function () { + // Infrastructure layer should handle external concerns + // This test verifies the structure is ready for future infrastructure services + + $definitions = InfrastructureDefinitions::getDefinitions(); + + // Should be an array (even if empty) + expect($definitions)->toBeArray(); + + // When infrastructure services are added, they should follow these principles: + // - Database connections and repositories + // - Cache implementations + // - External service adapters + // - File storage systems + }); + + it('can be safely called multiple times', function () { + $definitions1 = InfrastructureDefinitions::getDefinitions(); + $definitions2 = InfrastructureDefinitions::getDefinitions(); + + expect($definitions1)->toBe($definitions2); + }); + + it('can handle different configurations', function () { + $config1 = ['database' => ['host' => 'localhost']]; + $config2 = ['cache' => ['driver' => 'redis']]; + + $definitions1 = InfrastructureDefinitions::getDefinitions($config1); + $definitions2 = InfrastructureDefinitions::getDefinitions($config2); + + // Should handle different configs without breaking + expect($definitions1)->toBeArray(); + expect($definitions2)->toBeArray(); + }); + + it('is prepared for future database services', function () { + // This test documents the intended structure for Phase 4 implementation + + $definitions = InfrastructureDefinitions::getDefinitions([ + 'database' => [ + 'host' => '127.0.0.1', + 'port' => 3306, + 'database' => 'tp', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8mb4', + ], + ]); + + expect($definitions)->toBeArray(); + + // Future database services will be registered like: + // 'database.connection.default' => DI\factory(function () use ($config) { ... }), + // Connection::class => DI\get('database.connection.default'), + + // For now, verify the method works without breaking + expect(count($definitions))->toBeGreaterThanOrEqual(0); + }); + + it('is prepared for future cache services', function () { + $definitions = InfrastructureDefinitions::getDefinitions([ + 'cache' => [ + 'driver' => 'file', + 'file' => ['path' => '/tmp/cache'], + ], + ]); + + expect($definitions)->toBeArray(); + + // Future cache services will be registered like: + // 'cache.storage' => DI\factory(function () use ($config) { ... }), + // 'cache.factory' => DI\factory(function (ContainerInterface $c) { ... }), + }); + }); + + describe('architectural compliance', function () { + it('follows hexagonal architecture principles', function () { + // Infrastructure layer should handle external concerns and adapters + + $definitions = InfrastructureDefinitions::getDefinitions(); + + // Infrastructure definitions should focus on: + // 1. Database connections and persistence + // 2. Cache implementations + // 3. External service adapters + // 4. File storage systems + // 5. Third-party integrations + + expect($definitions)->toBeArray(); + }); + + it('supports dependency inversion', function () { + // Infrastructure should implement domain interfaces + + $definitions = InfrastructureDefinitions::getDefinitions(); + + // Future repository implementations will be registered here: + // 'TorrentPier\Infrastructure\Persistence\Repository\UserRepository' => DI\autowire() + // ->constructorParameter('connection', DI\get('database.connection.default')) + + expect($definitions)->toBeArray(); + }); + + it('handles configuration-based service creation', function () { + // Infrastructure services should be configurable + + $config = [ + 'database' => ['driver' => 'mysql'], + 'cache' => ['driver' => 'redis'], + 'storage' => ['driver' => 's3'], + ]; + + $definitions = InfrastructureDefinitions::getDefinitions($config); + + // Should handle configuration without breaking + expect($definitions)->toBeArray(); + }); + + it('prepares for multiple database connections', function () { + $config = [ + 'database' => [ + 'default' => 'mysql', + 'connections' => [ + 'mysql' => ['driver' => 'mysql'], + 'sqlite' => ['driver' => 'sqlite'], + ], + ], + ]; + + $definitions = InfrastructureDefinitions::getDefinitions($config); + expect($definitions)->toBeArray(); + }); + }); +}); diff --git a/tests/Unit/Infrastructure/DependencyInjection/Definitions/PresentationDefinitionsTest.php b/tests/Unit/Infrastructure/DependencyInjection/Definitions/PresentationDefinitionsTest.php new file mode 100644 index 000000000..b751b70ba --- /dev/null +++ b/tests/Unit/Infrastructure/DependencyInjection/Definitions/PresentationDefinitionsTest.php @@ -0,0 +1,147 @@ +toBeArray(); + }); + + it('returns empty array when no presentation services are implemented yet', function () { + $definitions = PresentationDefinitions::getDefinitions(); + + // Since we're in Phase 1 and presentation services aren't implemented yet, + // the definitions should be empty (all examples are commented out) + expect($definitions)->toBe([]); + }); + + it('follows presentation layer principles', function () { + // Presentation layer should handle user interface concerns + // This test verifies the structure is ready for future presentation services + + $definitions = PresentationDefinitions::getDefinitions(); + + // Should be an array (even if empty) + expect($definitions)->toBeArray(); + + // When presentation services are added, they should follow these principles: + // - HTTP controllers for web and API interfaces + // - CLI commands for console operations + // - Middleware for request/response processing + // - Response transformers for output formatting + }); + + it('can be safely called multiple times', function () { + $definitions1 = PresentationDefinitions::getDefinitions(); + $definitions2 = PresentationDefinitions::getDefinitions(); + + expect($definitions1)->toBe($definitions2); + }); + + it('is prepared for future HTTP controllers', function () { + // This test documents the intended structure for Phase 5 implementation + + $definitions = PresentationDefinitions::getDefinitions(); + expect($definitions)->toBeArray(); + + // Future HTTP controllers will be registered like: + // 'TorrentPier\Presentation\Http\Controllers\Web\HomeController' => DI\autowire(), + // 'TorrentPier\Presentation\Http\Controllers\Api\UserController' => DI\autowire(), + // 'TorrentPier\Presentation\Http\Controllers\Admin\DashboardController' => DI\autowire(), + + // For now, verify the method works without breaking + expect(count($definitions))->toBeGreaterThanOrEqual(0); + }); + + it('is prepared for future CLI commands', function () { + $definitions = PresentationDefinitions::getDefinitions(); + expect($definitions)->toBeArray(); + + // Future CLI commands will be registered like: + // 'TorrentPier\Presentation\Cli\Commands\CacheCommand' => DI\autowire(), + // 'TorrentPier\Presentation\Cli\Commands\MigrateCommand' => DI\autowire(), + }); + + it('is prepared for future middleware', function () { + $definitions = PresentationDefinitions::getDefinitions(); + expect($definitions)->toBeArray(); + + // Future middleware will be registered like: + // 'AuthenticationMiddleware' => DI\autowire('TorrentPier\Presentation\Http\Middleware\AuthenticationMiddleware'), + // 'CorsMiddleware' => DI\autowire('TorrentPier\Presentation\Http\Middleware\CorsMiddleware'), + }); + }); + + describe('architectural compliance', function () { + it('follows hexagonal architecture principles', function () { + // Presentation layer should handle user interface and external interfaces + + $definitions = PresentationDefinitions::getDefinitions(); + + // Presentation definitions should focus on: + // 1. HTTP controllers (Web, API, Admin) + // 2. CLI commands + // 3. Middleware for request processing + // 4. Response transformers + // 5. Input validation and output formatting + + expect($definitions)->toBeArray(); + }); + + it('supports multiple interface types', function () { + // Presentation layer should support web, API, and CLI interfaces + + $definitions = PresentationDefinitions::getDefinitions(); + + // Future implementation will include: + // - Web controllers for HTML responses + // - API controllers for JSON responses + // - Admin controllers for administrative interface + // - CLI commands for console operations + + expect($definitions)->toBeArray(); + }); + + it('prepares for middleware stack', function () { + // Presentation layer should support request/response middleware + + $definitions = PresentationDefinitions::getDefinitions(); + + // Future middleware will handle: + // - Authentication and authorization + // - CORS headers + // - Rate limiting + // - Request validation + // - Response transformation + + expect($definitions)->toBeArray(); + }); + + it('supports dependency injection for controllers', function () { + // Controllers should have their dependencies injected + + $definitions = PresentationDefinitions::getDefinitions(); + + // Future controllers will be autowired with dependencies: + // - Application services (command/query handlers) + // - Request validators + // - Response transformers + + expect($definitions)->toBeArray(); + }); + + it('prepares for different response formats', function () { + // Presentation layer should support multiple response formats + + $definitions = PresentationDefinitions::getDefinitions(); + + // Future response transformers: + // 'JsonResponseTransformer' => DI\autowire(...), + // 'HtmlResponseTransformer' => DI\autowire(...), + + expect($definitions)->toBeArray(); + }); + }); +}); diff --git a/tests/Unit/Infrastructure/DependencyInjection/ServiceProviderTest.php b/tests/Unit/Infrastructure/DependencyInjection/ServiceProviderTest.php new file mode 100644 index 000000000..69c7c68ac --- /dev/null +++ b/tests/Unit/Infrastructure/DependencyInjection/ServiceProviderTest.php @@ -0,0 +1,144 @@ +isInterface())->toBeTrue(); + expect($reflection->hasMethod('register'))->toBeTrue(); + expect($reflection->hasMethod('boot'))->toBeTrue(); + }); + + it('register method has correct signature', function () { + $reflection = new ReflectionClass(ServiceProvider::class); + $method = $reflection->getMethod('register'); + + expect($method->isPublic())->toBeTrue(); + expect($method->getParameters())->toHaveCount(1); + expect($method->getParameters()[0]->getType()?->getName())->toBe(Container::class); + expect($method->getReturnType()?->getName())->toBe('void'); + }); + + it('boot method has correct signature', function () { + $reflection = new ReflectionClass(ServiceProvider::class); + $method = $reflection->getMethod('boot'); + + expect($method->isPublic())->toBeTrue(); + expect($method->getParameters())->toHaveCount(1); + expect($method->getParameters()[0]->getType()?->getName())->toBe(Container::class); + expect($method->getReturnType()?->getName())->toBe('void'); + }); +}); + +describe('ServiceProvider implementation examples', function () { + it('can implement a basic service provider', function () { + $provider = new class implements ServiceProvider { + public function register(Container $container): void + { + $container->getWrappedContainer()->set('example.service', 'registered'); + } + + public function boot(Container $container): void + { + // Boot logic here + } + }; + + $container = $this->createTestContainer(); + + $provider->register($container); + + expect($container->get('example.service'))->toBe('registered'); + }); + + it('can implement a provider with complex services', function () { + $provider = new class implements ServiceProvider { + public function register(Container $container): void + { + $container->getWrappedContainer()->set('complex.service', \DI\factory(function () { + return new class { + public function getValue(): string + { + return 'complex_value'; + } + }; + })); + } + + public function boot(Container $container): void + { + // Could perform additional setup here + $service = $container->get('complex.service'); + // Setup complete + } + }; + + $container = $this->createTestContainer(); + + $provider->register($container); + $provider->boot($container); + + $service = $container->get('complex.service'); + expect($service->getValue())->toBe('complex_value'); + }); + + it('can implement a provider that registers multiple services', function () { + $provider = new class implements ServiceProvider { + public function register(Container $container): void + { + $wrapped = $container->getWrappedContainer(); + + $wrapped->set('service.a', 'value_a'); + $wrapped->set('service.b', 'value_b'); + $wrapped->set('service.c', \DI\factory(function () { + return 'value_c'; + })); + } + + public function boot(Container $container): void + { + // Boot all registered services + } + }; + + $container = $this->createTestContainer(); + + $provider->register($container); + $provider->boot($container); + + expect($container->get('service.a'))->toBe('value_a'); + expect($container->get('service.b'))->toBe('value_b'); + expect($container->get('service.c'))->toBe('value_c'); + }); + + it('boot method can access services registered by register method', function () { + $bootedServices = []; + + $provider = new class($bootedServices) implements ServiceProvider { + public function __construct(private array &$bootedServices) + { + } + + public function register(Container $container): void + { + $container->getWrappedContainer()->set('bootable.service', 'registered_value'); + } + + public function boot(Container $container): void + { + $value = $container->get('bootable.service'); + $this->bootedServices[] = $value; + } + }; + + $container = $this->createTestContainer(); + + $provider->register($container); + $provider->boot($container); + + expect($bootedServices)->toBe(['registered_value']); + }); +}); diff --git a/tests/Unit/Legacy/TemplateGracefulFallbackTest.php b/tests/Unit/Legacy/TemplateGracefulFallbackTest.php deleted file mode 100644 index 9f3df1466..000000000 --- a/tests/Unit/Legacy/TemplateGracefulFallbackTest.php +++ /dev/null @@ -1,501 +0,0 @@ -', '|', ' ']; - return str_replace($s, '_', trim($fname)); - } - } - - if (!function_exists('config')) { - function config() - { - return new class { - public function get($key, $default = null) - { - // Return sensible defaults for template configuration - return match ($key) { - 'xs_use_cache' => 0, - 'default_lang' => 'en', - default => $default - }; - } - }; - } - } - - // Create a temporary directory for templates and cache - $this->tempDir = createTempDirectory(); - $this->templateDir = $this->tempDir . '/templates'; - $this->cacheDir = $this->tempDir . '/cache'; - - mkdir($this->templateDir, 0755, true); - mkdir($this->cacheDir, 0755, true); - - // Set up global language array for testing - global $lang; - $lang = [ - 'EXISTING_KEY' => 'This key exists', - 'ANOTHER_KEY' => 'Another existing key' - ]; - - // Create template instance - $this->template = new Template($this->templateDir); - $this->template->cachedir = $this->cacheDir . '/'; - $this->template->use_cache = 0; // Disable caching for tests -}); - -afterEach(function () { - // Clean up - if (isset($this->tempDir)) { - removeTempDirectory($this->tempDir); - } - - // Reset global state - resetGlobalState(); -}); - -/** - * Execute a compiled template and return its output - * - * @param string $compiled The compiled template code - * @param array $variables Optional variables to set in scope (V array) - * @param array $additionalVars Optional additional variables to set in scope - * @return string The template output - */ -function executeTemplate(string $compiled, array $variables = [], array $additionalVars = []): string -{ - ob_start(); - global $lang; - $L = &$lang; - $V = $variables; - - // Set any additional variables in scope - foreach ($additionalVars as $name => $value) { - $$name = $value; - } - - // SECURITY NOTE: eval() is used intentionally here to execute compiled template code - // within a controlled test environment. While eval() poses security risks in production, - // its use is justified in this specific unit test scenario because: - // 1. We're testing the legacy template compilation system that generates PHP code - // 2. The input is controlled and comes from our own template compiler - // 3. This runs in an isolated test environment, not production - // 4. Testing the actual execution is necessary to verify template output correctness - // Future maintainers: Use extreme caution with eval() and avoid it in production code - eval('?>' . $compiled); - return ob_get_clean(); -} - -describe('Template Text Compilation - Graceful Fallback', function () { - - it('shows missing language variables as original syntax', function () { - $template = '{L_MISSING_KEY}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled); - - expect($output)->toBe('L_MISSING_KEY'); - }); - - it('shows existing language variables correctly', function () { - $template = '{L_EXISTING_KEY}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled); - - expect($output)->toBe('This key exists'); - }); - - it('shows missing regular variables as original syntax', function () { - $template = '{MISSING_VAR}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled); - - expect($output)->toBe(''); - }); - - it('shows existing regular variables correctly', function () { - $template = '{EXISTING_VAR}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled, ['EXISTING_VAR' => 'This variable exists']); - - expect($output)->toBe('This variable exists'); - }); - - it('shows missing constants as original syntax', function () { - $template = '{#MISSING_CONSTANT#}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled); - - expect($output)->toBe(''); - }); - - it('shows existing constants correctly', function () { - // Define a test constant - if (!defined('TEST_CONSTANT')) { - define('TEST_CONSTANT', 'This constant exists'); - } - - $template = '{#TEST_CONSTANT#}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled); - - expect($output)->toBe('This constant exists'); - }); - - it('handles mixed existing and missing variables correctly', function () { - $template = '{L_EXISTING_KEY} - {L_MISSING_KEY} - {EXISTING_VAR} - {MISSING_VAR}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled, ['EXISTING_VAR' => 'Variable exists']); - - expect($output)->toBe('This key exists - L_MISSING_KEY - Variable exists - '); - }); - - it('handles PHP variables correctly without fallback', function () { - $template = '{$test_var}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled, [], ['test_var' => 'PHP variable value']); - - expect($output)->toBe('PHP variable value'); - }); - - it('handles undefined PHP variables gracefully', function () { - $template = '{$undefined_var}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled); - - // PHP variables that don't exist should show empty string (original behavior) - expect($output)->toBe(''); - }); - -}); - -describe('Template Block Variable Fallback', function () { - - it('shows missing block variables as empty string', function () { - $namespace = 'testblock'; - $varname = 'MISSING_VAR'; - - $result = $this->template->generate_block_varref($namespace . '.', $varname); - - // Block variables should show empty string when missing, not the variable name - $expectedFormat = ""; - expect($result)->toBe($expectedFormat); - }); - - it('generates correct PHP code for block variable fallback', function () { - $namespace = 'news'; - $varname = 'TITLE'; - - $result = $this->template->generate_block_varref($namespace . '.', $varname); - - // Block variables should show empty string when missing, not the variable name - $expectedFormat = ""; - expect($result)->toBe($expectedFormat); - }); - -}); - -describe('Compiled Code Verification', function () { - - it('compiles language variables with proper fallback code', function () { - $template = '{L_MISSING_KEY}'; - $compiled = $this->template->_compile_text($template); - - // Verify the compiled PHP code contains the expected fallback logic - expect($compiled)->toContain("isset(\$L['MISSING_KEY'])"); - expect($compiled)->toContain("'L_MISSING_KEY'"); - }); - - it('compiles regular variables with proper fallback code', function () { - $template = '{MISSING_VAR}'; - $compiled = $this->template->_compile_text($template); - - // Verify the compiled PHP code contains the expected fallback logic - expect($compiled)->toContain("isset(\$V['MISSING_VAR'])"); - expect($compiled)->toContain("''"); - }); - - it('compiles constants with proper fallback code', function () { - $template = '{#MISSING_CONSTANT#}'; - $compiled = $this->template->_compile_text($template); - - // Verify the compiled PHP code contains the expected fallback logic - expect($compiled)->toContain("defined('MISSING_CONSTANT')"); - expect($compiled)->toContain("''"); - }); - -}); - -describe('Real-world Example - Admin Migrations', function () { - - it('handles the original L_MIGRATIONS_FILE error gracefully', function () { - // The exact template that was causing the error - $template = ''; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled); - - // Should show the fallback without braces instead of throwing an error - expect($output)->toContain('L_MIGRATIONS_FILE'); - expect($output)->toContain('
explain data
{L_MIGRATIONS_FILE}template->_compile_text($template); - $output = executeTemplate($compiled); - - // Empty braces should remain as literal text - expect($output)->toBe('{}'); - }); - - it('handles variables with special characters in names', function () { - $template = '{VAR_WITH_UNDERSCORES} {VAR-WITH-DASHES} {VAR123NUMBERS}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled, [ - 'VAR_WITH_UNDERSCORES' => 'underscore value', - 'VAR123NUMBERS' => 'number value' - ]); - - // Verify the compiled code contains proper fallback logic for special chars - expect($compiled)->toContain("isset(\$V['VAR_WITH_UNDERSCORES'])"); - expect($compiled)->toContain("isset(\$V['VAR123NUMBERS'])"); - - // Underscores and numbers should work, dashes might not be valid variable names - expect($output)->toContain('underscore value'); - expect($output)->toContain('number value'); - }); - - it('handles HTML entities and special characters in template content', function () { - $template = '
& {TEST_VAR} <script>
'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled, ['TEST_VAR' => 'safe content']); - - // HTML entities should be preserved, variable should be substituted - expect($output)->toBe('
& safe content <script>
'); - - // Verify fallback logic is present - expect($compiled)->toContain("isset(\$V['TEST_VAR'])"); - }); - - it('handles quotes and escaping in variable values', function () { - $template = 'Value: {QUOTED_VAR}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled, [ - 'QUOTED_VAR' => 'Contains "quotes" and \'apostrophes\'' - ]); - - expect($output)->toBe('Value: Contains "quotes" and \'apostrophes\''); - expect($compiled)->toContain("isset(\$V['QUOTED_VAR'])"); - }); - - it('handles very long variable names', function () { - $longVarName = 'VERY_LONG_VARIABLE_NAME_THAT_TESTS_BUFFER_LIMITS_AND_PARSING_' . str_repeat('X', 100); - $template = '{' . $longVarName . '}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled, [$longVarName => 'long var value']); - - expect($output)->toBe('long var value'); - expect($compiled)->toContain("isset(\$V['$longVarName'])"); - }); - - it('handles nested braces and malformed syntax', function () { - $template = '{{NESTED}} {UNCLOSED {NORMAL_VAR} }EXTRA}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled, ['NORMAL_VAR' => 'works']); - - // Should handle the valid variable and leave malformed parts as literals - expect($output)->toContain('works'); - expect($compiled)->toContain("isset(\$V['NORMAL_VAR'])"); - }); - - it('handles empty string values with proper fallback', function () { - $template = 'Before:{EMPTY_VAR}:After'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled, ['EMPTY_VAR' => '']); - - expect($output)->toBe('Before::After'); - expect($compiled)->toContain("isset(\$V['EMPTY_VAR'])"); - }); - - it('handles null and false values correctly', function () { - $template = 'Null:{NULL_VAR} False:{FALSE_VAR} Zero:{ZERO_VAR}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled, [ - 'NULL_VAR' => null, - 'FALSE_VAR' => false, - 'ZERO_VAR' => 0 - ]); - - // PHP's string conversion: null='', false='', 0='0' - expect($output)->toBe('Null: False: Zero:0'); - expect($compiled)->toContain("isset(\$V['NULL_VAR'])"); - expect($compiled)->toContain("isset(\$V['FALSE_VAR'])"); - expect($compiled)->toContain("isset(\$V['ZERO_VAR'])"); - }); - - it('handles whitespace around variable names', function () { - $template = '{ SPACED_VAR } {NORMAL_VAR}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled, [ - 'SPACED_VAR' => 'should not work', - 'NORMAL_VAR' => 'should work' - ]); - - // Spaces inside braces should make it not match as a variable pattern - expect($output)->toContain('should work'); - expect($compiled)->toContain("isset(\$V['NORMAL_VAR'])"); - }); - - it('handles multiple consecutive variables', function () { - $template = '{VAR1}{VAR2}{VAR3}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled, [ - 'VAR1' => 'A', - 'VAR2' => 'B', - 'VAR3' => 'C' - ]); - - expect($output)->toBe('ABC'); - expect($compiled)->toContain("isset(\$V['VAR1'])"); - expect($compiled)->toContain("isset(\$V['VAR2'])"); - expect($compiled)->toContain("isset(\$V['VAR3'])"); - }); - - it('handles variables with numeric suffixes', function () { - $template = '{VAR1} {VAR2} {VAR10} {VAR100}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled, [ - 'VAR1' => 'one', - 'VAR2' => 'two', - 'VAR10' => 'ten', - 'VAR100' => 'hundred' - ]); - - expect($output)->toBe('one two ten hundred'); - expect($compiled)->toContain("isset(\$V['VAR1'])"); - expect($compiled)->toContain("isset(\$V['VAR2'])"); - expect($compiled)->toContain("isset(\$V['VAR10'])"); - expect($compiled)->toContain("isset(\$V['VAR100'])"); - }); - - it('handles mixed case sensitivity correctly', function () { - $template = '{lowercase} {UPPERCASE} {MixedCase}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled, [ - 'lowercase' => 'lower', - 'UPPERCASE' => 'upper', - 'MixedCase' => 'mixed' - ]); - - expect($output)->toBe('lower upper mixed'); - expect($compiled)->toContain("isset(\$V['lowercase'])"); - expect($compiled)->toContain("isset(\$V['UPPERCASE'])"); - expect($compiled)->toContain("isset(\$V['MixedCase'])"); - }); - - it('handles language variables with special prefixes', function () { - global $lang; - $originalLang = $lang; - - // Add some special test language variables - $lang['TEST_SPECIAL_CHARS'] = 'Special: &<>"\''; - $lang['TEST_UNICODE'] = 'Unicode: ñáéíóú'; - - $template = '{L_TEST_SPECIAL_CHARS} | {L_TEST_UNICODE} | {L_MISSING_SPECIAL}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled); - - expect($output)->toBe('Special: &<>"\' | Unicode: ñáéíóú | L_MISSING_SPECIAL'); - expect($compiled)->toContain("isset(\$L['TEST_SPECIAL_CHARS'])"); - expect($compiled)->toContain("isset(\$L['TEST_UNICODE'])"); - expect($compiled)->toContain("'L_MISSING_SPECIAL'"); - - // Restore original language array - $lang = $originalLang; - }); - - it('handles constants with edge case names', function () { - // Define some test constants with edge case names - if (!defined('TEST_CONST_123')) { - define('TEST_CONST_123', 'numeric suffix'); - } - if (!defined('TEST_CONST_UNDERSCORE_')) { - define('TEST_CONST_UNDERSCORE_', 'trailing underscore'); - } - - $template = '{#TEST_CONST_123#} {#TEST_CONST_UNDERSCORE_#} {#UNDEFINED_CONST_EDGE#}'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled); - - expect($output)->toBe('numeric suffix trailing underscore '); - expect($compiled)->toContain("defined('TEST_CONST_123')"); - expect($compiled)->toContain("defined('TEST_CONST_UNDERSCORE_')"); - expect($compiled)->toContain("defined('UNDEFINED_CONST_EDGE')"); - }); - - it('handles complex nested HTML with variables', function () { - $template = '
{CELL1}{CELL2}
'; - $compiled = $this->template->_compile_text($template); - $output = executeTemplate($compiled, [ - 'CELL1' => 'First Cell', - 'CSS_CLASS' => 'highlight', - 'CELL2' => 'Second Cell' - ]); - - expect($output)->toBe('
First CellSecond Cell
'); - expect($compiled)->toContain("isset(\$V['CELL1'])"); - expect($compiled)->toContain("isset(\$V['CSS_CLASS'])"); - expect($compiled)->toContain("isset(\$V['CELL2'])"); - }); - -});