diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dbb8405b..fa51d3074 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,20 +10,21 @@ The format is based on Keep a Changelog. - Added `Request::expectsJson()`: true when the `Accept` header lists a JSON media type (`application/json` or a `+json` type, parameters and case ignored), or for an AJAX request that accepts any content type (#581) - Added lifecycle events for the web and console runtime: the runtime stages declare `web.preflight`, `web.route`, `web.not_found`, `web.prepare`, `web.dispatch`, `web.send` and `console.command`, each with `.before` and `.after`, and the `Pipeline` dispatches them with the runtime context as a named `context` payload. A stage that does not run dispatches nothing, a stage that throws dispatches no `.after`, and the closing `web.send` events also follow an OPTIONS or not found response. Boot stages declare no events. `console.command.after` fires only with a Symfony application that does not exit after the command (#536, #538) - Added adapter-based Lang provider support with `DeepL` and `Google Translate` adapters plus shared remote request/caching infrastructure (#533) +- Added the `is_web_app()` and `is_console_app()` helpers, which tell whether the running application is the web app or the console (#593) ### Changed - **BREAKING:** Took the debugbar out of the app flow and out of the packages that feed it (#539): no package outside `Quantum\Debugger` references it. Each package keeps its own state, and `Debugger::render()` reads it when the toolbar renders: every tab shows the entries written with `debugbar()->addToStoreCell()`, followed by the event listeners, the matched route and the rendered view, the query log, the log messages and the mailer transport log as they are at that point. The debugger store keeps only those direct writes, and the debugbar builds no database connection or mailer the request did not use. - Removed `InitDebuggerStage` (`Quantum\App\Stages\InitDebuggerStage` in 3.0.3) from the web boot. The debugger store creates a tab on its first write; code that wants the tabs up front can call `debugbar()->initStore()`. - The `events` tab lists the listeners registered when the toolbar renders, so listeners added later in the request, for example in a controller, are listed too. - `View::__construct()` no longer takes a `Debugger`: it is `(Renderer, AssetManager, ViewCache)`. Code that gets the view through `view()` or `ViewFactory` is unaffected. Added `View::getViewFile()`, the last view rendered with `render()`. - - `MessageAdapter`, the logger in debug mode, keeps its messages in memory (`getMessages()`) instead of writing them into the debugger store, and no longer reads `$context['tab']`: a message logged with a tab is listed in the `messages` tab, and `debugbar()->getStoreCell('messages')` returns only direct writes. - - `MailerInterface` gains `getTransportLog()`, implemented in `MailerTrait`: the errors of failed sends and, for SMTP in debug mode, the exchange with the server. A custom adapter that implements the interface without the trait must add it. A failed send is still logged as a warning. -- **BREAKING:** Reworked the app boot and runtime into pipelines of stages that the adapters run in explicit steps (#537, #538, #575, #576, #579, #583, #589): + - `MessageAdapter`, the logger in debug mode, keeps its messages in memory (`getMessages()`) instead of writing them into the debugger store, and no longer reads `$context['tab']`: a message logged with a tab is listed in the `messages` tab, and `debugbar()->getStoreCell('messages')` returns only direct writes. It keeps the last `logging.message.limit` messages (default `1000`, `0` keeps every message) and drops the oldest (#593). + - `MailerInterface` gains `getTransportLog()`, implemented in `MailerTrait`: the errors of failed sends and, for SMTP in debug mode, the exchange with the server. A custom adapter that implements the interface without the trait must add it. A failed send is still logged as a warning. At the start of each send the log is cut to its last `mailer.transport_log_limit` lines (default `1000`, `0` keeps every line), so it holds at most the limit plus the lines of the send in progress (#593). +- **BREAKING:** Reworked the app boot and runtime into pipelines of stages that the adapters run in explicit steps (#537, #538, #575, #576, #579, #583, #589, #593): - `AppInterface` gains `boot()`. The adapter constructors only store the context, and `AppFactory` calls `boot()` right after creating the adapter, so `AppFactory::create(...)->start()` is unchanged. Code that creates an adapter directly must call `boot()` before `start()`. - Removed the `AppAdapter` base class and the `WebAppTrait` and `ConsoleAppTrait` traits: `WebAppAdapter` and `ConsoleAppAdapter` implement `AppInterface` directly and moved to `Quantum\App\Adapters\Web` and `Quantum\App\Adapters\Console`. The stages are grouped by the adapter that runs them: the boot stages both adapters run in `Quantum\App\Stages\Boot`, the others in `Quantum\App\Adapters\Web\Stages\Boot`, `...\Web\Stages\Runtime`, `...\Console\Stages\Boot` and `...\Console\Stages\Runtime`. Code that goes through `AppFactory` is unaffected; code that references these classes directly must update its imports. - Replaced `BootPipeline` and `BootStageInterface` with one generic `Quantum\App\Pipeline`, which runs `StageInterface` stages in order and dispatches the `BEFORE`/`AFTER` events they declare. The context type is a PHPDoc template checked by PHPStan: a stage states it with `@implements StageInterface` (or `WebContext`, `ConsoleContext`) and takes `process(AppContext $context)`. A stage that calls `stop()` on the context ends the run; an optional closing stage, the second constructor argument, runs last, also after a stop, but not after a stage throws. - - `AppContext` keeps `getBaseDir()`, `getContainer()`, `getEnvironment()` and `getConfig()`, and carries the state of a run: `stop()`, `isStopped()`, `getExitCode()` and `setExitCode()`. Removed `AppContext::getRequest()`, `getResponse()` and `getRoutes()`: use `WebContext::getRequest()` and `WebContext::getRoutes()`, and `response()` for the shared response. - - The new `WebContext` and `ConsoleContext` extend `AppContext` with the same base directory and container. `WebContext` reads the request, the routes and the view cache from the container and stores the response to send; `ConsoleContext` reads the Symfony `Application`, the input and the output from the container. + - `AppContext` keeps `getBaseDir()`, `getContainer()`, `getEnvironment()` and `getConfig()`, and carries the state of a run: `stop()`, `isStopped()`, `getExitCode()` and `setExitCode()`. Removed `AppContext::getRequest()`, `getResponse()` and `getRoutes()`: use `WebContext::getRequest()` and `WebContext::getRoutes()`, and `response()` for the shared response. `AppContext` also records the app type: its constructor takes a third, required `$type` (`AppType::WEB` or `AppType::CONSOLE`) and `getType()` returns it. `AppFactory` passes it; code that creates an `AppContext` itself must pass it too. + - The new `WebContext` and `ConsoleContext` extend `AppContext` with the same base directory, container and app type. `WebContext` reads the request, the routes and the view cache from the container and stores the response to send; `ConsoleContext` reads the Symfony `Application`, the input and the output from the container. - The web runtime runs the preflight, route resolution, not found, prepare and dispatch stages, with `SendResponseStage` as its closing stage. Behavior is unchanged. - The console boot registers the Symfony `Application`, the input and the output in the container (`InitConsoleStage`), loads the helpers, registers the core commands, loads the environment, the config and the error handler, names the application from `app.name` and `app.version` (`UNKNOWN` without them), and registers the app commands last, so their constructors can use `env()` and `config()`. The console runtime runs the command and rejects a missing or unknown one. Registration goes through the new `Quantum\Console\CommandRegistrar`. Removed `ConsoleAppAdapter::createApplication()`: get the application with `Di::get(Application::class)`. - The console no longer decides by the `core:env` command name. Added `CliCommand::$requiresEnvironment` (default `true`) with `CliCommand::requiresEnvironment()`. When the `.env` file is missing or malformed, the new `ResolveEnvironmentStage` stops the boot for a core command that sets it to `false` (`core:env`) and for Symfony's `list` and `help`, so they still run; every other command fails with the exception of the environment file. With a valid `.env`, `core:env` now runs the full boot. A core command constructor must not use the environment or any config, since the core commands are registered before the environment is loaded. @@ -47,6 +48,8 @@ The format is based on Keep a Changelog. - **BREAKING:** Reshaped Lang configuration so `lang.default` now selects the adapter, locale fallback moved to `lang.default_locale`, and the unused `lang.enabled` toggle was removed (#533) - **BREAKING:** Removed `Lang::isEnabled()` from the public Lang API because it no longer affected runtime behavior (#533) - Applied the pending Rector refactors across `src/` (null-coalescing assignment for lazy initialization, explicit `instanceof` checks instead of truthiness checks on nullable objects, native return types on `env()`, `CurlAdapter::getId()` and `CurlAdapter::getResponseCookies()`) and added `composer rector:check` to the CI workflow after PHPStan (#587) +- In debug mode the console logs to the configured adapter, or the one a caller asks for, at every level. Before, debug mode always used the in-memory `message` adapter, so the console's log lines were never written anywhere, and `CronManager`'s `single` logger was replaced too. The web app keeps the `message` adapter for the debug toolbar (#593) +- The error handler chooses the console or the web output by the app type instead of checking `PHP_SAPI`; `php qt`, `php -S` and PHP-FPM behave as before (#593) ### Fixed - The page not found response is JSON for every client that asks for JSON, not only when the `Accept` header is exactly `application/json`: clients such as axios (`application/json, text/plain, */*`), a `charset` parameter, `+json` types and AJAX requests used to get the HTML page. `page_not_found_response()` decides with `Request::expectsJson()` and takes an optional request (#581) @@ -55,6 +58,10 @@ The format is based on Keep a Changelog. - The debugbar `routes` tab shows the matched route again (methods, pattern, module, controller, action or `Closure`, name, group, prefix, middlewares and params) next to the rendered view; since 3.0.0 it showed only the view path (#539) - The debugbar `queries` tab lists the queries the request ran; it has been empty since 2.6.0 (#539) - `SmtpAdapter` records the SMTP exchange in debug mode: it checked `config()->get('debug')`, which is never set, instead of `app.debug`, so the exchange never reached the `mails` tab (#539) +- Idiorm keeps no query log when debug mode is off. It received `app.debug` unconverted, and `DEBUG=false` from `.env` arrives as the text `"false"`, which PHP reads as true, so every query was kept in memory, also in production (#593) +- `mailer.mail_trap` set from `.env` is read as a boolean: `MAIL_TRAP=false` arrives as the text `"false"`, which kept every mail in the trap instead of sending it (#593) +- A "remember me" login works with `REMEMBER_LIFETIME` set in `.env`: the session auth adapter passed the text on to `Cookie::set()`, which takes an `int`, and threw a `TypeError` (#593) +- The Redis and Memcached cache adapters and the Redis rate limiter accept a `port` set from `.env`: `Memcached::addServer()` takes an `int` and threw a `TypeError` for the text (#593) ## [3.0.3] - 2026-07-10 diff --git a/src/App/Adapters/Console/ConsoleContext.php b/src/App/Adapters/Console/ConsoleContext.php index 4c10a3d25..e40e3c936 100644 --- a/src/App/Adapters/Console/ConsoleContext.php +++ b/src/App/Adapters/Console/ConsoleContext.php @@ -28,7 +28,7 @@ class ConsoleContext extends AppContext { public function __construct(AppContext $appContext) { - parent::__construct($appContext->getBaseDir(), $appContext->getContainer()); + parent::__construct($appContext->getBaseDir(), $appContext->getContainer(), $appContext->getType()); } public function getApplication(): Application diff --git a/src/App/Adapters/Web/WebContext.php b/src/App/Adapters/Web/WebContext.php index 6d0e5bf22..459cf97dd 100644 --- a/src/App/Adapters/Web/WebContext.php +++ b/src/App/Adapters/Web/WebContext.php @@ -33,7 +33,7 @@ class WebContext extends AppContext public function __construct(AppContext $appContext) { - parent::__construct($appContext->getBaseDir(), $appContext->getContainer()); + parent::__construct($appContext->getBaseDir(), $appContext->getContainer(), $appContext->getType()); } public function getRequest(): Request diff --git a/src/App/AppContext.php b/src/App/AppContext.php index 33ab4362a..5ea17b369 100644 --- a/src/App/AppContext.php +++ b/src/App/AppContext.php @@ -18,7 +18,7 @@ /** * Class AppContext * - * Carries the base directory and the container of an application, and the state of the run it is passed through: + * Carries the base directory, the container and the type (web or console) of an application, and the state of the run it is passed through: * the stopped flag, which ends the pipeline run early, and the exit code. * * @package Quantum\App @@ -29,14 +29,20 @@ class AppContext private DiContainer $container; + private string $type; + private bool $stopped = false; private int $exitCode = ExitCode::SUCCESS; - public function __construct(string $baseDir, DiContainer $container) + /** + * @param string $type AppType::WEB or AppType::CONSOLE + */ + public function __construct(string $baseDir, DiContainer $container, string $type) { $this->baseDir = $baseDir; $this->container = $container; + $this->type = $type; } public function getBaseDir(): string @@ -49,6 +55,14 @@ public function getContainer(): DiContainer return $this->container; } + /** + * Gets the type of the application: AppType::WEB or AppType::CONSOLE + */ + public function getType(): string + { + return $this->type; + } + public function getEnvironment(): Environment { return $this->resolveFromContainer(Environment::class); diff --git a/src/App/Factories/AppFactory.php b/src/App/Factories/AppFactory.php index 58e00e79c..d6ae28b1a 100644 --- a/src/App/Factories/AppFactory.php +++ b/src/App/Factories/AppFactory.php @@ -63,7 +63,7 @@ private static function createInstance(string $type, string $baseDir): App $container = new DiContainer(); - $context = new AppContext($baseDir, $container); + $context = new AppContext($baseDir, $container, $type); App::setContext($context); $adapterClass = self::ADAPTERS[$type]; diff --git a/src/App/Helpers/app.php b/src/App/Helpers/app.php index 86da60067..8f7daf2f4 100644 --- a/src/App/Helpers/app.php +++ b/src/App/Helpers/app.php @@ -5,3 +5,22 @@ * An open-source software development framework for PHP * @link https://quantumphp.io */ + +use Quantum\App\Enums\AppType; +use Quantum\App\App; + +/** + * Checks whether the running application is the web app + */ +function is_web_app(): bool +{ + return App::getContext()->getType() === AppType::WEB; +} + +/** + * Checks whether the running application is the console app + */ +function is_console_app(): bool +{ + return App::getContext()->getType() === AppType::CONSOLE; +} diff --git a/src/Auth/Adapters/SessionAuthAdapter.php b/src/Auth/Adapters/SessionAuthAdapter.php index 3d676b641..b12be1881 100644 --- a/src/Auth/Adapters/SessionAuthAdapter.php +++ b/src/Auth/Adapters/SessionAuthAdapter.php @@ -200,7 +200,7 @@ private function setRememberToken(User $user): void [$this->keyFields[AuthKeys::REMEMBER_TOKEN] => $rememberToken] ); - $rememberLifetime = $this->config['session']['remember_lifetime'] ?? self::DEFAULT_REMEMBER_LIFETIME; + $rememberLifetime = (int) ($this->config['session']['remember_lifetime'] ?? self::DEFAULT_REMEMBER_LIFETIME); cookie()->set( $this->keyFields[AuthKeys::REMEMBER_TOKEN], diff --git a/src/Cache/Adapters/MemcachedAdapter.php b/src/Cache/Adapters/MemcachedAdapter.php index 88935b8a6..536d47218 100644 --- a/src/Cache/Adapters/MemcachedAdapter.php +++ b/src/Cache/Adapters/MemcachedAdapter.php @@ -39,7 +39,7 @@ public function __construct(array $params) $this->prefix = $params['prefix']; $this->memcached = new Memcached(); - $this->memcached->addServer($params['host'], $params['port']); + $this->memcached->addServer($params['host'], (int) $params['port']); if (!$this->memcached->getStats()) { throw CacheException::cantConnect('Memcached server'); diff --git a/src/Cache/Adapters/RedisAdapter.php b/src/Cache/Adapters/RedisAdapter.php index f134e9e52..2785c90cf 100644 --- a/src/Cache/Adapters/RedisAdapter.php +++ b/src/Cache/Adapters/RedisAdapter.php @@ -38,7 +38,7 @@ public function __construct(array $params) $this->prefix = $params['prefix']; $this->redis = new Redis(); - $this->redis->connect($params['host'], $params['port']); + $this->redis->connect($params['host'], (int) $params['port']); } /** diff --git a/src/Database/Adapters/Idiorm/IdiormDbal.php b/src/Database/Adapters/Idiorm/IdiormDbal.php index 0649c5824..74376e162 100644 --- a/src/Database/Adapters/Idiorm/IdiormDbal.php +++ b/src/Database/Adapters/Idiorm/IdiormDbal.php @@ -21,11 +21,11 @@ use Quantum\Database\Exceptions\DatabaseException; use Quantum\Database\Contracts\DbalInterface; use Quantum\App\Exceptions\BaseException; +use Quantum\Di\Exceptions\DiException; use InvalidArgumentException; +use ReflectionException; use ORM; use PDO; -use Quantum\Di\Exceptions\DiException; -use ReflectionException; /** * Class IdiormDbal @@ -259,7 +259,7 @@ protected static function getBaseConfig(string $driver, array $config): array { return [ 'connection_string' => self::buildConnectionString($driver, $config), - 'logging' => config()->get('app.debug', false), + 'logging' => is_debug_mode(), 'error_mode' => PDO::ERRMODE_EXCEPTION, ]; } diff --git a/src/Logger/Adapters/MessageAdapter.php b/src/Logger/Adapters/MessageAdapter.php index fd641073c..c9e0626b2 100644 --- a/src/Logger/Adapters/MessageAdapter.php +++ b/src/Logger/Adapters/MessageAdapter.php @@ -25,9 +25,23 @@ class MessageAdapter implements ReportableInterface */ private array $messages = []; + /** + * The number of last messages kept; 0 keeps every message + */ + private int $limit; + + public function __construct(int $limit = 1000) + { + $this->limit = $limit; + } + public function report(string $level, string|Stringable $message, ?array $context = []): void { $this->messages[] = [$level => $message]; + + if ($this->limit > 0 && count($this->messages) > $this->limit) { + $this->messages = array_slice($this->messages, -$this->limit); + } } /** diff --git a/src/Logger/Factories/LoggerFactory.php b/src/Logger/Factories/LoggerFactory.php index f8e6afc39..9af5895f4 100644 --- a/src/Logger/Factories/LoggerFactory.php +++ b/src/Logger/Factories/LoggerFactory.php @@ -69,7 +69,7 @@ public function resolve(?string $adapter = null): Logger throw LoggerException::adapterNotSupported(LoggerType::MESSAGE); } - $adapter = $isDebug ? LoggerType::MESSAGE : ($adapter ?? config()->get('logging.default')); + $adapter = $isDebug && is_web_app() ? LoggerType::MESSAGE : ($adapter ?? config()->get('logging.default')); $adapterClass = $this->getAdapterClass($adapter); @@ -86,7 +86,7 @@ public function resolve(?string $adapter = null): Logger private function createInstance(string $adapterClass, string $adapter): Logger { if ($adapter === LoggerType::MESSAGE) { - return new Logger(new MessageAdapter()); + return new Logger(new MessageAdapter((int) config()->get('logging.message.limit', 1000))); } $adapterInstance = new $adapterClass(config()->get('logging.' . $adapter)); diff --git a/src/Mailer/Traits/MailerTrait.php b/src/Mailer/Traits/MailerTrait.php index fe381be17..ffa1f925d 100644 --- a/src/Mailer/Traits/MailerTrait.php +++ b/src/Mailer/Traits/MailerTrait.php @@ -84,7 +84,7 @@ trait MailerTrait protected array $stringAttachments = []; /** - * Transport log + * Transport log, cut to its last mailer.transport_log_limit lines at the start of each send * @var array */ private array $transportLog = []; @@ -222,9 +222,16 @@ public function getBody() */ public function send(): bool { + // Cut here, as every send reaches this point, also after the previous one threw + $limit = (int) config()->get('mailer.transport_log_limit', 1000); + + if ($limit > 0 && count($this->transportLog) > $limit) { + $this->transportLog = array_slice($this->transportLog, -$limit); + } + $this->prepare(); - $sent = config()->get('mailer.mail_trap') ? $this->saveEmail() : $this->sendEmail(); + $sent = filter_var(config()->get('mailer.mail_trap'), FILTER_VALIDATE_BOOLEAN) ? $this->saveEmail() : $this->sendEmail(); $this->resetFields(); diff --git a/src/RateLimit/Adapters/RedisRateLimitAdapter.php b/src/RateLimit/Adapters/RedisRateLimitAdapter.php index 7796ee456..cf1356ce8 100644 --- a/src/RateLimit/Adapters/RedisRateLimitAdapter.php +++ b/src/RateLimit/Adapters/RedisRateLimitAdapter.php @@ -36,7 +36,7 @@ public function __construct(array $params) $this->prefix = (string) ($params['prefix'] ?? ''); $this->redis = new Redis(); - $this->redis->connect($params['host'], $params['port']); + $this->redis->connect($params['host'], (int) $params['port']); } public function hit(string $key, int $limit, int $interval): bool diff --git a/src/Tracer/ErrorHandler.php b/src/Tracer/ErrorHandler.php index b1b39263f..5aebf9514 100644 --- a/src/Tracer/ErrorHandler.php +++ b/src/Tracer/ErrorHandler.php @@ -79,7 +79,7 @@ public function handleError(int $severity, string $message, string $file, int $l */ public function handleException(Throwable $throwable): void { - if (PHP_SAPI === 'cli') { + if (is_console_app()) { $this->handleCliException($throwable); } else { $this->handleWebException($throwable); diff --git a/tests/Unit/App/Adapters/Console/ConsoleAppAdapterTest.php b/tests/Unit/App/Adapters/Console/ConsoleAppAdapterTest.php index c10d39e4d..4ac3982ce 100644 --- a/tests/Unit/App/Adapters/Console/ConsoleAppAdapterTest.php +++ b/tests/Unit/App/Adapters/Console/ConsoleAppAdapterTest.php @@ -15,6 +15,7 @@ use Quantum\Environment\Environment; use Quantum\Tests\Unit\AppTestCase; use Quantum\Tracer\ErrorHandler; +use Quantum\App\Enums\AppType; use Quantum\App\AppContext; use Quantum\App\Pipeline; use ReflectionProperty; @@ -244,7 +245,7 @@ public function testConsoleAppAdapterConstructorDoesNotBoot(): void { $_SERVER['argv'] = ['qt', 'list', '--quiet']; - new ConsoleAppAdapter($this->createContext()); + new ConsoleAppAdapter($this->createContext(AppType::CONSOLE)); $this->assertFalse(Di::isRegistered(Application::class)); $this->assertFalse(Di::has(Environment::class)); @@ -254,7 +255,7 @@ public function testConsoleAppAdapterBootRunsTheBootStages(): void { $_SERVER['argv'] = ['qt', 'list', '--quiet']; - $adapter = new ConsoleAppAdapter($this->createContext()); + $adapter = new ConsoleAppAdapter($this->createContext(AppType::CONSOLE)); $adapter->boot(); $this->assertTrue(Di::isRegistered(Application::class)); @@ -266,7 +267,7 @@ public function testConsoleAppAdapterRegistersTheConsoleObjectsInTheContainerAft { $_SERVER['argv'] = ['qt', 'core:env', '--yes']; - (new ConsoleAppAdapter($this->createContext()))->boot(); + (new ConsoleAppAdapter($this->createContext(AppType::CONSOLE)))->boot(); $this->assertTrue(Di::isRegistered(Application::class)); $this->assertTrue(Di::isRegistered(OutputInterface::class)); @@ -279,7 +280,7 @@ public function testConsoleAppAdapterRegistersTheConsoleObjectsInTheContainerWhe $_SERVER['argv'] = ['qt', 'list', '--quiet']; $this->withoutEnvironmentFile(function (): void { - (new ConsoleAppAdapter($this->createContext()))->boot(); + (new ConsoleAppAdapter($this->createContext(AppType::CONSOLE)))->boot(); $this->assertTrue(Di::isRegistered(Application::class)); $this->assertTrue(Di::isRegistered(OutputInterface::class)); @@ -316,7 +317,7 @@ public function testConsoleAppAdapterNamesTheApplicationUnknownWhenTheAppNameIsE { $_SERVER['argv'] = ['qt', 'list', '--quiet']; - $context = $this->createContext(); + $context = $this->createContext(AppType::CONSOLE); (new Pipeline([new LoadHelpersStage()]))->run($context); @@ -333,7 +334,7 @@ public function testConsoleAppAdapterKeepsAnEmptyAppNameAndVersionFromTheConfig( { $_SERVER['argv'] = ['qt', 'list', '--quiet']; - $context = $this->createContext(); + $context = $this->createContext(AppType::CONSOLE); (new Pipeline([new LoadHelpersStage()]))->run($context); @@ -677,7 +678,7 @@ private function withFileContent(string $file, string $content, callable $test): */ private function createAdapter(?Application $application = null, ?AppContext $context = null): ConsoleAppAdapter { - $context ??= $this->createContext(); + $context ??= $this->createContext(AppType::CONSOLE); if ($application !== null) { Di::set(Application::class, $application); diff --git a/tests/Unit/App/Adapters/Console/ConsoleContextTest.php b/tests/Unit/App/Adapters/Console/ConsoleContextTest.php index 1c4bcc706..b7e3c9dfb 100644 --- a/tests/Unit/App/Adapters/Console/ConsoleContextTest.php +++ b/tests/Unit/App/Adapters/Console/ConsoleContextTest.php @@ -10,6 +10,7 @@ use Quantum\Tests\Unit\AppTestCase; use Quantum\App\Adapters\Console\ConsoleContext; use Quantum\App\Enums\ExitCode; +use Quantum\App\Enums\AppType; use Quantum\App\AppContext; use Quantum\Di\DiContainer; use Quantum\App\App; @@ -47,11 +48,12 @@ public function testConsoleContextIsAnAppContextBuiltFromTheOneItIsGiven(): void { $container = new DiContainer(); - $consoleContext = new ConsoleContext(new AppContext('/base/dir', $container)); + $consoleContext = new ConsoleContext(new AppContext('/base/dir', $container, AppType::CONSOLE)); $this->assertInstanceOf(AppContext::class, $consoleContext); $this->assertSame('/base/dir', $consoleContext->getBaseDir()); $this->assertSame($container, $consoleContext->getContainer()); + $this->assertSame(AppType::CONSOLE, $consoleContext->getType()); } public function testConsoleContextExposesApplicationInputAndOutputFromTheContainer(): void @@ -63,7 +65,7 @@ public function testConsoleContextExposesApplicationInputAndOutputFromTheContain public function testConsoleContextReadsTheConsoleObjectsFromTheContainerWhenAsked(): void { - $context = new AppContext(PROJECT_ROOT, new \Quantum\Di\DiContainer()); + $context = new AppContext(PROJECT_ROOT, new \Quantum\Di\DiContainer(), AppType::CONSOLE); $consoleContext = new ConsoleContext($context); $application = new Application('Late', '1.0'); diff --git a/tests/Unit/App/Adapters/Console/Stages/Boot/InitConsoleStageTest.php b/tests/Unit/App/Adapters/Console/Stages/Boot/InitConsoleStageTest.php index bf7104a28..d7adff49a 100644 --- a/tests/Unit/App/Adapters/Console/Stages/Boot/InitConsoleStageTest.php +++ b/tests/Unit/App/Adapters/Console/Stages/Boot/InitConsoleStageTest.php @@ -11,13 +11,14 @@ use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Application; use Quantum\Tests\Unit\AppTestCase; +use Quantum\App\Enums\AppType; use Quantum\Di\Di; class InitConsoleStageTest extends AppTestCase { public function setUp(): void { - $this->context = $this->createContext(); + $this->context = $this->createContext(AppType::CONSOLE); } public function tearDown(): void diff --git a/tests/Unit/App/Adapters/Console/Stages/Boot/SetupConsoleApplicationStageTest.php b/tests/Unit/App/Adapters/Console/Stages/Boot/SetupConsoleApplicationStageTest.php index cbac2050a..c922a2a4b 100644 --- a/tests/Unit/App/Adapters/Console/Stages/Boot/SetupConsoleApplicationStageTest.php +++ b/tests/Unit/App/Adapters/Console/Stages/Boot/SetupConsoleApplicationStageTest.php @@ -6,13 +6,14 @@ use Quantum\App\Stages\Boot\LoadHelpersStage; use Symfony\Component\Console\Application; use Quantum\Tests\Unit\AppTestCase; +use Quantum\App\Enums\AppType; use Quantum\Di\Di; class SetupConsoleApplicationStageTest extends AppTestCase { public function setUp(): void { - $this->context = $this->createContext(); + $this->context = $this->createContext(AppType::CONSOLE); (new LoadHelpersStage())->process($this->context); @@ -59,7 +60,7 @@ public function testSetupConsoleApplicationStageNamesAnApplicationThatIsAlreadyR { $application = new Application('Registered', '0.1'); - $this->context = $this->createContext(); + $this->context = $this->createContext(AppType::CONSOLE); (new LoadHelpersStage())->process($this->context); Di::set(Application::class, $application); diff --git a/tests/Unit/App/Adapters/Web/Stages/Runtime/DispatchRequestStageTest.php b/tests/Unit/App/Adapters/Web/Stages/Runtime/DispatchRequestStageTest.php index 5ef189809..8d920f0c4 100644 --- a/tests/Unit/App/Adapters/Web/Stages/Runtime/DispatchRequestStageTest.php +++ b/tests/Unit/App/Adapters/Web/Stages/Runtime/DispatchRequestStageTest.php @@ -50,6 +50,7 @@ public function apply(Request $request, Closure $next): Response use Quantum\ResourceCache\ViewCache; use Quantum\App\Adapters\Web\WebContext; use Quantum\Router\MatchedRoute; + use Quantum\App\Enums\AppType; use Quantum\Di\DiContainer; use Quantum\App\AppContext; use Quantum\Http\Request; @@ -190,7 +191,7 @@ public function testDispatchRequestStagePassesTheContextRequestToTheMiddlewares( DispatchStageMwRecorder::$request = null; - (new DispatchRequestStage())->process(new WebContext(new AppContext(PROJECT_ROOT, $container))); + (new DispatchRequestStage())->process(new WebContext(new AppContext(PROJECT_ROOT, $container, AppType::WEB))); $this->assertSame($request, DispatchStageMwRecorder::$request); } diff --git a/tests/Unit/App/Adapters/Web/Stages/Runtime/HandlePreflightStageTest.php b/tests/Unit/App/Adapters/Web/Stages/Runtime/HandlePreflightStageTest.php index 451d297b3..2fdb620ce 100644 --- a/tests/Unit/App/Adapters/Web/Stages/Runtime/HandlePreflightStageTest.php +++ b/tests/Unit/App/Adapters/Web/Stages/Runtime/HandlePreflightStageTest.php @@ -6,6 +6,7 @@ use Quantum\Tests\Unit\AppTestCase; use Quantum\Http\Enums\StatusCode; use Quantum\App\Adapters\Web\WebContext; +use Quantum\App\Enums\AppType; use Quantum\Di\DiContainer; use Quantum\App\AppContext; use Quantum\Http\Request; @@ -53,7 +54,7 @@ public function testHandlePreflightStageReadsTheRequestFromTheContext(): void $container = new DiContainer(); $container->set(Request::class, $request); - $webContext = new WebContext(new AppContext(PROJECT_ROOT, $container)); + $webContext = new WebContext(new AppContext(PROJECT_ROOT, $container, AppType::WEB)); (new HandlePreflightStage())->process($webContext); diff --git a/tests/Unit/App/Adapters/Web/Stages/Runtime/HandleRouteNotFoundStageTest.php b/tests/Unit/App/Adapters/Web/Stages/Runtime/HandleRouteNotFoundStageTest.php index f33586a50..624a1b631 100644 --- a/tests/Unit/App/Adapters/Web/Stages/Runtime/HandleRouteNotFoundStageTest.php +++ b/tests/Unit/App/Adapters/Web/Stages/Runtime/HandleRouteNotFoundStageTest.php @@ -8,6 +8,7 @@ use Quantum\Http\Enums\StatusCode; use Quantum\App\Adapters\Web\WebContext; use Quantum\Router\MatchedRoute; +use Quantum\App\Enums\AppType; use Quantum\Di\DiContainer; use Quantum\App\AppContext; use Quantum\Router\Route; @@ -70,7 +71,7 @@ public function testHandleRouteNotFoundStageDecidesByTheContextRequest(): void $container = new DiContainer(); $container->set(Request::class, $request); - $webContext = new WebContext(new AppContext(PROJECT_ROOT, $container)); + $webContext = new WebContext(new AppContext(PROJECT_ROOT, $container, AppType::WEB)); (new HandleRouteNotFoundStage())->process($webContext); diff --git a/tests/Unit/App/Adapters/Web/Stages/Runtime/PrepareRequestStageTest.php b/tests/Unit/App/Adapters/Web/Stages/Runtime/PrepareRequestStageTest.php index 08665af74..943867bfc 100644 --- a/tests/Unit/App/Adapters/Web/Stages/Runtime/PrepareRequestStageTest.php +++ b/tests/Unit/App/Adapters/Web/Stages/Runtime/PrepareRequestStageTest.php @@ -7,6 +7,7 @@ use Quantum\ResourceCache\ViewCache; use Quantum\App\Adapters\Web\WebContext; use Quantum\Router\MatchedRoute; +use Quantum\App\Enums\AppType; use Quantum\Di\DiContainer; use Quantum\App\AppContext; use Quantum\Router\Route; @@ -88,7 +89,7 @@ public function testPrepareRequestStageRegistersViewCacheInTheContextContainer() { $container = new DiContainer(); - (new PrepareRequestStage())->process(new WebContext(new AppContext(PROJECT_ROOT, $container))); + (new PrepareRequestStage())->process(new WebContext(new AppContext(PROJECT_ROOT, $container, AppType::WEB))); $this->assertTrue($container->isRegistered(ViewCache::class)); $this->assertFalse(Di::isRegistered(ViewCache::class)); @@ -105,7 +106,7 @@ public function testPrepareRequestStageSetsUpTheViewCacheOfTheContextContainer() $container->get(ViewCache::class)->enableCaching(true); try { - (new PrepareRequestStage())->process(new WebContext(new AppContext(PROJECT_ROOT, $container))); + (new PrepareRequestStage())->process(new WebContext(new AppContext(PROJECT_ROOT, $container, AppType::WEB))); $this->assertDirectoryExists($moduleDir); } finally { diff --git a/tests/Unit/App/Adapters/Web/Stages/Runtime/ResolveRouteStageTest.php b/tests/Unit/App/Adapters/Web/Stages/Runtime/ResolveRouteStageTest.php index 137fb09c8..d8bf611d1 100644 --- a/tests/Unit/App/Adapters/Web/Stages/Runtime/ResolveRouteStageTest.php +++ b/tests/Unit/App/Adapters/Web/Stages/Runtime/ResolveRouteStageTest.php @@ -7,6 +7,7 @@ use Quantum\App\Adapters\Web\WebContext; use Quantum\Router\RouteCollection; use Quantum\Router\MatchedRoute; +use Quantum\App\Enums\AppType; use Quantum\Di\DiContainer; use Quantum\App\AppContext; use Quantum\Http\Request; @@ -72,7 +73,7 @@ public function testResolveRouteStageReadsTheRequestAndRoutesFromTheContext(): v $container->set(Request::class, $request); $container->set(RouteCollection::class, $routes); - (new ResolveRouteStage())->process(new WebContext(new AppContext(PROJECT_ROOT, $container))); + (new ResolveRouteStage())->process(new WebContext(new AppContext(PROJECT_ROOT, $container, AppType::WEB))); $this->assertSame('contextAction', $request->getMatchedRoute()->getRoute()->getAction()); $this->assertNull(request()->getMatchedRoute()); diff --git a/tests/Unit/App/Adapters/Web/Stages/Runtime/SendResponseStageTest.php b/tests/Unit/App/Adapters/Web/Stages/Runtime/SendResponseStageTest.php index 0758ffd61..71933e526 100644 --- a/tests/Unit/App/Adapters/Web/Stages/Runtime/SendResponseStageTest.php +++ b/tests/Unit/App/Adapters/Web/Stages/Runtime/SendResponseStageTest.php @@ -7,6 +7,7 @@ use Quantum\Tests\Unit\AppTestCase; use Quantum\App\Adapters\Web\WebContext; use Quantum\Router\MatchedRoute; +use Quantum\App\Enums\AppType; use Quantum\Di\DiContainer; use Quantum\App\AppContext; use Quantum\Config\Config; @@ -125,7 +126,7 @@ public function testSendResponseStageReadsTheConfigAndRequestFromTheContext(): v $container->set(Request::class, $request); $container->set(Config::class, $config); - $webContext = new WebContext(new AppContext(PROJECT_ROOT, $container)); + $webContext = new WebContext(new AppContext(PROJECT_ROOT, $container, AppType::WEB)); $webContext->setResponse(response()->html('')); ob_start(); diff --git a/tests/Unit/App/Adapters/Web/WebContextTest.php b/tests/Unit/App/Adapters/Web/WebContextTest.php index 012fdab21..69e3156fd 100644 --- a/tests/Unit/App/Adapters/Web/WebContextTest.php +++ b/tests/Unit/App/Adapters/Web/WebContextTest.php @@ -7,6 +7,7 @@ use Quantum\Router\RouteCollection; use Quantum\Tests\Unit\AppTestCase; use Quantum\Router\MatchedRoute; +use Quantum\App\Enums\AppType; use Quantum\Di\DiContainer; use Quantum\App\AppContext; use Quantum\Http\Request; @@ -32,11 +33,12 @@ public function testWebContextIsAnAppContextBuiltFromTheOneItIsGiven(): void { $container = new DiContainer(); - $webContext = new WebContext(new AppContext('/base/dir', $container)); + $webContext = new WebContext(new AppContext('/base/dir', $container, AppType::WEB)); $this->assertInstanceOf(AppContext::class, $webContext); $this->assertSame('/base/dir', $webContext->getBaseDir()); $this->assertSame($container, $webContext->getContainer()); + $this->assertSame(AppType::WEB, $webContext->getType()); } public function testWebContextExposesRequestAndRoutesFromTheContainer(): void @@ -48,7 +50,7 @@ public function testWebContextExposesRequestAndRoutesFromTheContainer(): void $container->set(Request::class, $request); $container->set(RouteCollection::class, $routes); - $webContext = new WebContext(new AppContext('/base/dir', $container)); + $webContext = new WebContext(new AppContext('/base/dir', $container, AppType::WEB)); $this->assertSame($request, $webContext->getRequest()); $this->assertSame($routes, $webContext->getRoutes()); @@ -80,7 +82,7 @@ public function testWebContextReadsViewCacheFromTheContainer(): void $viewCache = Mockery::mock(ViewCache::class); $container->set(ViewCache::class, $viewCache); - $webContext = new WebContext(new AppContext('/base/dir', $container)); + $webContext = new WebContext(new AppContext('/base/dir', $container, AppType::WEB)); $this->assertSame($viewCache, $webContext->getViewCache()); } diff --git a/tests/Unit/App/AppContextTest.php b/tests/Unit/App/AppContextTest.php index d999812cf..7077a6141 100644 --- a/tests/Unit/App/AppContextTest.php +++ b/tests/Unit/App/AppContextTest.php @@ -5,6 +5,7 @@ use Quantum\Environment\Environment; use PHPUnit\Framework\TestCase; use Quantum\App\Enums\ExitCode; +use Quantum\App\Enums\AppType; use Quantum\Di\DiContainer; use Quantum\App\AppContext; use Quantum\Config\Config; @@ -14,15 +15,22 @@ class AppContextTest extends TestCase { public function testAppContextBaseDir(): void { - $context = new AppContext('/my/base/dir', new DiContainer()); + $context = new AppContext('/my/base/dir', new DiContainer(), AppType::WEB); $this->assertSame('/my/base/dir', $context->getBaseDir()); } + public function testAppContextType(): void + { + $context = new AppContext('/tmp', new DiContainer(), AppType::CONSOLE); + + $this->assertSame(AppType::CONSOLE, $context->getType()); + } + public function testAppContextContainer(): void { $container = new DiContainer(); - $context = new AppContext('/tmp', $container); + $context = new AppContext('/tmp', $container, AppType::WEB); $this->assertSame($container, $context->getContainer()); } @@ -33,7 +41,7 @@ public function testAppContextGetEnvironment(): void $environment = Mockery::mock(Environment::class); $container->set(Environment::class, $environment); - $context = new AppContext('/tmp', $container); + $context = new AppContext('/tmp', $container, AppType::WEB); $this->assertSame($environment, $context->getEnvironment()); } @@ -44,21 +52,21 @@ public function testAppContextGetConfig(): void $config = Mockery::mock(Config::class); $container->set(Config::class, $config); - $context = new AppContext('/tmp', $container); + $context = new AppContext('/tmp', $container, AppType::WEB); $this->assertSame($config, $context->getConfig()); } public function testAppContextIsNotStoppedByDefault(): void { - $context = new AppContext('/tmp', new DiContainer()); + $context = new AppContext('/tmp', new DiContainer(), AppType::WEB); $this->assertFalse($context->isStopped()); } public function testAppContextStop(): void { - $context = new AppContext('/tmp', new DiContainer()); + $context = new AppContext('/tmp', new DiContainer(), AppType::WEB); $context->stop(); @@ -67,14 +75,14 @@ public function testAppContextStop(): void public function testAppContextExitCodeDefaultsToSuccess(): void { - $context = new AppContext('/tmp', new DiContainer()); + $context = new AppContext('/tmp', new DiContainer(), AppType::WEB); $this->assertSame(ExitCode::SUCCESS, $context->getExitCode()); } public function testAppContextSetExitCode(): void { - $context = new AppContext('/tmp', new DiContainer()); + $context = new AppContext('/tmp', new DiContainer(), AppType::WEB); $context->setExitCode(ExitCode::INVALID); diff --git a/tests/Unit/App/Factories/AppFactoryTest.php b/tests/Unit/App/Factories/AppFactoryTest.php index b216b6a3d..e3218ecd2 100644 --- a/tests/Unit/App/Factories/AppFactoryTest.php +++ b/tests/Unit/App/Factories/AppFactoryTest.php @@ -90,6 +90,19 @@ public function testAppFactoryDestroy(): void $this->assertNotSame($app1, $app2); } + public function testAppFactorySetsTheContextTypeOfTheCreatedApp(): void + { + AppFactory::destroy(AppType::WEB); + AppFactory::create(AppType::WEB, PROJECT_ROOT); + + $this->assertSame(AppType::WEB, App::getContext()->getType()); + + AppFactory::destroy(AppType::CONSOLE); + AppFactory::create(AppType::CONSOLE, PROJECT_ROOT); + + $this->assertSame(AppType::CONSOLE, App::getContext()->getType()); + } + public function testAppFactoryResetsContainerOnCreate(): void { AppFactory::destroy(AppType::WEB); diff --git a/tests/Unit/App/Helpers/AppHelperTest.php b/tests/Unit/App/Helpers/AppHelperTest.php new file mode 100644 index 000000000..a39f62c5b --- /dev/null +++ b/tests/Unit/App/Helpers/AppHelperTest.php @@ -0,0 +1,25 @@ +useAppType(AppType::WEB); + + $this->assertTrue(is_web_app()); + $this->assertFalse(is_console_app()); + } + + public function testIsConsoleAppInTheConsole(): void + { + $this->useAppType(AppType::CONSOLE); + + $this->assertTrue(is_console_app()); + $this->assertFalse(is_web_app()); + } +} diff --git a/tests/Unit/App/PipelineTest.php b/tests/Unit/App/PipelineTest.php index b1cb35867..584f5d5e4 100644 --- a/tests/Unit/App/PipelineTest.php +++ b/tests/Unit/App/PipelineTest.php @@ -15,6 +15,7 @@ use Quantum\App\Adapters\Web\WebContext; use Quantum\Tests\Unit\AppTestCase; use Quantum\App\Enums\ExitCode; +use Quantum\App\Enums\AppType; use InvalidArgumentException; use Quantum\Di\DiContainer; use Quantum\App\AppContext; @@ -47,21 +48,21 @@ public function testPipelineRunsStagesInOrder(): void $this->createStage(function () use (&$log): void { $log[] = 'third'; }), - ]))->run(new AppContext('', new DiContainer())); + ]))->run(new AppContext('', new DiContainer(), AppType::WEB)); $this->assertSame(['first', 'second', 'third'], $log); } public function testEmptyPipelineRunsWithoutError(): void { - (new Pipeline([]))->run(new AppContext('', new DiContainer())); + (new Pipeline([]))->run(new AppContext('', new DiContainer(), AppType::WEB)); $this->assertTrue(true); } public function testPipelinePassesTheContextToEveryStage(): void { - $context = new AppContext('/test/dir', new DiContainer()); + $context = new AppContext('/test/dir', new DiContainer(), AppType::WEB); $received = []; $stage = $this->createStage(function (AppContext $context) use (&$received): void { diff --git a/tests/Unit/AppTestCase.php b/tests/Unit/AppTestCase.php index 40f9f4b61..7ac1fed5e 100644 --- a/tests/Unit/AppTestCase.php +++ b/tests/Unit/AppTestCase.php @@ -57,14 +57,22 @@ public function tearDown(): void $this->clearAppContext(); } - protected function createContext(): AppContext + protected function createContext(string $type = AppType::WEB): AppContext { - $context = new AppContext(PROJECT_ROOT, new DiContainer()); + $context = new AppContext(PROJECT_ROOT, new DiContainer(), $type); App::setContext($context); return $context; } + /** + * Replaces the app context with one of the given type that keeps the current container + */ + protected function useAppType(string $type): void + { + App::setContext(new AppContext(PROJECT_ROOT, App::getContext()->getContainer(), $type)); + } + protected function clearAppContext(): void { $prop = new ReflectionProperty(App::class, 'context'); diff --git a/tests/Unit/Auth/Adapters/SessionAuthAdapterTest.php b/tests/Unit/Auth/Adapters/SessionAuthAdapterTest.php index 48e707e3b..ff20637c0 100644 --- a/tests/Unit/Auth/Adapters/SessionAuthAdapterTest.php +++ b/tests/Unit/Auth/Adapters/SessionAuthAdapterTest.php @@ -239,4 +239,14 @@ public function testWebRememberTokenLifetimeIsConfigurable(): void $this->assertTrue(cookie()->has('remember_token')); } + + public function testWebRememberTokenLifetimeAcceptsTheTextFromEnv(): void + { + config()->set('auth.session.remember_lifetime', '86400'); + $this->sessionAuth = $this->createSessionAuth(); + + $this->sessionAuth->signin('admin@qt.com', 'qwerty', true); + + $this->assertTrue(cookie()->has('remember_token')); + } } diff --git a/tests/Unit/Cache/Adapters/MemcachedAdapterTest.php b/tests/Unit/Cache/Adapters/MemcachedAdapterTest.php index fd63e8425..ad016225f 100644 --- a/tests/Unit/Cache/Adapters/MemcachedAdapterTest.php +++ b/tests/Unit/Cache/Adapters/MemcachedAdapterTest.php @@ -29,6 +29,20 @@ public function tearDown(): void $this->memCached->clear(); } + public function testMemcachedAdapterConnectsWithThePortAsText(): void + { + $adapter = new MemcachedAdapter([ + 'prefix' => 'test', + 'host' => '127.0.0.1', + 'port' => '11211', + 'ttl' => 60, + ]); + + $adapter->set('port-as-text', 'connected'); + + $this->assertSame('connected', $adapter->get('port-as-text')); + } + public function testMemcachedAdapterSetGetDelete(): void { diff --git a/tests/Unit/Cache/Adapters/RedisAdapterTest.php b/tests/Unit/Cache/Adapters/RedisAdapterTest.php index 5d1340d07..66689a97e 100644 --- a/tests/Unit/Cache/Adapters/RedisAdapterTest.php +++ b/tests/Unit/Cache/Adapters/RedisAdapterTest.php @@ -29,6 +29,20 @@ public function tearDown(): void $this->redis->clear(); } + public function testRedisAdapterConnectsWithThePortAsText(): void + { + $adapter = new RedisAdapter([ + 'prefix' => 'test', + 'host' => '127.0.0.1', + 'port' => '6379', + 'ttl' => 60, + ]); + + $adapter->set('port-as-text', 'connected'); + + $this->assertSame('connected', $adapter->get('port-as-text')); + } + public function testRedisAdapterSetGetDelete(): void { diff --git a/tests/Unit/Database/Adapters/Idiorm/IdiormDbalTest.php b/tests/Unit/Database/Adapters/Idiorm/IdiormDbalTest.php index 3ae3f09fe..9dcda6841 100644 --- a/tests/Unit/Database/Adapters/Idiorm/IdiormDbalTest.php +++ b/tests/Unit/Database/Adapters/Idiorm/IdiormDbalTest.php @@ -3,6 +3,7 @@ namespace Quantum\Tests\Unit\Database\Adapters\Idiorm; use Quantum\Database\Adapters\Idiorm\IdiormDbal; +use Quantum\App\Enums\AppType; class IdiormDbalTest extends IdiormDbalTestCase { @@ -19,4 +20,42 @@ public function testIdiormGetTable(): void $this->assertEquals('users', $userModel->getTable()); } + + public function testIdiormLogsQueriesInTheWebAppInDebugMode(): void + { + config()->set('app.debug', true); + + $this->assertTrue($this->getBaseConfig()['logging']); + } + + public function testIdiormLogsQueriesInTheConsoleInDebugMode(): void + { + $this->useAppType(AppType::CONSOLE); + + config()->set('app.debug', true); + + $this->assertTrue($this->getBaseConfig()['logging']); + } + + public function testIdiormLogsNoQueriesOutsideDebugMode(): void + { + config()->set('app.debug', false); + + $this->assertFalse($this->getBaseConfig()['logging']); + } + + public function testIdiormLogsNoQueriesWhenDebugIsTheTextFalseFromEnv(): void + { + config()->set('app.debug', 'false'); + + $this->assertFalse($this->getBaseConfig()['logging']); + } + + /** + * @return array + */ + private function getBaseConfig(): array + { + return $this->invokePrivateMethod(new IdiormDbal('users'), 'getBaseConfig', ['sqlite', ['database' => ':memory:']]); + } } diff --git a/tests/Unit/Environment/EnvironmentTest.php b/tests/Unit/Environment/EnvironmentTest.php index b9a13a8f1..70faa5cf6 100644 --- a/tests/Unit/Environment/EnvironmentTest.php +++ b/tests/Unit/Environment/EnvironmentTest.php @@ -29,7 +29,7 @@ public function testLoadReportsMissingBootstrapConfig(): void $context = App::getContext(); try { - App::setContext(new AppContext(PROJECT_ROOT . DS . 'cron-command-tests-empty', $context->getContainer())); + App::setContext(new AppContext(PROJECT_ROOT . DS . 'cron-command-tests-empty', $context->getContainer(), $context->getType())); $this->expectException(EnvException::class); $this->expectExceptionMessage('shared' . DS . 'config' . DS . 'env.php'); @@ -65,7 +65,7 @@ public function testLoadDoesNotReadBootstrapAgain(): void $context = App::getContext(); try { - App::setContext(new AppContext(PROJECT_ROOT . DS . 'cron-command-tests-empty', $context->getContainer())); + App::setContext(new AppContext(PROJECT_ROOT . DS . 'cron-command-tests-empty', $context->getContainer(), $context->getType())); $environment->load(); diff --git a/tests/Unit/Logger/Adapters/MessageAdapterTest.php b/tests/Unit/Logger/Adapters/MessageAdapterTest.php index 928378683..99ebf6461 100644 --- a/tests/Unit/Logger/Adapters/MessageAdapterTest.php +++ b/tests/Unit/Logger/Adapters/MessageAdapterTest.php @@ -28,4 +28,39 @@ public function testReportKeepsTheMessagesInOrder(): void $this->assertSame([['info' => 'Test message'], ['error' => 'Error message']], $this->adapter->getMessages()); } + + public function testReportKeepsOnlyTheLastMessagesOverTheLimit(): void + { + $adapter = new MessageAdapter(2); + + $adapter->report('info', 'First'); + $adapter->report('info', 'Second'); + $adapter->report('info', 'Third'); + + $this->assertSame([['info' => 'Second'], ['info' => 'Third']], $adapter->getMessages()); + } + + public function testReportKeepsEveryMessageWithZeroLimit(): void + { + $adapter = new MessageAdapter(0); + + for ($i = 1; $i <= 1001; $i++) { + $adapter->report('info', 'Message ' . $i); + } + + $this->assertCount(1001, $adapter->getMessages()); + } + + public function testReportKeepsTheLastThousandMessagesByDefault(): void + { + for ($i = 1; $i <= 1001; $i++) { + $this->adapter->report('info', 'Message ' . $i); + } + + $messages = $this->adapter->getMessages(); + + $this->assertCount(1000, $messages); + $this->assertSame(['info' => 'Message 2'], $messages[0]); + $this->assertSame(['info' => 'Message 1001'], $messages[999]); + } } diff --git a/tests/Unit/Logger/Factories/LoggerFactoryTest.php b/tests/Unit/Logger/Factories/LoggerFactoryTest.php index 077d26160..496edb863 100644 --- a/tests/Unit/Logger/Factories/LoggerFactoryTest.php +++ b/tests/Unit/Logger/Factories/LoggerFactoryTest.php @@ -10,6 +10,7 @@ use Quantum\Logger\Adapters\DailyAdapter; use Quantum\Logger\Enums\LoggerType; use Quantum\Tests\Unit\AppTestCase; +use Quantum\App\Enums\AppType; use Quantum\Logger\Logger; use Quantum\Di\Di; @@ -69,6 +70,61 @@ public function testLoggerFactoryGetMessageAdapter(): void $this->assertInstanceOf(ReportableInterface::class, $logger->getAdapter()); } + public function testLoggerFactoryPassesTheMessageLimitFromTheConfig(): void + { + config()->set('logging.message.limit', 2); + config()->set('app.debug', true); + + $logger = LoggerFactory::get(); + + $logger->info('First'); + $logger->info('Second'); + $logger->info('Third'); + + $this->assertSame([['info' => 'Second'], ['info' => 'Third']], $logger->getAdapter()->getMessages()); + } + + public function testLoggerFactoryUsesTheDefaultMessageLimitWithoutTheKey(): void + { + config()->set('logging.message', []); + config()->set('app.debug', true); + + $logger = LoggerFactory::get(); + + for ($i = 1; $i <= 1001; $i++) { + $logger->info('Message ' . $i); + } + + $this->assertCount(1000, $logger->getAdapter()->getMessages()); + } + + public function testLoggerFactoryInConsoleDebugModeGetsTheDefaultAdapter(): void + { + $this->useAppType(AppType::CONSOLE); + + config()->set('app.debug', true); + + $this->assertInstanceOf(SingleAdapter::class, LoggerFactory::get()->getAdapter()); + } + + public function testLoggerFactoryInConsoleDebugModeGetsTheRequestedAdapter(): void + { + $this->useAppType(AppType::CONSOLE); + + config()->set('app.debug', true); + + $this->assertInstanceOf(DailyAdapter::class, LoggerFactory::get(LoggerType::DAILY)->getAdapter()); + } + + public function testLoggerFactoryInConsoleDebugModeGetsTheMessageAdapterWhenAsked(): void + { + $this->useAppType(AppType::CONSOLE); + + config()->set('app.debug', true); + + $this->assertInstanceOf(MessageAdapter::class, LoggerFactory::get(LoggerType::MESSAGE)->getAdapter()); + } + public function testLoggerFactoryTryingToGetMessageAdapter(): void { $this->expectException(LoggerException::class); diff --git a/tests/Unit/Mailer/Adapters/MailerAdapterTraitTest.php b/tests/Unit/Mailer/Adapters/MailerAdapterTraitTest.php index dbfba7a69..147dce1b8 100644 --- a/tests/Unit/Mailer/Adapters/MailerAdapterTraitTest.php +++ b/tests/Unit/Mailer/Adapters/MailerAdapterTraitTest.php @@ -128,4 +128,106 @@ protected function getTransportErrors(): array $this->assertSame(['Connection refused'], $adapter->getTransportLog()); $this->assertSame([['warning' => 'Connection refused']], logger()->getAdapter()->getMessages()); } + + public function testMailerTraitSendCutsTheTransportLogToItsLastLinesBeforeSending(): void + { + config()->set('mailer.transport_log_limit', 2); + + $adapter = $this->createFailingAdapter(1); + + for ($i = 1; $i <= 4; $i++) { + $adapter->send(); + } + + $this->assertSame(['Send 2 line 1', 'Send 3 line 1', 'Send 4 line 1'], $adapter->getTransportLog()); + } + + public function testMailerTraitSendKeepsEveryLineOfTheSendInProgress(): void + { + config()->set('mailer.transport_log_limit', 1); + + $adapter = $this->createFailingAdapter(3); + + $adapter->send(); + + $this->assertSame(['Send 1 line 1', 'Send 1 line 2', 'Send 1 line 3'], $adapter->getTransportLog()); + + $adapter->send(); + + $this->assertSame( + ['Send 1 line 3', 'Send 2 line 1', 'Send 2 line 2', 'Send 2 line 3'], + $adapter->getTransportLog() + ); + } + + public function testMailerTraitSendKeepsTheWholeTransportLogWithZeroLimit(): void + { + config()->set('mailer.transport_log_limit', 0); + + $adapter = $this->createFailingAdapter(1); + + for ($i = 1; $i <= 5; $i++) { + $adapter->send(); + } + + $this->assertCount(5, $adapter->getTransportLog()); + } + + public function testMailerTraitSendKeepsTheLastThousandLinesWithoutTheKey(): void + { + config()->delete('mailer.transport_log_limit'); + + $adapter = $this->createFailingAdapter(1001); + + $adapter->send(); + $adapter->send(); + + $log = $adapter->getTransportLog(); + + $this->assertCount(2001, $log); + $this->assertSame('Send 1 line 2', $log[0]); + } + + /** + * Creates an adapter whose every send fails with the given number of transport error lines + */ + private function createFailingAdapter(int $linesPerSend): MailerInterface + { + config()->set('mailer.mail_trap', false); + + return new class ($linesPerSend) implements MailerInterface { + use MailerTrait; + + private int $linesPerSend; + + private int $sends = 0; + + public function __construct(int $linesPerSend) + { + $this->linesPerSend = $linesPerSend; + } + + protected function prepare(): void + { + } + + protected function sendEmail(): bool + { + $this->sends++; + + return false; + } + + protected function getTransportErrors(): array + { + $lines = []; + + for ($i = 1; $i <= $this->linesPerSend; $i++) { + $lines[] = 'Send ' . $this->sends . ' line ' . $i; + } + + return $lines; + } + }; + } } diff --git a/tests/Unit/Mailer/Adapters/SmtpAdapterTest.php b/tests/Unit/Mailer/Adapters/SmtpAdapterTest.php index 30e86f423..873569386 100644 --- a/tests/Unit/Mailer/Adapters/SmtpAdapterTest.php +++ b/tests/Unit/Mailer/Adapters/SmtpAdapterTest.php @@ -5,6 +5,8 @@ use Quantum\Tests\Unit\Mailer\MailerTestCase; use Quantum\Mailer\Contracts\MailerInterface; use Quantum\Mailer\Adapters\SmtpAdapter; +use PHPMailer\PHPMailer\Exception; +use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\SMTP; use Quantum\Config\Setup; @@ -114,6 +116,110 @@ public function testSmtpAdapterKeepsTheSmtpExchangeInDebugMode(): void $this->assertSame(['CLIENT -> SERVER: EHLO localhost'], $adapter->getTransportLog()); } + public function testSmtpAdapterCutsTheSmtpExchangeToItsLastLinesAtTheNextSend(): void + { + config()->set('app.debug', true); + config()->set('mailer.transport_log_limit', 2); + + $adapter = new SmtpAdapter(config()->get('mailer.smtp')); + + $debugOutput = $this->getPrivateProperty($adapter, 'mailer')->Debugoutput; + + $debugOutput('CLIENT -> SERVER: EHLO localhost', 2); + $debugOutput('CLIENT -> SERVER: MAIL FROM:', 2); + $debugOutput('CLIENT -> SERVER: QUIT', 2); + + $adapter->setFrom('john@hotmail.com', 'John Doe'); + $adapter->setAddress('benny@gmail.com', 'Benny'); + $adapter->setBody('Lorem ipsum dolor sit amet'); + + $this->assertTrue($adapter->send()); + + $this->assertSame( + ['CLIENT -> SERVER: MAIL FROM:', 'CLIENT -> SERVER: QUIT'], + $adapter->getTransportLog() + ); + } + + public function testSmtpAdapterKeepsTheSmtpExchangeOfAFailedSendAndCutsItAtTheNextSend(): void + { + config()->set('app.debug', true); + config()->set('mailer.mail_trap', false); + config()->set('mailer.transport_log_limit', 1); + + $adapter = new SmtpAdapter(config()->get('mailer.smtp')); + + $failingMailer = new class (true) extends PHPMailer { + public int $sends = 0; + + public function send() + { + $this->sends++; + + call_user_func($this->Debugoutput, 'Send ' . $this->sends . ': CLIENT -> SERVER: EHLO localhost', 2); + call_user_func($this->Debugoutput, 'Send ' . $this->sends . ': SERVER -> CLIENT: 554 Rejected', 2); + + throw new Exception('SMTP Error: data not accepted.'); + } + }; + + $failingMailer->Debugoutput = $this->getPrivateProperty($adapter, 'mailer')->Debugoutput; + $this->setPrivateProperty($adapter, 'mailer', $failingMailer); + + for ($i = 1; $i <= 2; $i++) { + $adapter->setFrom('john@hotmail.com', 'John Doe'); + $adapter->setAddress('benny@gmail.com', 'Benny'); + + try { + $adapter->send(); + $this->fail('Expected the SMTP send to throw'); + } catch (Exception $e) { + $this->assertSame('SMTP Error: data not accepted.', $e->getMessage()); + } + } + + $this->assertSame([ + 'Send 1: SERVER -> CLIENT: 554 Rejected', + 'Send 2: CLIENT -> SERVER: EHLO localhost', + 'Send 2: SERVER -> CLIENT: 554 Rejected', + ], $adapter->getTransportLog()); + } + + public function testSmtpAdapterSendsThroughTheTransportWhenMailTrapIsTheTextFalse(): void + { + config()->set('mailer.mail_trap', 'false'); + + $adapter = new SmtpAdapter(config()->get('mailer.smtp')); + + $this->setPrivateProperty($adapter, 'mailer', new class (true) extends PHPMailer { + public function send() + { + throw new Exception('Sent through the transport'); + } + }); + + $adapter->setFrom('john@hotmail.com', 'John Doe'); + $adapter->setAddress('benny@gmail.com', 'Benny'); + $adapter->setBody('Lorem ipsum dolor sit amet'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Sent through the transport'); + + $adapter->send(); + } + + public function testSmtpAdapterSavesToTheMailTrapWhenMailTrapIsTheTextTrue(): void + { + config()->set('mailer.mail_trap', 'true'); + + $this->adapter->setFrom('john@hotmail.com', 'John Doe'); + $this->adapter->setAddress('benny@gmail.com', 'Benny'); + $this->adapter->setSubject('Lorem'); + $this->adapter->setBody('Lorem ipsum dolor sit amet'); + + $this->assertTrue($this->adapter->send()); + } + public function testSmtpAdapterKeepsNoSmtpExchangeOutsideDebugMode(): void { config()->set('app.debug', false); diff --git a/tests/Unit/RateLimit/Adapters/RedisRateLimitAdapterTest.php b/tests/Unit/RateLimit/Adapters/RedisRateLimitAdapterTest.php index 075a1c0c9..aea28a14f 100644 --- a/tests/Unit/RateLimit/Adapters/RedisRateLimitAdapterTest.php +++ b/tests/Unit/RateLimit/Adapters/RedisRateLimitAdapterTest.php @@ -25,6 +25,17 @@ public function tearDown(): void $this->adapter->reset('k2'); } + public function testRedisAdapterConnectsWithThePortAsText(): void + { + $adapter = new RedisRateLimitAdapter([ + 'host' => '127.0.0.1', + 'port' => '6379', + 'ttl' => 30, + ]); + + $this->assertTrue($adapter->hit('k2', 5, 30)); + } + public function testRedisAdapterHitAndResetFlow(): void { $this->assertTrue($this->adapter->hit('k2', 1, 60)); diff --git a/tests/Unit/Storage/Uploads/UploadConfigProviderTest.php b/tests/Unit/Storage/Uploads/UploadConfigProviderTest.php index ea13bdb1e..79fc7fcdd 100644 --- a/tests/Unit/Storage/Uploads/UploadConfigProviderTest.php +++ b/tests/Unit/Storage/Uploads/UploadConfigProviderTest.php @@ -25,7 +25,7 @@ public function testMissingOptionalUploadConfigReturnsEmptyMap(): void $context = App::getContext(); try { - App::setContext(new AppContext(PROJECT_ROOT . DS . 'cron-command-tests-empty', $context->getContainer())); + App::setContext(new AppContext(PROJECT_ROOT . DS . 'cron-command-tests-empty', $context->getContainer(), $context->getType())); $this->assertSame([], (new UploadConfigProvider())->getAllowedMimeTypesMap()); $this->assertFalse(config()->has('uploads')); diff --git a/tests/Unit/Tracer/ErrorHandlerTest.php b/tests/Unit/Tracer/ErrorHandlerTest.php index 3c12df59a..9ff87fe84 100644 --- a/tests/Unit/Tracer/ErrorHandlerTest.php +++ b/tests/Unit/Tracer/ErrorHandlerTest.php @@ -6,6 +6,7 @@ use Quantum\Tracer\WebExceptionRenderer; use Quantum\Tests\Unit\AppTestCase; use Quantum\Tracer\ErrorHandler; +use Quantum\App\Enums\AppType; use Quantum\Logger\Logger; use ReflectionException; use ErrorException; @@ -92,6 +93,8 @@ public function testHandleErrorReturnsFalseForSuppressedErrors(): void public function testHandleExceptionCliWritesExceptionMessage(): void { + $this->useAppType(AppType::CONSOLE); + config()->set('app.debug', false); $output = new BufferedOutput(); @@ -104,6 +107,8 @@ public function testHandleExceptionCliWritesExceptionMessage(): void public function testHandleExceptionCliInDebugModeWritesVerboseDiagnostics(): void { + $this->useAppType(AppType::CONSOLE); + config()->set('app.debug', true); $output = new BufferedOutput(); @@ -117,6 +122,21 @@ public function testHandleExceptionCliInDebugModeWritesVerboseDiagnostics(): voi $this->assertStringContainsString(__FILE__, $display); } + public function testHandleExceptionInWebAppRendersTheErrorPage(): void + { + config()->set('app.debug', true); + + $output = new BufferedOutput(); + $this->errorHandler->setCliOutput($output); + + ob_start(); + $this->errorHandler->handleException(new Exception('Web app failure')); + ob_get_clean(); + + $this->assertStringContainsString('Error :: Web app failure', response()->getContent()); + $this->assertSame('', $output->fetch()); + } + public function testHandleWebExceptionInDebugModeRendersTraceView(): void { config()->set('app.debug', true); diff --git a/tests/_root/shared/config/logging.php b/tests/_root/shared/config/logging.php index 0fff5b72b..1d5261020 100644 --- a/tests/_root/shared/config/logging.php +++ b/tests/_root/shared/config/logging.php @@ -12,4 +12,8 @@ 'path' => logs_dir(), 'level' => 'warning', ], + + 'message' => [ + 'limit' => 1000, + ], ]; diff --git a/tests/_root/shared/config/mailer.php b/tests/_root/shared/config/mailer.php index 87b0fc3a2..1869c8130 100644 --- a/tests/_root/shared/config/mailer.php +++ b/tests/_root/shared/config/mailer.php @@ -5,6 +5,8 @@ 'mail_trap' => true, + 'transport_log_limit' => 1000, + 'smtp' => [ 'host' => '127.0.0.1', 'secure' => 'ssl',