Search code examples
ajaxsymfonycsrf

Get the CSRF token in a test, "CSRF token is invalid" - functional ajax test


I'm trying to test an ajax request in Symfony2. I'm writing a unit test which is throwing the following error in my app/logs/test.log:

request.CRITICAL: Uncaught PHP Exception Twig_Error_Runtime: 
"Impossible to access an attribute ("0") on a string variable 
("The CSRF token is invalid. Please try to resubmit the form.")
in .../vendor/twig/twig/lib/Twig/Template.php:388

My code is fairly straight-forward.

public function testAjaxJsonResponse()
{
    $form['post']['title'] = 'test title';
    $form['post']['content'] = 'test content';
    $form['post']['_token'] = $client->getContainer()->get('form.csrf_provider')->generateCsrfToken();

    $client->request('POST', '/path/to/ajax/', $form, array(), array(
        'HTTP_X-Requested-With' => 'XMLHttpRequest',
    ));

    $response = $client->getResponse();
    $this->assertSame(200, $client->getResponse()->getStatusCode());
    $this->assertSame('application/json', $response->headers->get('Content-Type'));
}

The issue seems to be the CSRF token, I could disable it for the tests, but I don't really want to do that, I had it working by making 2 requests (the first one loads the page with the form, we grab the _token and make a second request using with XMLHttpRequest) - This obviously seems rather silly and inefficient!


Solution

  • Solution

    We can generate our own CSRF token for our ajax request with:

    $client->getContainer()->get('form.csrf_provider')->generateCsrfToken($intention);
    

    Here the variable $intention refers to an array key set in your Form Type Options.

    Add the intention

    In your Form Type you will need to add the intention key. e.g:

    # AcmeBundle\Form\Type\PostType.php
    
    /**
     *  Additional fields (if you want to edit them), the values shown are the default
     * 
     * 'csrf_protection' => true,
     * 'csrf_field_name' => '_token', // This must match in your test
     *
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Acme\AcmeBundle\Entity\Post',
            // a unique key to help generate the secret token
            'intention' => 'post_type',
        ));
    }
    

    Read the documentation

    Generate the CSRF Token in your Functional test

    Now we have that intention, we can use it in our unit test to generate a valid CSRF token.

    /**
     * Test Ajax JSON Response with CSRF Token
     * Example uses a `post` entity
     *
     * The PHP code returns `return new JsonResponse(true, 200);`
     */
    public function testAjaxJsonResponse()
    {
        // Form fields (make sure they pass validation!)
        $form['post']['title'] = 'test title';
        $form['post']['content'] = 'test content';
    
        // Create our CSRF token - with $intention = `post_type`
        $csrfToken = $client->getContainer()->get('form.csrf_provider')->generateCsrfToken('post_type');
        $form['post']['_token'] = $csrfToken; // Add it to your `csrf_field_name`
    
        // Simulate the ajax request
        $client->request('POST', '/path/to/ajax/', $form, array(), array(
            'HTTP_X-Requested-With' => 'XMLHttpRequest',
        ));
    
        // Test we get a valid JSON response
        $response = $client->getResponse();
        $this->assertSame(200, $client->getResponse()->getStatusCode());
        $this->assertSame('application/json', $response->headers->get('Content-Type'));
    
        // Assert the content
        $this->assertEquals('true', $response->getContent());
        $this->assertNotEmpty($client->getResponse()->getContent());
    }