skip to Main Content

I have a newly created Symfony 7 app and I have a DTO called: AuthCallbackDto

<?php

declare(strict_types=1);

namespace AppApplicationModelAuth;

use SymfonyComponentValidatorConstraints as Assert;

readonly class AuthCallbackDto
{
    public function __construct(
        #[AssertNotBlank(message: 'The `code` value cannot be blank')]
        #[AssertLength(min: 5)]
        private string $code,
        #[AssertNotBlank(message: 'The `session_state` value cannot be blank')]
        #[AssertLength(min: 5)]
        private string $session_state,
    ) {
    }

    public function getCode(): string
    {
        return $this->code;
    }

    public function getSessionState(): string
    {
        return $this->session_state;
    }
}

I use this in a controller like so:

<?php

declare(strict_types=1);

namespace AppApplicationControllerAuth;

use AppApplicationModelAuthAuthCallbackDto;
use SymfonyComponentHttpFoundationJsonResponse;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentHttpKernelAttributeAsController;
use SymfonyComponentHttpKernelAttributeMapQueryString;
use SymfonyComponentRoutingAttributeRoute;

#[AsController]
class Callback
{
    #[Route('/auth/callback', name: 'auth/callback', methods: ['GET'])]
    public function test(
        #[MapQueryString] AuthCallbackDto $authCallbackDto,
    ): Response {

        return new JsonResponse([
            'code' => $authCallbackDto->getCode(),
            'session_state' => $authCallbackDto->getSessionState()
        ]);
    }
}

If the code or session_state values are not present or < 5 characters then an error is thrown. This is good!

http://localhost:8000/auth/callback?code=oihiohoih&session_state=34f34f34f – Works (good)
http://localhost:8000/auth/callback?code=&session_state= – fails (good)
‘http://localhost:8000/auth/callback?code=123&session_state=123 – fails (good)

Feeling over confident, I then decided to write a very small unit test for the DTO class. However, I then noticed that the constraint attributes are not being applied when I run PHP Unit from the console. The exception in the test is never thrown.

Can anyone tell me what I am doing wrong or am I simply assuming too much?

Here is my unit test:

<?php

declare(strict_types=1);

namespace AppTestsUnitApplicationModelAuh;

use AppApplicationModelAuthAuthCallbackDto;
use PHPUnitFrameworkAttributesDataProvider;
use PHPUnitFrameworkTestCase;

class AuhCallbackDtoTest extends TestCase
{
    /**
     * @return array<array<int, string|bool>>
     */
    public static function dtoData(): array
    {
        return [
            [
                '',
                '',
                true,
            ],
            [
                'someCodeABC12344',
                'someSessionState',
                false,
            ],
        ];
    }

    #[DataProvider('dtoData')]
    public function testDtoCanBeCreated(string $code, string $sessionState, bool $exception): void
    {
        if ($exception) {
            $this->expectException(Exception::class);
        }

        $dto = new AuthCallbackDto($code, $sessionState);

        self::assertEquals($dto->getCode(), $code);
        self::assertEquals($dto->getSessionState(), $sessionState);
    }
}

I have tried debugging the validation rules using console and looks good:


bin/console debug:validator 'AppApplicationModelAuthAuthCallbackDto'


AppApplicationModelAuthAuthCallbackDto
------------------------------------------

+---------------+--------------------------------------------------+--------------------------+---------------------------------------------------------------------------------+
| Property      | Name                                             | Groups                   | Options                                                                         |
+---------------+--------------------------------------------------+--------------------------+---------------------------------------------------------------------------------+
| code          | property options                                 |                          | [                                                                               |
|               |                                                  |                          |   "cascadeStrategy" =>                                                          |
|               |                                                  |                          | "None",                                                                         |
|               |                                                  |                          |   "autoMappingStrategy" =>                                                      |
|               |                                                  |                          | "None",                                                                         |
|               |                                                  |                          |   "traversalStrategy" =>                                                        |
|               |                                                  |                          | "None"                                                                          |
|               |                                                  |                          | ]                                                                               |
| code          | SymfonyComponentValidatorConstraintsNotBlank | Default, AuthCallbackDto | [                                                                               |
|               |                                                  |                          |   "allowNull" =>                                                                |
|               |                                                  |                          | false,                                                                          |
|               |                                                  |                          |   "message" => "The `code`                                                      |
|               |                                                  |                          | value cannot be blank",                                                         |
|               |                                                  |                          |   "normalizer" =>                                                               |
|               |                                                  |                          | null,                                                                           |
|               |                                                  |                          |   "payload" =>                                                                  |
|               |                                                  |                          | null                                                                            |
|               |                                                  |                          | ]                                                                               |
| code          | SymfonyComponentValidatorConstraintsLength   | Default, AuthCallbackDto | [                                                                               |
|               |                                                  |                          |   "charset" =>                                                                  |
|               |                                                  |                          | "UTF-8",                                                                        |
|               |                                                  |                          |   "charsetMessage" => "This                                                     |
|               |                                                  |                          | value does not match the expected {{ charset }} charset.",                      |
|               |                                                  |                          |   "countUnit" =>                                                                |
|               |                                                  |                          | "codepoints",                                                                   |
|               |                                                  |                          |   "exactMessage" => "This                                                       |
|               |                                                  |                          | value should have exactly {{ limit }} character.|This value should have exactly |
|               |                                                  |                          | {{ limit }} characters.",                                                       |
|               |                                                  |                          |   "max" =>                                                                      |
|               |                                                  |                          | null,                                                                           |
|               |                                                  |                          |   "maxMessage" => "This value                                                   |
|               |                                                  |                          | is too long. It should have {{ limit }} character or less.|This value is too    |
|               |                                                  |                          | long. It should have {{ limit }} characters or less.",                          |
|               |                                                  |                          |   "min" => 5,                                                                   |
|               |                                                  |                          |   "minMessage" => "This value                                                   |
|               |                                                  |                          | is too short. It should have {{ limit }} character or more.|This value is too   |
|               |                                                  |                          | short. It should have {{ limit }} characters or more.",                         |
|               |                                                  |                          |   "normalizer" =>                                                               |
|               |                                                  |                          | null,                                                                           |
|               |                                                  |                          |   "payload" =>                                                                  |
|               |                                                  |                          | null                                                                            |
|               |                                                  |                          | ]                                                                               |
| session_state | property options                                 |                          | [                                                                               |
|               |                                                  |                          |   "cascadeStrategy" =>                                                          |
|               |                                                  |                          | "None",                                                                         |
|               |                                                  |                          |   "autoMappingStrategy" =>                                                      |
|               |                                                  |                          | "None",                                                                         |
|               |                                                  |                          |   "traversalStrategy" =>                                                        |
|               |                                                  |                          | "None"                                                                          |
|               |                                                  |                          | ]                                                                               |
| session_state | SymfonyComponentValidatorConstraintsNotBlank | Default, AuthCallbackDto | [                                                                               |
|               |                                                  |                          |   "allowNull" =>                                                                |
|               |                                                  |                          | false,                                                                          |
|               |                                                  |                          |   "message" => "The                                                             |
|               |                                                  |                          | `session_state` value cannot be blank",                                         |
|               |                                                  |                          |   "normalizer" =>                                                               |
|               |                                                  |                          | null,                                                                           |
|               |                                                  |                          |   "payload" =>                                                                  |
|               |                                                  |                          | null                                                                            |
|               |                                                  |                          | ]                                                                               |
| session_state | SymfonyComponentValidatorConstraintsLength   | Default, AuthCallbackDto | [                                                                               |
|               |                                                  |                          |   "charset" =>                                                                  |
|               |                                                  |                          | "UTF-8",                                                                        |
|               |                                                  |                          |   "charsetMessage" => "This                                                     |
|               |                                                  |                          | value does not match the expected {{ charset }} charset.",                      |
|               |                                                  |                          |   "countUnit" =>                                                                |
|               |                                                  |                          | "codepoints",                                                                   |
|               |                                                  |                          |   "exactMessage" => "This                                                       |
|               |                                                  |                          | value should have exactly {{ limit }} character.|This value should have exactly |
|               |                                                  |                          | {{ limit }} characters.",                                                       |
|               |                                                  |                          |   "max" =>                                                                      |
|               |                                                  |                          | null,                                                                           |
|               |                                                  |                          |   "maxMessage" => "This value                                                   |
|               |                                                  |                          | is too long. It should have {{ limit }} character or less.|This value is too    |
|               |                                                  |                          | long. It should have {{ limit }} characters or less.",                          |
|               |                                                  |                          |   "min" => 5,                                                                   |
|               |                                                  |                          |   "minMessage" => "This value                                                   |
|               |                                                  |                          | is too short. It should have {{ limit }} character or more.|This value is too   |
|               |                                                  |                          | short. It should have {{ limit }} characters or more.",                         |
|               |                                                  |                          |   "normalizer" =>                                                               |
|               |                                                  |                          | null,                                                                           |
|               |                                                  |                          |   "payload" =>                                                                  |
|               |                                                  |                          | null                                                                            |
|               |                                                  |                          | ]                                                                               |
+---------------+--------------------------------------------------+--------------------------+---------------------------------------------------------------------------------+

I can update the constructor of my DTO to have some standard Webmozart static assert calls but that will be unnecessary when the app runs as intended.

...
 public function __construct(
        #[AssertNotBlank]
        #[AssertLength(min: 5)]
        private string $code,
        #[AssertNotBlank]
        #[AssertLength(min: 5)]
        private string $session_state,
    ) {
        WebmozartAssertAssert::notEmpty($this->code);
        WebmozartAssertAssert::minLength($code, 5);

        WebmozartAssertAssert::notEmpty($this->session_state);
        WebmozartAssertAssert::minLength($this->session_state, 5);
    }
...

2

Answers


  1. Chosen as BEST ANSWER

    Thanks @Leroy, in the end I went with the WebTestCase approach:

    <?php
    
    declare(strict_types=1);
    
    namespace AppTestsUnitApplicationControllerAuth;
    
    use PHPUnitFrameworkAttributesDataProvider;
    use SymfonyBundleFrameworkBundleTestWebTestCase;
    use SymfonyComponentHttpFoundationRequest;
    
    class CallbackTest extends WebTestCase
    {
        /**
         * @return array<array<int, null|int|string>>
         */
        public static function dtoData(): array
        {
            return [
                [
                    '',
                    '',
                    404,
                    'This value should not be blank.',
                ],
                [
                    'a',
                    'b',
                    404,
                    'This value is too short. It should have 5 characters or more.',
                ],
                [
                    'someCodeABC12344',
                    'someSessionState',
                    200,
                    null,
                ],
            ];
        }
    
        #[DataProvider('dtoData')]
        public function testDtoCanBeCreated(string $code, string $sessionState, int $statusCode, ?string $errorMessage): void
        {
            $client = static::createClient();
    
            $client->request(
                method: Request::METHOD_GET,
                uri: '/auth/callback',
                parameters: ['code' => $code, 'session_state' => $sessionState],
            );
    
            $response = (string) $client->getResponse()->getContent();
            $constraints = substr($response, 0, (int) strpos($response, '<!DOCTYPE html>'));
    
            self::assertEquals($statusCode, $client->getResponse()->getStatusCode());
    
            if ($errorMessage !== null) {
                self::assertStringContainsString($errorMessage, $constraints);
            }
        }
    }
    
    

  2. Those Assert attributes comes from the Symfony Validator component. Php doesn’t automatically runs business logic on it. But it does do this in the framework.

    You need to pass this through the symfony validator.

    However, since you are testing a Controller, you could create a WebTestCase instead. You can (functionally) test your controllers by sending a request and validating the response. In short, run functional tests as if your application is a "black box".

    See this chapter for more information: https://symfony.com/doc/current/testing.html#write-your-first-application-test

    If you really not want this, and you really want to test your Dto. You have to validate your Dto using the Symfony Validator.

    class AuhCallbackDtoTest extends TestCase
    {
    
        public function testInvalidDto(): void
        {
            $validator = Validation::createValidator();
    
            $dto = new AuthCallbackDto("", "");
    
            $errors = $validator->validate($dto);
            
            $this->assertCount(2, (array)$errors);  // there are 2 errors
            // You can check specifically for the error and the error message too
        }
    }
    

    FYI, I haven’t really tested this. So the Validation::createValidator() may need some additional configuration to understand those constructor property attributes, but I’m not entirely sure.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search