Valinor/tests/Unit/MapperBuilderTest.php
Romain Canon 6ce1a439ad feat!: filter userland exceptions to hide potential sensible data
/!\ This change fixes a security issue.

Userland exception thrown in a constructor will not be automatically
caught by the mapper anymore. This prevents messages with sensible
information from reaching the final user — for instance an SQL exception
showing a part of a query.

To allow exceptions to be considered as safe, the new method
`MapperBuilder::filterExceptions()` must be used, with caution.

```php
final class SomeClass
{
    public function __construct(private string $value)
    {
        \Webmozart\Assert\Assert::startsWith($value, 'foo_');
    }
}

try {
    (new \CuyZ\Valinor\MapperBuilder())
        ->filterExceptions(function (Throwable $exception) {
            if ($exception instanceof \Webmozart\Assert\InvalidArgumentException) {
                return \CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage::from($exception);
            }

            // If the exception should not be caught by this library, it
            // must be thrown again.
            throw $exception;
        })
        ->mapper()
        ->map(SomeClass::class, 'bar_baz');
} catch (\CuyZ\Valinor\Mapper\MappingError $exception) {
    // Should print something similar to:
    // > Expected a value to start with "foo_". Got: "bar_baz"
    echo $exception->node()->messages()[0];
}
```
2022-07-08 13:58:48 +02:00

54 lines
1.8 KiB
PHP

<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit;
use CuyZ\Valinor\MapperBuilder;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeErrorMessage;
use DateTime;
use DateTimeInterface;
use PHPUnit\Framework\TestCase;
use stdClass;
use function sys_get_temp_dir;
final class MapperBuilderTest extends TestCase
{
private MapperBuilder $mapperBuilder;
protected function setUp(): void
{
parent::setUp();
$this->mapperBuilder = new MapperBuilder();
}
public function test_builder_methods_return_clone_of_builder_instance(): void
{
$builderA = $this->mapperBuilder;
$builderB = $builderA->infer(DateTimeInterface::class, static fn () => DateTime::class);
$builderC = $builderA->bind(static fn (): DateTime => new DateTime());
$builderD = $builderA->registerConstructor(static fn (): stdClass => new stdClass());
$builderE = $builderA->alter(static fn (string $value): string => 'foo');
$builderF = $builderA->flexible();
$builderG = $builderA->filterExceptions(fn () => new FakeErrorMessage());
$builderH = $builderA->withCacheDir(sys_get_temp_dir());
$builderI = $builderA->enableLegacyDoctrineAnnotations();
self::assertNotSame($builderA, $builderB);
self::assertNotSame($builderA, $builderC);
self::assertNotSame($builderA, $builderD);
self::assertNotSame($builderA, $builderE);
self::assertNotSame($builderA, $builderF);
self::assertNotSame($builderA, $builderG);
self::assertNotSame($builderA, $builderH);
self::assertNotSame($builderA, $builderI);
}
public function test_mapper_instance_is_the_same(): void
{
self::assertSame($this->mapperBuilder->mapper(), $this->mapperBuilder->mapper());
}
}