Search code examples
phpsymfonyphpunitbasic-authenticationapi-platform.com

Basic Authentication in phpunit test case


I came up with a briliant idea of using Basic Auth in my small script since I thought it's gonna be faster than using jwt ... well I was wrong. I cannot test my endpoint now because I constantly get 401.

Source code from below is also available here: https://github.com/tarach/blog-api/tree/feature/posts-resource-test

public function testShouldCreatePost(): void
    {
        $client = static::createClient();

        $headers = [
            'Content-Type' => 'application/json',
            'Authorization' => 'Basic ' . base64_encode('test:qwe123'),
        ];

        dump($headers);

        $client->request('POST', '/api/posts', [
            'headers' => $headers,
            'json' => [],
        ]);

        dump([
            'response' => [
                'status' => $client->getResponse()->getStatusCode(),
                'body' => $client->getResponse()->getContent(),
            ],
        ]);
    }

enter image description here

security.yaml

security:
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    encoders:
        App\Infrastructure\Symfony\User\EnvironmentUser:
            algorithm: plaintext
    providers:
        environment_user_provider:
            id: App\Infrastructure\Symfony\User\EnvironmentUserProvider
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            pattern: ^/api
            stateless: true
            anonymous: true
            provider: environment_user_provider
            http_basic:
                realm: Protected

    access_control:
         - { path: ^/api/posts, methods: ["GET"], roles: IS_AUTHENTICATED_ANONYMOUSLY }
         - { path: ^/api/docs, roles: IS_AUTHENTICATED_ANONYMOUSLY }
         - { path: ^/api,       roles: IS_AUTHENTICATED_FULLY }

EnvironmentUserProvider uses two env. variables USER_NAME and USER_PASSWORD. via Postman it works correctly: enter image description here

Solution:

public function testShouldCreatePost(): void
    {
        // Method A)
        $client = static::createClient([], [
            'PHP_AUTH_USER' => 'test',
            'PHP_AUTH_PW'   => 'qwe123',
        ]);

        // Method B)
        $client->setServerParameter('HTTP_AUTHORIZATION', 'Basic ' . base64_encode('test:qwe123'));

        $content = json_encode([
            'title' => 'Some title',
            'body' => '<strong>Some Body</strong>'
        ]);

        $client->request(
            'POST',
            '/api/posts',
            [],
            [],
            [
                'CONTENT_TYPE' => 'application/json',
            ],
            $content
        );

        $this->assertEquals(201, $client->getResponse()->getStatusCode());
    }

Solution

  • If you use a recent Symfony version (5.1 or higher) there is a neat helper on client that you can use:

    $client = static::createClient();
    $userRepository = static::$container->get(UserRepository::class);
    
    // retrieve the test user
    $testUser = $userRepository->findOneByEmail('john.doe@example.com');
    
    // simulate $testUser being logged in
    $client->loginUser($testUser);
    
    // test e.g. the profile page
    $client->request('GET', '/profile');
    

    Example taken from the docs for Logging in Users in Tests.

    If you use basic auth, you can pass in the credentials for the auth dialog like this:

    $client = static::createClient([], [
        'PHP_AUTH_USER' => 'username',
        'PHP_AUTH_PW'   => 'pa$$word',
    ]);
    

    As seen in the old docs for How to Simulate HTTP Authentication in a Functional Test.

    If you want to submit the headers from your example, things are a bit tricky because you will need to pass in the internal representation as can be seen in the BrowserKit Browser.

    In your case the headers should probably change like this:

    $headers = [
        'CONTENT_TYPE' => 'application/json',
        'HTTP_AUTHORIZATION' => 'Basic ' . base64_encode('test:qwe123'),
    ];
    

    You can see the which internal representation Symfony uses in the code for the HttpFoundation ServerBag.