Skip to content

Testing Authority rules

Tortue Torche edited this page Mar 17, 2015 · 47 revisions

It can be difficult to thoroughly test user permissions at the functional/integration level because there are often many branching possibilities. Since AuthorityController handles all permission logic in a single AuthorityController config file this makes it easy to have a solid set of unit test for complete coverage.

The can() method can be called directly on any Authority (like you would in the controller or view) so it is easy to test permission logic.

// user should only destroy projects which he owns
public function testUserCanOnlyDestroyProjectsWhichHeOwns()
{
    // Disables mass assignment exceptions from being thrown from model inserts and updates
    Eloquent::unguard();

    $user = User::create([
                'email'     => 'user@localhost',
                'name'  => 'User',
                'password'  => Hash::make('password'),
            ]);
    $authority = App::make('authority');
    $authority->setCurrentUser($user);

    // You can set the 'allow' rule directly in the test or fallback to the rules defined in the Authority config file
    $rulesCount = $authority->getRules()->count();
    $authority->allow('destroy', 'Project', function($self, $project) {
        return $self->user()->id === $project->user_id;
    });
    $this->assertGreaterThan($rulesCount, $authority->getRules()->count());

    $this->assertTrue($authority->can('destroy', new Project(['user_id' => $user->id])));
    $this->assertTrue($authority->cannot('destroy', new Project));

    // Renables any ability to throw mass assignment exceptions
    Eloquent::reguard();
}

Controller Testing

If you want to test authorization functionality at the controller level one option is to log-in the user who has the appropriate permissions.

// Disables mass assignment exceptions from being thrown from model inserts and updates
Eloquent::unguard();

$user = User::create([
            'email'     => 'admin@localhost',
            'name'  => 'Administrator',
            'password'  => Hash::make('password'),
        ]); // I recommend a factory for this
$roleAdmin = Role::where('name', 'admin')->firstOrFail(); // Or Role::create(['name' => 'admin']);
$user->roles()->attach($roleAdmin->id);
$user->load('roles'); // Reload the model to update the roles association
Auth::login($user); // log in user however you like, alternatively stub Auth::guest() and $yourControllerInstance->getCurrentUser() method
$this->action('GET', "ProjectsController@index");
$this->assertViewHas('projects'); // render the view with the 'projects' variable, since it should have access

// Renables any ability to throw mass assignment exceptions
Eloquent::reguard();

Alternatively, if you want to test the controller behavior independently from what is inside the AuthorityController config file, it is easy to stub out the authority with any behavior you want. Then you can use the code below (keep in mind that this code is for the ProjectsController class):

<?php
// app/tests/controllers/ProjectsControllerTest.php

class ProjectsControllerTest extends TestCase
{
    public function setUp()
    {
        parent::setUp();
        Route::enableFilters(); // For Laravel 5.0 you MUST remove this line

        $user = Auth::user(); // Feel free to log a user here, like this: Auth::login($myUserInstance);
        $this->app['authority'] = new Efficiently\AuthorityController\Authority($user);
        $this->authority = App::make('authority');

        // Uncomment to load the rules of the default config file:
        // $fn = $this->app['config']->get('authority-controller::initialize');
        // if ($fn) {
        //     $fn($this->authority);
        // }
    }

    public function tearDown()
    {
        parent::tearDown();
    }

    // render index if have read authority on project
    public function testRenderIndexIfHaveReadAuthorityOnProject()
    {
        $this->authority->allow('read', 'Project');
        $response = $this->action('GET', "ProjectsController@index");
        $view = $response->original;
        $this->assertEquals($view->getName(), 'projects.index');
        $this->assertViewHas('projects');
    }

    //...
}

If you have very complex permissions it can lead to many branching possibilities. If these are all tested in the controller layer then it can lead to slow and bloated tests. Instead I recommend keeping controller authorization tests light and testing the authorization functionality more thoroughly in the Authority config file through unit tests as shown at the top.

Functional tests with Codeception

Codeception functional tests have some pitfalls with Laravel application lifecycle.

Source: http://codeception.com/docs/05-FunctionalTests#Shared-Memory

Authority-Controller uses Laravel events to inject authorizations into the Controllers. And with the functional tests of Codeception, who uses shared memory, the events of previous requests are still attached to controllers...

So here a guide to use functional tests with Authority-Controller

First of all, edit the codeception.yml file to enable route filters of Laravel:

actor: Tester
paths:
    tests: app/tests
    log: app/tests/_output
    data: app/tests/_data
    helpers: app/tests/_support
settings:
    bootstrap: _bootstrap.php
    colors: true
    memory_limit: 1024M
modules:
    config:
        Db:
            dsn: ''
            user: ''
            password: ''
            dump: app/tests/_data/dump.sql
        Laravel4:
            filters: true

Then, create a app/tests/_support/AuthorityControllerHelper.php file with the following code:

<?php namespace Codeception\Module;

class AuthorityControllerHelper extends \Codeception\Module
{
    public function amOnPage($page)
    {
        \BaseController::flushAuthorityEvents('*');
        return $this->getModule('Laravel4')->amOnPage($page);
    }

    public function amOnAction($action, $params = null)
    {
        \BaseController::flushAuthorityEvents('*');
        return $this->getModule('Laravel4')->amOnAction($action, $params);
    }

    public function amOnRoute($route, $params = null)
    {
        \BaseController::flushAuthorityEvents('*');
        return $this->getModule('Laravel4')->amOnRoute($route, $params);
    }

    public function sendAjaxGetRequest($uri, $params = null)
    {
        \BaseController::flushAuthorityEvents('*');
        return $this->getModule('Laravel4')->sendAjaxGetRequest($uri, $params);
    }

    public function click($link, $context = null)
    {
        \BaseController::flushAuthorityEvents('*');
        return $this->getModule('Laravel4')->click($link, $context);
    }

    public function sendAjaxPostRequest($uri, $params = null)
    {
        \BaseController::flushAuthorityEvents('*');
        return $this->getModule('Laravel4')->sendAjaxPostRequest($uri, $params);
    }

    public function sendAjaxRequest($method, $uri, $params = null)
    {
        \BaseController::flushAuthorityEvents('*');
        return $this->getModule('Laravel4')->sendAjaxRequest($method, $uri, $params);
    }

    public function submitForm($selector, $params)
    {
        \BaseController::flushAuthorityEvents('*');
        return $this->getModule('Laravel4')->submitForm($selector, $params);
    }
}

Require this helper module in your app/tests/functional.suite.yml file:

class_name: FunctionalTester
modules:
    enabled: [Laravel4, Filesystem, FunctionalHelper, AuthorityControllerHelper]

Finally run this command in a terminal:

codecept build

Enjoy :)

Codeception and Laravel 4.* Exceptions

Codeception version 2.0.5 and below is buggy with App::error() and all other handling errors system of Laravel 4.*.

See:

So if you test some unauthorized actions, you need this try and catch tweak:

$I = new FunctionalTester($scenario);
//...
$document = Document::first();
try {
    $I->amOnPage("/documents/".$document->id);
} catch (Efficiently\AuthorityController\Exceptions\AccessDenied $e) {
    $message = $e->getMessage();
    Session::flash('error', $message);
    $I->amOnPage('/');
}
$I->seeCurrentUrlEquals('/');
$I->seeSessionHasValues(['error' => 'You are not authorized to access this page.']);

Additional Docs

Clone this wiki locally