Valinor/tests/Integration/Mapping/Attribute/ObjectBuilderStrategyMappingTest.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

131 lines
3.6 KiB
PHP

<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Integration\Mapping\Attribute;
use Attribute;
use CuyZ\Valinor\Attribute\StaticMethodConstructor;
use CuyZ\Valinor\Definition\ClassDefinition;
use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\Mapper\Object\Exception\TooManyObjectBuilderFactoryAttributes;
use CuyZ\Valinor\Mapper\Object\Factory\ObjectBuilderFactory;
use CuyZ\Valinor\MapperBuilder;
use CuyZ\Valinor\Tests\Fake\Mapper\Object\FakeObjectBuilder;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeErrorMessage;
use CuyZ\Valinor\Tests\Integration\IntegrationTest;
final class ObjectBuilderStrategyMappingTest extends IntegrationTest
{
public function test_object_builder_attribute_is_used(): void
{
try {
$result = (new MapperBuilder())->mapper()->map(ObjectWithBuilderStrategyAttribute::class, [
'foo' => 'foo',
'bar' => 'bar',
]);
} catch (MappingError $error) {
$this->mappingFail($error);
}
self::assertSame('foo', $result->foo);
self::assertSame('bar', $result->bar);
self::assertTrue($result->staticConstructorCalled);
}
public function test_named_constructor_throwing_exception_is_caught_by_mapper(): void
{
try {
(new MapperBuilder())->mapper()->map(ObjectWithFailingBuilderStrategyAttribute::class, []);
} catch (MappingError $exception) {
$error = $exception->node()->messages()[0];
self::assertSame('1656076067', $error->code());
self::assertSame('some error message', (string)$error);
}
}
public function test_repeated_object_builder_factory_attributes_throws_exception(): void
{
$factoryClass = ObjectBuilderFactory::class;
$objectClass = ObjectWithSeveralBuilderStrategyAttributes::class;
$this->expectException(TooManyObjectBuilderFactoryAttributes::class);
$this->expectExceptionCode(1634044714);
$this->expectExceptionMessage("Only one attribute of type `$factoryClass` is allowed, class `$objectClass` contains 2.");
(new MapperBuilder())->mapper()->map($objectClass, 'foo');
}
}
/**
* @Annotation
*/
#[Attribute(Attribute::TARGET_CLASS)]
final class ForeignAttribute
{
}
/**
* @Annotation
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class ObjectBuilderStrategyAttribute implements ObjectBuilderFactory
{
public function for(ClassDefinition $class): iterable
{
return [new FakeObjectBuilder()];
}
}
/**
* @ForeignAttribute
* @StaticMethodConstructor("create")
*/
#[ForeignAttribute]
#[StaticMethodConstructor('create')]
final class ObjectWithBuilderStrategyAttribute
{
public bool $staticConstructorCalled = false;
public string $foo;
public string $bar;
private function __construct(string $foo, string $bar)
{
$this->foo = $foo;
$this->bar = $bar;
}
public static function create(string $foo, string $bar = 'optional value'): self
{
$instance = new self($foo, $bar);
$instance->staticConstructorCalled = true;
return $instance;
}
}
/**
* @StaticMethodConstructor("failingConstructor")
*/
#[StaticMethodConstructor('failingConstructor')]
final class ObjectWithFailingBuilderStrategyAttribute
{
public static function failingConstructor(): self
{
throw new FakeErrorMessage('some error message', 1656076067);
}
}
/**
* @ObjectBuilderStrategyAttribute
* @ObjectBuilderStrategyAttribute
*/
#[ObjectBuilderStrategyAttribute]
#[ObjectBuilderStrategyAttribute]
final class ObjectWithSeveralBuilderStrategyAttributes
{
}