-
Notifications
You must be signed in to change notification settings - Fork 17
Testing Authority rules
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();
}
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.
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...
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 version 2.0.5 and below is buggy with App::error()
and all other handling errors system of Laravel 4.*.
See:
- http://laravel.io/forum/03-23-2014-codeception-laravel-and-exceptions
- https://github.com/Codeception/Codeception/issues/1227
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.']);