Add v2.5.0 release preparation tests and update CHANGELOG

This commit is contained in:
Younes ENNAJI
2026-03-07 18:28:24 +00:00
parent e4337b0b63
commit e0766c1198
27 changed files with 1175 additions and 47 deletions
+27 -11
View File
@@ -1,20 +1,34 @@
# CHANGELOG for 2.x
## [Unreleased](https://github.com/php-flasher/php-flasher/compare/v2.1.4...2.x)
## [Unreleased](https://github.com/php-flasher/php-flasher/compare/v2.2.0...2.x)
## [v2.5.0](https://github.com/php-flasher/php-flasher/compare/v2.1.4...v2.2.0) - 2026-03-07
### Added
* feature [Laravel] Improve Laravel Octane support by resetting FallbackSession static storage between requests to prevent notification leakage
* feature [Symfony] Add FrankenPHP/Swoole/RoadRunner support with WorkerListener that implements ResetInterface and is tagged with kernel.reset
* feature [Symfony] Add reset() method to FallbackSession for long-running process support
* feature [Flasher] Add Hotwire/Turbo Drive support with turbo:before-cache event listener to clean up notifications before page caching
* fix [Flasher] Fix potential runtime error in Envelope::toArray() when no PresentableStampInterface stamps exist
* fix [Flasher] Use more specific \Random\RandomException in IdStamp instead of broad \Exception
* fix [Flasher] Update Livewire navigation cleanup to use correct .fl-wrapper selector instead of unused .fl-no-cache class
* feature [Flasher] Add event dispatching system for all notification adapters and themes with Livewire integration:
- [Toastr] Dispatch events: `flasher:toastr:click`, `flasher:toastr:close`, `flasher:toastr:show`, `flasher:toastr:hidden`
- [Noty] Dispatch events: `flasher:noty:click`, `flasher:noty:close`, `flasher:noty:show`, `flasher:noty:hover`
- [Notyf] Dispatch events: `flasher:notyf:click`, `flasher:notyf:dismiss`
- [Themes] Dispatch events: `flasher:theme:click` (generic) and `flasher:theme:{name}:click` (specific)
- [Laravel] Add LivewireListener classes for all adapters and themes to enable Livewire event handling
* feature [Flasher] Add 16 notification themes: Amazon, Amber, Jade, Crystal, and more
* feature [DX] Add `@method` annotations to FlasherInterface and NotificationFactoryInterface for better IDE autocompletion
* feature [DX] Add Type::all() and Type::isValid() helper methods with PHPStan type narrowing
* feature [DX] Add `@throws` annotations to FlasherContainer methods for better exception documentation
* feature [DX] Add FlasherContainer::setContainer() method as convenient alias for testing
* feature [DX] Add PHPStan type alias `NotificationType` for valid notification types
### Fixed
* fix [SweetAlert] Fix SweetAlertBuilder::question() bug where options parameter was being ignored
* fix [Flasher] Fix potential runtime error in Envelope::toArray() when no PresentableStampInterface stamps exist
* fix [Flasher] Use more specific \Random\RandomException in IdStamp instead of broad \Exception
* fix [Flasher] Update Livewire navigation cleanup to use correct .fl-wrapper selector instead of unused .fl-no-cache class
* fix [Flasher] Fix FilterCriteria uninitialized property error when constructed with empty array
* fix [Flasher] Fix null comparison issues in PriorityCriteria, HopsCriteria, and DelayCriteria that relied on PHP's implicit null-to-0 coercion
* fix [Flasher] Add type validation for callable factory return values in NotificationFactoryLocator with descriptive error messages
@@ -28,13 +42,15 @@
* fix [Flasher] Add type validation for callable presenter return values in ResponseManager with descriptive error messages
* fix [Flasher] Fix FlasherPlugin::normalizePlugins() losing scripts/styles when both top-level and plugin-level configs are provided - replaced array union operator with array_merge
* fix [Flasher] Simplify FlasherPlugin::normalizeFlashBag() by replacing redundant array union with direct array_merge
* fix [Flasher] Standardize exception message format in PresetNotFoundException to use brackets like other exceptions
* fix [Flasher] Standardize exception message wording in CriteriaNotRegisteredException to use "not found" instead of "is not found"
* feature [DX] Add `@method` annotations to FlasherInterface and NotificationFactoryInterface for better IDE autocompletion
* feature [DX] Add Type::all() and Type::isValid() helper methods with PHPStan type narrowing
* feature [DX] Add `@throws` annotations to FlasherContainer methods for better exception documentation
* feature [DX] Add FlasherContainer::setContainer() method as convenient alias for testing
* feature [DX] Add PHPStan type alias `NotificationType` for valid notification types
* fix [Flasher] Fix array_merge() syntax in InstallCommand by removing unnecessary empty array
* fix [Flasher] Add return type validation in FilterCriteria::apply() with proper exception handling
### Changed
* refactor [Flasher] Reduce theme configuration duplication by generating themes programmatically
* refactor [Flasher] Standardize exception message format in PresetNotFoundException to use brackets like other exceptions
* refactor [Flasher] Standardize exception message wording in CriteriaNotRegisteredException to use "not found" instead of "is not found"
* refactor [SweetAlert] Replace @phpstan-ignore-line suppressions with proper @var annotations
## [v2.1.3](https://github.com/php-flasher/php-flasher/compare/v2.1.2...v2.1.3) - 2025-01-25
+1 -1
View File
@@ -29,7 +29,7 @@
"require": {
"php": ">=8.2",
"illuminate/support": "^11.0|^12.0|^13.0",
"php-flasher/flasher": "^2.4.0"
"php-flasher/flasher": "^2.5.0"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -28,8 +28,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher-laravel": "^2.4.0",
"php-flasher/flasher-noty": "^2.4.0"
"php-flasher/flasher-laravel": "^2.5.0",
"php-flasher/flasher-noty": "^2.5.0"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@flasher/flasher-noty",
"version": "2.4.0",
"version": "2.5.0",
"type": "module",
"license": "MIT",
"main": "dist/flasher-noty.cjs.js",
@@ -11,7 +11,7 @@
"ncu": "ncu -u"
},
"peerDependencies": {
"@flasher/flasher": "^2.4.0",
"@flasher/flasher": "^2.5.0",
"noty": "^3.2.0-beta-deprecated"
}
}
+1 -1
View File
@@ -33,7 +33,7 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher": "^2.4.0"
"php-flasher/flasher": "^2.5.0"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -28,8 +28,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher-noty": "^2.4.0",
"php-flasher/flasher-symfony": "^2.4.0"
"php-flasher/flasher-noty": "^2.5.0",
"php-flasher/flasher-symfony": "^2.5.0"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -29,8 +29,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher-laravel": "^2.4.0",
"php-flasher/flasher-notyf": "^2.4.0"
"php-flasher/flasher-laravel": "^2.5.0",
"php-flasher/flasher-notyf": "^2.5.0"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@flasher/flasher-notyf",
"version": "2.4.0",
"version": "2.5.0",
"type": "module",
"license": "MIT",
"main": "dist/flasher-notyf.cjs.js",
@@ -11,7 +11,7 @@
"ncu": "ncu -u"
},
"peerDependencies": {
"@flasher/flasher": "^2.4.0",
"@flasher/flasher": "^2.5.0",
"notyf": "^3.10.0"
}
}
+1 -1
View File
@@ -33,7 +33,7 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher": "^2.4.0"
"php-flasher/flasher": "^2.5.0"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -29,8 +29,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher-notyf": "^2.4.0",
"php-flasher/flasher-symfony": "^2.4.0"
"php-flasher/flasher-notyf": "^2.5.0",
"php-flasher/flasher-symfony": "^2.5.0"
},
"autoload": {
"psr-4": {
+1 -1
View File
@@ -18,7 +18,7 @@ final readonly class Flasher implements FlasherInterface
{
use ForwardsCalls;
public const VERSION = '2.4.0';
public const VERSION = '2.5.0';
public function __construct(
private string $default,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@flasher/flasher",
"version": "2.4.0",
"version": "2.5.0",
"type": "module",
"license": "MIT",
"main": "dist/flasher.cjs.js",
@@ -45,10 +45,7 @@ final class FilterCriteria implements CriteriaInterface
$result = $callback($envelopes);
if (!\is_array($result)) {
throw new \InvalidArgumentException(\sprintf(
'Filter callback must return an array, got "%s".',
get_debug_type($result)
));
throw new \InvalidArgumentException(\sprintf('Filter callback must return an array, got "%s".', get_debug_type($result)));
}
/** @var Envelope[] $result */
+2 -2
View File
@@ -30,8 +30,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher-laravel": "^2.4.0",
"php-flasher/flasher-sweetalert": "^2.4.0"
"php-flasher/flasher-laravel": "^2.5.0",
"php-flasher/flasher-sweetalert": "^2.5.0"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@flasher/flasher-sweetalert",
"version": "2.4.0",
"version": "2.5.0",
"type": "module",
"license": "MIT",
"main": "dist/flasher-sweetalert.cjs.js",
@@ -11,7 +11,7 @@
"ncu": "ncu -u"
},
"peerDependencies": {
"@flasher/flasher": "^2.4.0",
"@flasher/flasher": "^2.5.0",
"sweetalert2": "^11.6.13"
}
}
+1 -1
View File
@@ -33,7 +33,7 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher": "^2.4.0"
"php-flasher/flasher": "^2.5.0"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -30,8 +30,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher-sweetalert": "^2.4.0",
"php-flasher/flasher-symfony": "^2.4.0"
"php-flasher/flasher-sweetalert": "^2.5.0",
"php-flasher/flasher-symfony": "^2.5.0"
},
"autoload": {
"psr-4": {
+1 -1
View File
@@ -28,7 +28,7 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher": "^2.4.0",
"php-flasher/flasher": "^2.5.0",
"symfony/config": "^7.0|^8.0",
"symfony/console": "^7.0|^8.0",
"symfony/dependency-injection": "^7.0|^8.0",
+2 -2
View File
@@ -29,8 +29,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher-laravel": "^2.4.0",
"php-flasher/flasher-toastr": "^2.4.0"
"php-flasher/flasher-laravel": "^2.5.0",
"php-flasher/flasher-toastr": "^2.5.0"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@flasher/flasher-toastr",
"version": "2.4.0",
"version": "2.5.0",
"type": "module",
"license": "MIT",
"main": "dist/flasher-toastr.cjs.js",
@@ -11,7 +11,7 @@
"ncu": "ncu -u"
},
"peerDependencies": {
"@flasher/flasher": "^2.4.0",
"@flasher/flasher": "^2.5.0",
"toastr": "^2.1.4"
},
"devDependencies": {
+1 -1
View File
@@ -33,7 +33,7 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher": "^2.4.0"
"php-flasher/flasher": "^2.5.0"
},
"autoload": {
"psr-4": {
+2 -2
View File
@@ -29,8 +29,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php-flasher/flasher-symfony": "^2.4.0",
"php-flasher/flasher-toastr": "^2.4.0"
"php-flasher/flasher-symfony": "^2.5.0",
"php-flasher/flasher-toastr": "^2.5.0"
},
"autoload": {
"psr-4": {
+239
View File
@@ -0,0 +1,239 @@
<?php
declare(strict_types=1);
namespace Flasher\Tests\Prime;
use Flasher\Prime\Configuration;
use PHPUnit\Framework\TestCase;
final class ConfigurationTest extends TestCase
{
public function testFromWithEmptyArray(): void
{
$config = ['default' => 'flasher'];
$result = Configuration::from($config);
$this->assertSame($config, $result);
}
public function testFromWithMinimalConfig(): void
{
$config = [
'default' => 'toastr',
];
$result = Configuration::from($config);
$this->assertSame($config, $result);
$this->assertSame('toastr', $result['default']);
}
public function testFromWithFullConfig(): void
{
$config = [
'default' => 'flasher',
'main_script' => '/assets/flasher.min.js',
'scripts' => ['/assets/plugin1.js', '/assets/plugin2.js'],
'styles' => ['/assets/flasher.min.css'],
'inject_assets' => true,
'translate' => true,
'excluded_paths' => ['/admin', '/api'],
'options' => ['timeout' => 5000, 'position' => 'top-right'],
'filter' => ['limit' => 5],
];
$result = Configuration::from($config);
$this->assertSame($config, $result);
$this->assertSame('flasher', $result['default']);
$this->assertSame('/assets/flasher.min.js', $result['main_script']);
$this->assertCount(2, $result['scripts']);
$this->assertCount(1, $result['styles']);
$this->assertTrue($result['inject_assets']);
$this->assertTrue($result['translate']);
$this->assertCount(2, $result['excluded_paths']);
$this->assertSame(5000, $result['options']['timeout']);
$this->assertSame(5, $result['filter']['limit']);
}
public function testFromWithPluginsConfig(): void
{
$config = [
'default' => 'flasher',
'plugins' => [
'toastr' => [
'scripts' => ['/assets/toastr.min.js'],
'styles' => ['/assets/toastr.min.css'],
'options' => ['closeButton' => true],
],
'sweetalert' => [
'scripts' => ['/assets/sweetalert.min.js'],
'styles' => ['/assets/sweetalert.min.css'],
'options' => ['showConfirmButton' => false],
],
],
];
$result = Configuration::from($config);
$this->assertSame($config, $result);
$this->assertArrayHasKey('plugins', $result);
$this->assertArrayHasKey('toastr', $result['plugins']);
$this->assertArrayHasKey('sweetalert', $result['plugins']);
$this->assertSame(['/assets/toastr.min.js'], $result['plugins']['toastr']['scripts']);
$this->assertTrue($result['plugins']['toastr']['options']['closeButton']);
}
public function testFromWithPresetsConfig(): void
{
$config = [
'default' => 'flasher',
'presets' => [
'entity_saved' => [
'type' => 'success',
'title' => 'Saved',
'message' => 'Entity has been saved successfully.',
'options' => ['timeout' => 3000],
],
'entity_deleted' => [
'type' => 'error',
'title' => 'Deleted',
'message' => 'Entity has been deleted.',
'options' => ['timeout' => 5000],
],
],
];
$result = Configuration::from($config);
$this->assertSame($config, $result);
$this->assertArrayHasKey('presets', $result);
$this->assertArrayHasKey('entity_saved', $result['presets']);
$this->assertSame('success', $result['presets']['entity_saved']['type']);
$this->assertSame('Saved', $result['presets']['entity_saved']['title']);
$this->assertSame('Entity has been saved successfully.', $result['presets']['entity_saved']['message']);
$this->assertSame(3000, $result['presets']['entity_saved']['options']['timeout']);
}
public function testFromWithFlashBagConfig(): void
{
$config = [
'default' => 'flasher',
'flash_bag' => [
'success' => ['success', 'ok'],
'error' => ['error', 'danger', 'fail'],
'warning' => ['warning', 'warn'],
'info' => ['info', 'notice'],
],
];
$result = Configuration::from($config);
$this->assertSame($config, $result);
$this->assertArrayHasKey('flash_bag', $result);
$this->assertIsArray($result['flash_bag']);
$this->assertSame(['success', 'ok'], $result['flash_bag']['success']);
$this->assertSame(['error', 'danger', 'fail'], $result['flash_bag']['error']);
}
public function testFromWithFlashBagDisabled(): void
{
$config = [
'default' => 'flasher',
'flash_bag' => false,
];
$result = Configuration::from($config);
$this->assertSame($config, $result);
$this->assertFalse($result['flash_bag']);
}
public function testFromWithThemesConfig(): void
{
$config = [
'default' => 'flasher',
'plugins' => [
'theme.amazon' => [
'scripts' => ['/assets/themes/amazon.js'],
'styles' => ['/assets/themes/amazon.css'],
'options' => ['position' => 'bottom-right'],
],
'theme.bootstrap' => [
'scripts' => ['/assets/themes/bootstrap.js'],
'styles' => ['/assets/themes/bootstrap.css'],
'options' => ['position' => 'top-center'],
],
],
];
$result = Configuration::from($config);
$this->assertSame($config, $result);
$this->assertArrayHasKey('plugins', $result);
$this->assertArrayHasKey('theme.amazon', $result['plugins']);
$this->assertArrayHasKey('theme.bootstrap', $result['plugins']);
}
public function testFromPreservesConfigValues(): void
{
$originalConfig = [
'default' => 'noty',
'main_script' => '/custom/path/flasher.js',
'scripts' => ['/custom/script1.js'],
'styles' => ['/custom/style1.css'],
'inject_assets' => false,
'translate' => false,
'excluded_paths' => ['/api/v1', '/api/v2'],
'options' => [
'timeout' => 10000,
'position' => 'bottom-left',
'closeButton' => true,
],
'filter' => [
'limit' => 10,
'orderBy' => 'priority',
],
'presets' => [
'my_preset' => [
'type' => 'info',
'title' => 'Info',
'message' => 'This is info',
'options' => [],
],
],
'plugins' => [
'noty' => [
'scripts' => ['/noty.js'],
'styles' => ['/noty.css'],
'options' => ['layout' => 'topRight'],
],
],
];
$result = Configuration::from($originalConfig);
// Verify exact pass-through behavior
$this->assertSame($originalConfig, $result);
// Verify no mutation occurred
$this->assertSame('noty', $result['default']);
$this->assertFalse($result['inject_assets']);
$this->assertFalse($result['translate']);
$this->assertSame(10000, $result['options']['timeout']);
$this->assertSame('topRight', $result['plugins']['noty']['options']['layout']);
}
public function testFromReturnsArrayByReference(): void
{
$config = ['default' => 'flasher'];
$result1 = Configuration::from($config);
$result2 = Configuration::from($config);
// Both should be equal
$this->assertSame($result1, $result2);
}
}
+238
View File
@@ -0,0 +1,238 @@
<?php
declare(strict_types=1);
namespace Flasher\Tests\Prime;
use Flasher\Prime\Container\FlasherContainer;
use Flasher\Prime\Factory\NotificationFactoryLocatorInterface;
use Flasher\Prime\Flasher;
use Flasher\Prime\FlasherInterface;
use Flasher\Prime\Notification\Envelope;
use Flasher\Prime\Notification\Type;
use Flasher\Prime\Response\ResponseManagerInterface;
use Flasher\Prime\Storage\StorageManagerInterface;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use PHPUnit\Framework\TestCase;
use function Flasher\Prime\flash as namespacedFlash;
final class FunctionsTest extends TestCase
{
use MockeryPHPUnitIntegration;
private FlasherInterface $flasher;
protected function setUp(): void
{
parent::setUp();
$factoryLocator = \Mockery::mock(NotificationFactoryLocatorInterface::class);
$responseManager = \Mockery::mock(ResponseManagerInterface::class);
$storageManager = \Mockery::mock(StorageManagerInterface::class);
$storageManager->allows('add')->andReturnUsing(fn ($envelope) => $envelope);
$this->flasher = new Flasher('flasher', $factoryLocator, $responseManager, $storageManager);
FlasherContainer::reset();
FlasherContainer::setContainer($this->flasher);
}
protected function tearDown(): void
{
FlasherContainer::reset();
parent::tearDown();
}
public function testFlashWithNoArgumentsReturnsFlasherInterface(): void
{
$result = namespacedFlash();
$this->assertInstanceOf(FlasherInterface::class, $result);
}
public function testFlashWithMessageReturnsEnvelope(): void
{
$result = namespacedFlash('Hello World');
$this->assertInstanceOf(Envelope::class, $result);
$this->assertSame('Hello World', $result->getMessage());
$this->assertSame(Type::SUCCESS, $result->getType());
}
public function testFlashWithAllArgumentsReturnsEnvelope(): void
{
$result = namespacedFlash(
'Operation completed',
Type::INFO,
['timeout' => 5000],
'Custom Title'
);
$this->assertInstanceOf(Envelope::class, $result);
$this->assertSame('Operation completed', $result->getMessage());
$this->assertSame(Type::INFO, $result->getType());
$this->assertSame('Custom Title', $result->getTitle());
$this->assertSame(5000, $result->getOption('timeout'));
}
public function testFlashWithDifferentTypes(): void
{
$successResult = namespacedFlash('Success message', Type::SUCCESS);
$this->assertSame(Type::SUCCESS, $successResult->getType());
$errorResult = namespacedFlash('Error message', Type::ERROR);
$this->assertSame(Type::ERROR, $errorResult->getType());
$warningResult = namespacedFlash('Warning message', Type::WARNING);
$this->assertSame(Type::WARNING, $warningResult->getType());
$infoResult = namespacedFlash('Info message', Type::INFO);
$this->assertSame(Type::INFO, $infoResult->getType());
}
public function testFlashWithOptions(): void
{
$options = [
'timeout' => 3000,
'position' => 'top-right',
'closeButton' => true,
];
$result = namespacedFlash('Test message', Type::SUCCESS, $options);
$this->assertInstanceOf(Envelope::class, $result);
$this->assertSame(3000, $result->getOption('timeout'));
$this->assertSame('top-right', $result->getOption('position'));
$this->assertTrue($result->getOption('closeButton'));
}
public function testFlashWithTitle(): void
{
$result = namespacedFlash('Message body', Type::SUCCESS, [], 'Notification Title');
$this->assertInstanceOf(Envelope::class, $result);
$this->assertSame('Notification Title', $result->getTitle());
$this->assertSame('Message body', $result->getMessage());
}
public function testNamespacedFlashFunction(): void
{
// Test that the namespaced function exists and works
$this->assertTrue(\function_exists('Flasher\Prime\flash'));
$result = namespacedFlash();
$this->assertInstanceOf(FlasherInterface::class, $result);
$envelope = namespacedFlash('Test message');
$this->assertInstanceOf(Envelope::class, $envelope);
}
public function testGlobalFlashFunction(): void
{
// Ensure the global function exists
$this->assertTrue(\function_exists('flash'));
$result = flash();
$this->assertInstanceOf(FlasherInterface::class, $result);
$envelope = flash('Test message');
$this->assertInstanceOf(Envelope::class, $envelope);
}
public function testBothFunctionsReturnSameResult(): void
{
// Both functions should use the same container and return equivalent results
$namespacedResult = namespacedFlash();
$globalResult = flash();
// Both should return the same FlasherInterface instance
$this->assertInstanceOf(FlasherInterface::class, $namespacedResult);
$this->assertInstanceOf(FlasherInterface::class, $globalResult);
// Create notifications with both functions
$namespacedEnvelope = namespacedFlash('Test message', Type::SUCCESS);
$globalEnvelope = flash('Test message', Type::SUCCESS);
// Both should produce Envelopes with the same structure
$this->assertInstanceOf(Envelope::class, $namespacedEnvelope);
$this->assertInstanceOf(Envelope::class, $globalEnvelope);
$this->assertSame('Test message', $namespacedEnvelope->getMessage());
$this->assertSame('Test message', $globalEnvelope->getMessage());
$this->assertSame(Type::SUCCESS, $namespacedEnvelope->getType());
$this->assertSame(Type::SUCCESS, $globalEnvelope->getType());
}
public function testFlashWithEmptyMessage(): void
{
$result = namespacedFlash('');
$this->assertInstanceOf(Envelope::class, $result);
$this->assertSame('', $result->getMessage());
}
public function testFlashWithNullMessage(): void
{
// When message is null and func_num_args is 0, returns FlasherInterface
// When message is explicitly null but args > 0, should still flash
$result = namespacedFlash(null);
// Based on the function signature, passing null should trigger the envelope creation
$this->assertInstanceOf(Envelope::class, $result);
}
public function testFlashWithUnicodeMessage(): void
{
$unicodeMessage = 'Hello';
$result = namespacedFlash($unicodeMessage, Type::INFO, [], 'Title');
$this->assertInstanceOf(Envelope::class, $result);
$this->assertSame($unicodeMessage, $result->getMessage());
$this->assertSame('Title', $result->getTitle());
}
public function testFlashWithSpecialCharactersInMessage(): void
{
$specialMessage = '<script>alert("XSS")</script> & "quotes" \'single\'';
$result = namespacedFlash($specialMessage);
$this->assertInstanceOf(Envelope::class, $result);
$this->assertSame($specialMessage, $result->getMessage());
}
public function testFlashWithEmptyOptions(): void
{
$result = namespacedFlash('Message', Type::SUCCESS, []);
$this->assertInstanceOf(Envelope::class, $result);
// Options should be empty or have default values
$options = $result->getOptions();
$this->assertIsArray($options);
}
public function testFlashWithNullTitle(): void
{
$result = namespacedFlash('Message', Type::ERROR, [], null);
$this->assertInstanceOf(Envelope::class, $result);
// Title should be empty string when null is passed
$this->assertSame('', $result->getTitle());
}
public function testFlashChainability(): void
{
// When called without arguments, should return FlasherInterface
// which can be used for chaining
$flasher = namespacedFlash();
$this->assertInstanceOf(FlasherInterface::class, $flasher);
// Should be able to use the flasher methods
$envelope = $flasher->success('Success message');
$this->assertInstanceOf(Envelope::class, $envelope);
}
}
@@ -519,4 +519,248 @@ final class ContentSecurityPolicyHandlerTest extends TestCase
$resultCsp = $headers['Content-Security-Policy'];
$this->assertStringNotContainsString('; ;', $resultCsp);
}
public function testHandlesCsp3Directives(): void
{
$this->nonceGeneratorMock->method('generate')->willReturn('csp3nonce');
$this->requestMock->method('hasHeader')->willReturn(false);
$headers = [];
$this->responseMock->method('hasHeader')->willReturnCallback(function ($name) use (&$headers) {
return isset($headers[$name]);
});
$this->responseMock->method('getHeader')->willReturnCallback(function ($name) use (&$headers) {
return $headers[$name] ?? null;
});
$this->responseMock->method('setHeader')->willReturnCallback(function ($name, $value) use (&$headers) {
$headers[$name] = $value;
});
$this->responseMock->method('removeHeader')->willReturnCallback(function ($name) use (&$headers) {
unset($headers[$name]);
});
// CSP Level 3 directives
$headers['Content-Security-Policy'] = "script-src 'self'; worker-src 'self'; navigate-to 'self'";
$nonces = $this->cspHandler->updateResponseHeaders($this->requestMock, $this->responseMock);
$this->assertNotEmpty($nonces);
$this->assertArrayHasKey('csp_script_nonce', $nonces);
// The handler should add nonces to script-src but leave worker-src and navigate-to alone
$resultCsp = $headers['Content-Security-Policy'];
$this->assertStringContainsString("'nonce-csp3nonce'", $resultCsp);
}
public function testHandlesMultipleSourcesInDirective(): void
{
$this->nonceGeneratorMock->method('generate')->willReturn('multisourcenonce');
$this->requestMock->method('hasHeader')->willReturn(false);
$headers = [];
$this->responseMock->method('hasHeader')->willReturnCallback(function ($name) use (&$headers) {
return isset($headers[$name]);
});
$this->responseMock->method('getHeader')->willReturnCallback(function ($name) use (&$headers) {
return $headers[$name] ?? null;
});
$this->responseMock->method('setHeader')->willReturnCallback(function ($name, $value) use (&$headers) {
$headers[$name] = $value;
});
$this->responseMock->method('removeHeader')->willReturnCallback(function ($name) use (&$headers) {
unset($headers[$name]);
});
// CSP with multiple sources in a single directive
$headers['Content-Security-Policy'] = "script-src 'self' https://cdn.example.com https://api.example.com data: blob:";
$nonces = $this->cspHandler->updateResponseHeaders($this->requestMock, $this->responseMock);
$this->assertNotEmpty($nonces);
$resultCsp = $headers['Content-Security-Policy'];
// Should preserve existing sources and add nonce
$this->assertStringContainsString("'self'", $resultCsp);
$this->assertStringContainsString('https://cdn.example.com', $resultCsp);
$this->assertStringContainsString('https://api.example.com', $resultCsp);
$this->assertStringContainsString('data:', $resultCsp);
$this->assertStringContainsString('blob:', $resultCsp);
$this->assertStringContainsString("'nonce-multisourcenonce'", $resultCsp);
}
public function testHandlesVeryLongCspHeader(): void
{
$this->nonceGeneratorMock->method('generate')->willReturn('longnonce');
$this->requestMock->method('hasHeader')->willReturn(false);
$headers = [];
$this->responseMock->method('hasHeader')->willReturnCallback(function ($name) use (&$headers) {
return isset($headers[$name]);
});
$this->responseMock->method('getHeader')->willReturnCallback(function ($name) use (&$headers) {
return $headers[$name] ?? null;
});
$this->responseMock->method('setHeader')->willReturnCallback(function ($name, $value) use (&$headers) {
$headers[$name] = $value;
});
$this->responseMock->method('removeHeader')->willReturnCallback(function ($name) use (&$headers) {
unset($headers[$name]);
});
// Generate a very long CSP header with many directives
$domains = [];
for ($i = 0; $i < 50; ++$i) {
$domains[] = "https://cdn{$i}.example.com";
}
$longCsp = "script-src 'self' ".implode(' ', $domains)."; style-src 'self' ".implode(' ', $domains);
$headers['Content-Security-Policy'] = $longCsp;
$nonces = $this->cspHandler->updateResponseHeaders($this->requestMock, $this->responseMock);
$this->assertNotEmpty($nonces);
$resultCsp = $headers['Content-Security-Policy'];
// Should handle the long header without issues
$this->assertStringContainsString("'nonce-longnonce'", $resultCsp);
// Verify multiple domains are preserved
$this->assertStringContainsString('https://cdn0.example.com', $resultCsp);
$this->assertStringContainsString('https://cdn49.example.com', $resultCsp);
}
public function testResetClearsAllState(): void
{
$this->nonceGeneratorMock->method('generate')->willReturn('statetestnonce');
// First, disable CSP and verify it's disabled
$this->cspHandler->disableCsp();
$removedHeaders = [];
$this->responseMock->method('removeHeader')->willReturnCallback(function ($headerName) use (&$removedHeaders) {
$removedHeaders[] = $headerName;
});
$nonces = $this->cspHandler->updateResponseHeaders($this->requestMock, $this->responseMock);
$this->assertSame([], $nonces, 'CSP should be disabled');
// Reset should clear all state including the disabled flag
$this->cspHandler->reset();
// Create fresh mocks for the second call
$request2 = $this->createMock(RequestInterface::class);
$response2 = $this->createMock(ResponseInterface::class);
$request2->method('hasHeader')->willReturn(false);
$setHeaders = [];
$response2->method('hasHeader')->willReturn(false);
$response2->method('setHeader')->willReturnCallback(function ($headerName, $value) use (&$setHeaders) {
$setHeaders[$headerName] = $value;
});
// After reset, CSP should be enabled again and generate nonces
$nonces = $this->cspHandler->updateResponseHeaders($request2, $response2);
$this->assertNotEmpty($nonces, 'CSP should be enabled after reset');
$this->assertArrayHasKey('csp_script_nonce', $nonces);
$this->assertArrayHasKey('csp_style_nonce', $nonces);
$this->assertSame('statetestnonce', $nonces['csp_script_nonce']);
$this->assertSame('statetestnonce', $nonces['csp_style_nonce']);
}
public function testHandlesScriptSrcElemAndStyleSrcElemDirectives(): void
{
$this->nonceGeneratorMock->method('generate')->willReturn('elemnonce');
$this->requestMock->method('hasHeader')->willReturn(false);
$headers = [];
$this->responseMock->method('hasHeader')->willReturnCallback(function ($name) use (&$headers) {
return isset($headers[$name]);
});
$this->responseMock->method('getHeader')->willReturnCallback(function ($name) use (&$headers) {
return $headers[$name] ?? null;
});
$this->responseMock->method('setHeader')->willReturnCallback(function ($name, $value) use (&$headers) {
$headers[$name] = $value;
});
$this->responseMock->method('removeHeader')->willReturnCallback(function ($name) use (&$headers) {
unset($headers[$name]);
});
// CSP with script-src-elem and style-src-elem (CSP Level 3)
$headers['Content-Security-Policy'] = "script-src-elem 'self'; style-src-elem 'self'";
$nonces = $this->cspHandler->updateResponseHeaders($this->requestMock, $this->responseMock);
$this->assertNotEmpty($nonces);
$resultCsp = $headers['Content-Security-Policy'];
// Both directives should have nonces added
$this->assertStringContainsString("'nonce-elemnonce'", $resultCsp);
}
public function testHandlesMultipleCspHeaders(): void
{
$this->nonceGeneratorMock->method('generate')->willReturn('multinonce');
$this->requestMock->method('hasHeader')->willReturn(false);
$headers = [];
$this->responseMock->method('hasHeader')->willReturnCallback(function ($name) use (&$headers) {
return isset($headers[$name]);
});
$this->responseMock->method('getHeader')->willReturnCallback(function ($name) use (&$headers) {
return $headers[$name] ?? null;
});
$this->responseMock->method('setHeader')->willReturnCallback(function ($name, $value) use (&$headers) {
$headers[$name] = $value;
});
$this->responseMock->method('removeHeader')->willReturnCallback(function ($name) use (&$headers) {
unset($headers[$name]);
});
// Multiple CSP headers (standard, report-only, and legacy X- prefix)
$headers['Content-Security-Policy'] = "script-src 'self'";
$headers['Content-Security-Policy-Report-Only'] = "script-src 'self'";
$headers['X-Content-Security-Policy'] = "script-src 'self'";
$nonces = $this->cspHandler->updateResponseHeaders($this->requestMock, $this->responseMock);
$this->assertNotEmpty($nonces);
// All three headers should be updated with nonces
$this->assertStringContainsString("'nonce-multinonce'", $headers['Content-Security-Policy']);
$this->assertStringContainsString("'nonce-multinonce'", $headers['Content-Security-Policy-Report-Only']);
$this->assertStringContainsString("'nonce-multinonce'", $headers['X-Content-Security-Policy']);
}
public function testNoncesPersistAcrossMultipleCalls(): void
{
$callCount = 0;
$this->nonceGeneratorMock->method('generate')->willReturnCallback(function () use (&$callCount) {
return 'nonce'.++$callCount;
});
// First call - nonces from request headers
$this->requestMock->method('hasHeader')->willReturnCallback(function ($headerName) {
return \in_array($headerName, ['X-PHPFlasher-Script-Nonce', 'X-PHPFlasher-Style-Nonce']);
});
$this->requestMock->method('getHeader')->willReturnCallback(function ($headerName) {
return 'X-PHPFlasher-Script-Nonce' === $headerName ? 'existing-script-nonce' : 'existing-style-nonce';
});
$nonces = $this->cspHandler->getNonces($this->requestMock);
$this->assertSame([
'csp_script_nonce' => 'existing-script-nonce',
'csp_style_nonce' => 'existing-style-nonce',
], $nonces);
// The nonces from headers should be used, not generated ones
$this->assertSame(0, $callCount, 'No nonces should be generated when headers exist');
}
}
+114
View File
@@ -88,4 +88,118 @@ final class RequestExtensionTest extends TestCase
$this->assertSame($expectedFlatMapping, $flatMappingProperty->getValue($extension), 'Mapping should be flattened correctly.');
}
public function testProcessMultipleMessagesInSingleBag(): void
{
$messages = ['First message', 'Second message', 'Third message'];
$this->request->expects()->hasSession()->andReturns(true);
$this->request->allows('hasType')->andReturnUsing(fn ($type) => 'happy' === $type);
$this->request->allows('getType')->andReturnUsing(fn ($type) => 'happy' === $type ? $messages : []);
// Should flash all three messages
$this->flasher->expects()->flash('success', 'First message')->once();
$this->flasher->expects()->flash('success', 'Second message')->once();
$this->flasher->expects()->flash('success', 'Third message')->once();
$this->request->expects()->forgetType('happy')->once();
$extension = new RequestExtension($this->flasher, $this->mapping);
$extension->flash($this->request, $this->response);
}
public function testProcessMessagesWithSpecialCharacters(): void
{
$specialMessage = '<script>alert("XSS")</script> & "quotes" \'single\' < > &amp;';
$this->request->expects()->hasSession()->andReturns(true);
$this->request->allows('hasType')->andReturnUsing(fn ($type) => 'happy' === $type);
$this->request->allows('getType')->andReturnUsing(fn ($type) => 'happy' === $type ? [$specialMessage] : []);
$this->flasher->expects()->flash('success', $specialMessage)->once();
$this->request->expects()->forgetType('happy')->once();
$extension = new RequestExtension($this->flasher, $this->mapping);
$extension->flash($this->request, $this->response);
}
public function testProcessMessagesWithUnicodeContent(): void
{
$unicodeMessages = [
'Hello',
'Bonjour',
'Hola',
];
$this->request->expects()->hasSession()->andReturns(true);
$this->request->allows('hasType')->andReturnUsing(fn ($type) => 'happy' === $type);
$this->request->allows('getType')->andReturnUsing(fn ($type) => 'happy' === $type ? $unicodeMessages : []);
foreach ($unicodeMessages as $message) {
$this->flasher->expects()->flash('success', $message)->once();
}
$this->request->expects()->forgetType('happy')->once();
$extension = new RequestExtension($this->flasher, $this->mapping);
$extension->flash($this->request, $this->response);
}
public function testProcessEmptyMessageValue(): void
{
$this->request->expects()->hasSession()->andReturns(true);
$this->request->allows('hasType')->andReturnUsing(fn ($type) => 'happy' === $type);
$this->request->allows('getType')->andReturnUsing(fn ($type) => 'happy' === $type ? [''] : []);
// Empty string message should still be flashed
$this->flasher->expects()->flash('success', '')->once();
$this->request->expects()->forgetType('happy')->once();
$extension = new RequestExtension($this->flasher, $this->mapping);
$extension->flash($this->request, $this->response);
}
public function testProcessNullMessageValue(): void
{
$this->request->expects()->hasSession()->andReturns(true);
$this->request->allows('hasType')->andReturnUsing(fn ($type) => 'happy' === $type);
$this->request->allows('getType')->andReturnUsing(fn ($type) => 'happy' === $type ? [null] : []);
// Null values in the messages array should be passed through
$this->flasher->expects()->flash('success', null)->once();
$this->request->expects()->forgetType('happy')->once();
$extension = new RequestExtension($this->flasher, $this->mapping);
$extension->flash($this->request, $this->response);
}
public function testProcessMixedMessageTypes(): void
{
// Messages can include strings, nulls, and empty strings
$mixedMessages = ['Valid message', '', 'Another valid message'];
$this->request->expects()->hasSession()->andReturns(true);
$this->request->allows('hasType')->andReturnUsing(fn ($type) => 'happy' === $type);
$this->request->allows('getType')->andReturnUsing(fn ($type) => 'happy' === $type ? $mixedMessages : []);
$this->flasher->expects()->flash('success', 'Valid message')->once();
$this->flasher->expects()->flash('success', '')->once();
$this->flasher->expects()->flash('success', 'Another valid message')->once();
$this->request->expects()->forgetType('happy')->once();
$extension = new RequestExtension($this->flasher, $this->mapping);
$extension->flash($this->request, $this->response);
}
public function testProcessStringMessageAsArray(): void
{
// getType can return a string which gets cast to array
$this->request->expects()->hasSession()->andReturns(true);
$this->request->allows('hasType')->andReturnUsing(fn ($type) => 'happy' === $type);
$this->request->allows('getType')->andReturnUsing(fn ($type) => 'happy' === $type ? 'Single string message' : []);
$this->flasher->expects()->flash('success', 'Single string message')->once();
$this->request->expects()->forgetType('happy')->once();
$extension = new RequestExtension($this->flasher, $this->mapping);
$extension->flash($this->request, $this->response);
}
}
+280
View File
@@ -549,4 +549,284 @@ final class ResponseExtensionTest extends TestCase
set_error_handler($previousHandler);
}
}
public function testRenderWithUnicodeContent(): void
{
$flasher = \Mockery::mock(FlasherInterface::class);
$cspHandler = \Mockery::mock(ContentSecurityPolicyHandlerInterface::class);
$request = \Mockery::mock(RequestInterface::class);
$response = \Mockery::mock(ResponseInterface::class);
$unicodeHtmlResponse = '<div></div>';
$contentBefore = '<!DOCTYPE html><html><head></head><body>'.HtmlPresenter::BODY_END_PLACE_HOLDER.'</body></html>';
$cspHandler->allows()->updateResponseHeaders($request, $response)->andReturn([]);
$flasher->allows()->render('html', [], \Mockery::any())->andReturn($unicodeHtmlResponse);
$request->allows([
'isXmlHttpRequest' => false,
'isHtmlRequestFormat' => true,
]);
$response->allows([
'isSuccessful' => true,
'isHtml' => true,
'isRedirection' => false,
'isAttachment' => false,
'isJson' => false,
'getContent' => $contentBefore,
'setContent' => \Mockery::on(function ($content) use ($unicodeHtmlResponse) {
$this->assertStringContainsString($unicodeHtmlResponse, $content);
return true;
}),
]);
$responseExtension = new ResponseExtension($flasher, $cspHandler);
$result = $responseExtension->render($request, $response);
$this->assertInstanceOf(ResponseInterface::class, $result);
}
public function testRenderWithSpecialHtmlCharacters(): void
{
$flasher = \Mockery::mock(FlasherInterface::class);
$cspHandler = \Mockery::mock(ContentSecurityPolicyHandlerInterface::class);
$request = \Mockery::mock(RequestInterface::class);
$response = \Mockery::mock(ResponseInterface::class);
$specialHtmlResponse = '<div class="flasher" data-message="Test &amp; &lt;script&gt;">Notification</div>';
$contentBefore = '<html><body>Content with &amp; special <characters>'.HtmlPresenter::BODY_END_PLACE_HOLDER.'</body></html>';
$cspHandler->allows()->updateResponseHeaders($request, $response)->andReturn([]);
$flasher->allows()->render('html', [], \Mockery::any())->andReturn($specialHtmlResponse);
$request->allows([
'isXmlHttpRequest' => false,
'isHtmlRequestFormat' => true,
]);
$response->allows([
'isSuccessful' => true,
'isHtml' => true,
'isRedirection' => false,
'isAttachment' => false,
'isJson' => false,
'getContent' => $contentBefore,
]);
$response->expects('setContent')->once()->with(\Mockery::on(function ($content) use ($specialHtmlResponse) {
$this->assertStringContainsString($specialHtmlResponse, $content);
$this->assertStringContainsString('&amp; special', $content);
return true;
}));
$responseExtension = new ResponseExtension($flasher, $cspHandler);
$responseExtension->render($request, $response);
}
public function testRenderWithVeryLargeResponseBody(): void
{
$flasher = \Mockery::mock(FlasherInterface::class);
$cspHandler = \Mockery::mock(ContentSecurityPolicyHandlerInterface::class);
$request = \Mockery::mock(RequestInterface::class);
$response = \Mockery::mock(ResponseInterface::class);
$htmlResponse = '<div>Flasher notification</div>';
// Create a large content body (simulate a large HTML page)
$largeContent = str_repeat('<p>Lorem ipsum dolor sit amet</p>', 10000);
$contentBefore = '<html><body>'.$largeContent.HtmlPresenter::BODY_END_PLACE_HOLDER.'</body></html>';
$cspHandler->allows()->updateResponseHeaders($request, $response)->andReturn([]);
$flasher->allows()->render('html', [], \Mockery::any())->andReturn($htmlResponse);
$request->allows([
'isXmlHttpRequest' => false,
'isHtmlRequestFormat' => true,
]);
$response->allows([
'isSuccessful' => true,
'isHtml' => true,
'isRedirection' => false,
'isAttachment' => false,
'isJson' => false,
'getContent' => $contentBefore,
]);
$response->expects('setContent')->once()->with(\Mockery::on(function ($content) use ($htmlResponse, $largeContent, $contentBefore) {
$this->assertStringContainsString($htmlResponse, $content);
$this->assertStringContainsString($largeContent, $content);
// Verify content length increased by the HTML response
$this->assertGreaterThan(\strlen($contentBefore), \strlen($content));
return true;
}));
$responseExtension = new ResponseExtension($flasher, $cspHandler);
$responseExtension->render($request, $response);
}
public function testRenderWithMultiplePlaceholdersUsesLast(): void
{
$flasher = \Mockery::mock(FlasherInterface::class);
$cspHandler = \Mockery::mock(ContentSecurityPolicyHandlerInterface::class);
$request = \Mockery::mock(RequestInterface::class);
$response = \Mockery::mock(ResponseInterface::class);
$htmlResponse = '<div>Flasher notification</div>';
// Content with multiple placeholders - should use the last one found (BODY_END_PLACE_HOLDER)
$contentBefore = '<html>'.HtmlPresenter::HEAD_END_PLACE_HOLDER.'<body>content'.HtmlPresenter::BODY_END_PLACE_HOLDER.'</body></html>';
$cspHandler->allows()->updateResponseHeaders($request, $response)->andReturn([]);
$flasher->allows()->render('html', [], \Mockery::any())->andReturn($htmlResponse);
$request->allows([
'isXmlHttpRequest' => false,
'isHtmlRequestFormat' => true,
]);
$response->allows([
'isSuccessful' => true,
'isHtml' => true,
'isRedirection' => false,
'isAttachment' => false,
'isJson' => false,
'getContent' => $contentBefore,
]);
$response->expects('setContent')->once()->with(\Mockery::on(function ($content) use ($htmlResponse) {
// The injection should happen at the last placeholder found (strripos)
// Based on the code, it iterates through placeholders and uses the last position found
$this->assertStringContainsString($htmlResponse, $content);
return true;
}));
$responseExtension = new ResponseExtension($flasher, $cspHandler);
$responseExtension->render($request, $response);
}
public function testRenderPreservesContentEncoding(): void
{
$flasher = \Mockery::mock(FlasherInterface::class);
$cspHandler = \Mockery::mock(ContentSecurityPolicyHandlerInterface::class);
$request = \Mockery::mock(RequestInterface::class);
$response = \Mockery::mock(ResponseInterface::class);
$htmlResponse = '<div>Notification</div>';
// Content with various encodings and character sets
$contentBefore = '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"></head><body>Content with special chars: &copy; &trade; &nbsp;'.HtmlPresenter::BODY_END_PLACE_HOLDER.'</body></html>';
$cspHandler->allows()->updateResponseHeaders($request, $response)->andReturn([]);
$flasher->allows()->render('html', [], \Mockery::any())->andReturn($htmlResponse);
$request->allows([
'isXmlHttpRequest' => false,
'isHtmlRequestFormat' => true,
]);
$response->allows([
'isSuccessful' => true,
'isHtml' => true,
'isRedirection' => false,
'isAttachment' => false,
'isJson' => false,
'getContent' => $contentBefore,
]);
$response->expects('setContent')->once()->with(\Mockery::on(function ($content) {
// Verify HTML entities are preserved
$this->assertStringContainsString('&copy;', $content);
$this->assertStringContainsString('&trade;', $content);
$this->assertStringContainsString('&nbsp;', $content);
$this->assertStringContainsString('charset="UTF-8"', $content);
return true;
}));
$responseExtension = new ResponseExtension($flasher, $cspHandler);
$responseExtension->render($request, $response);
}
public function testRenderWithEmptyBody(): void
{
$flasher = \Mockery::mock(FlasherInterface::class);
$cspHandler = \Mockery::mock(ContentSecurityPolicyHandlerInterface::class);
$request = \Mockery::mock(RequestInterface::class);
$response = \Mockery::mock(ResponseInterface::class);
// Empty body but with placeholder
$contentBefore = HtmlPresenter::BODY_END_PLACE_HOLDER;
$htmlResponse = '<div>Flasher</div>';
$cspHandler->allows()->updateResponseHeaders($request, $response)->andReturn([]);
$flasher->allows()->render('html', [], \Mockery::any())->andReturn($htmlResponse);
$request->allows([
'isXmlHttpRequest' => false,
'isHtmlRequestFormat' => true,
]);
$response->allows([
'isSuccessful' => true,
'isHtml' => true,
'isRedirection' => false,
'isAttachment' => false,
'isJson' => false,
'getContent' => $contentBefore,
]);
$response->expects('setContent')->once()->with(\Mockery::on(function ($content) use ($htmlResponse) {
$this->assertStringContainsString($htmlResponse, $content);
return true;
}));
$responseExtension = new ResponseExtension($flasher, $cspHandler);
$responseExtension->render($request, $response);
}
public function testRenderWithFlasherReplaceMePlaceholder(): void
{
$flasher = \Mockery::mock(FlasherInterface::class);
$cspHandler = \Mockery::mock(ContentSecurityPolicyHandlerInterface::class);
$request = \Mockery::mock(RequestInterface::class);
$response = \Mockery::mock(ResponseInterface::class);
// Using FLASHER_REPLACE_ME placeholder triggers special handling
$contentBefore = 'content '.HtmlPresenter::FLASHER_REPLACE_ME.' more content';
$htmlResponse = '{"envelopes":[]}';
$cspHandler->allows()->updateResponseHeaders($request, $response)->andReturn([]);
// When FLASHER_REPLACE_ME is used, envelopes_only should be true
$flasher->expects()->render('html', [], \Mockery::on(function ($context) {
return true === $context['envelopes_only'];
}))->once()->andReturn($htmlResponse);
$request->allows([
'isXmlHttpRequest' => false,
'isHtmlRequestFormat' => true,
]);
$response->allows([
'isSuccessful' => true,
'isHtml' => true,
'isRedirection' => false,
'isAttachment' => false,
'isJson' => false,
'getContent' => $contentBefore,
]);
$response->expects('setContent')->once()->with(\Mockery::on(function ($content) {
// Should wrap with options.push()
$this->assertStringContainsString('options.push(', $content);
return true;
}));
$responseExtension = new ResponseExtension($flasher, $cspHandler);
$responseExtension->render($request, $response);
}
}