mirror of
https://github.com/php-flasher/php-flasher.git
synced 2026-03-31 15:07:47 +01:00
Add v2.5.0 release preparation tests and update CHANGELOG
This commit is contained in:
+27
-11
@@ -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
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,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 */
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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\' < > &';
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 & <script>">Notification</div>';
|
||||
$contentBefore = '<html><body>Content with & 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('& 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: © ™ '.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('©', $content);
|
||||
$this->assertStringContainsString('™', $content);
|
||||
$this->assertStringContainsString(' ', $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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user