CakePHP & AuthComponent unit test doesn't appear to call isAuthorized() - unit-testing

I am writing unit tests for my UsersController so that users can only edit their own profile. I am using CakePHP 2.4.2 and AuthComponent with Controller authorize to do this.
Auth config:
public $components = array(
'Auth' => array(
'loginRedirect' => '/',
'logoutRedirect' => '/',
'authenticate' => array('Ldap'),
'authError' => 'You are not allowed to access this page',
'authorize' => 'Controller'));
isAuthorized() in UsersController:
public function isAuthorized($user = null) {
if($this->Auth->loggedIn()) {
return $this->request->params['pass'][0] == $user['id'];
}
return false;
}
Unit test for edit:
public function testEdit() {
$result = $this->testAction('/users/view/1', array('return' => 'view'));
$this->assertRegExp('/Adam C Hobaugh/', $result);
$user = $this->generate('Users', array(
'components' => array(
'Session',
'Auth' => array('user'))));
$test = array('id' => 1);
$user->Auth->expects($this->once())->method('loggedIn')
->with($this->returnValue(true));
$user->Auth->expects($this->any())->method('user')
->with($this->returnValue($test));
$user->Session->expects($this->any())->method('setFlash');
$result = $this->testAction('/users/edit/1', array(
'return' => 'headers',
'data' => array('User' => array( {user array} ))));
debug($result);
$this->assertContains('/users', #$result['Location']);
$result = $this->testAction('/users/view/1', array('return' => 'view'));
$this->assertRegExp('/John Jacob Doe/', $result);
}
I am getting Expectation failed for method name is equal to <string:loggedIn> when invoked 1 time(s). Method was expected to be called 1 times, actually called 0 times. when I run the test. Also when I changed this->once() to $this->any() and the id in the $test array to 2, a situation that should fail and does from the browser, it succeeds in passing the test.
With those combined, it appears that isAuthorized() is not being called during the unit test. I am at a loss. Thanks for any help that you could give.

First: In your test code you call the view method and not the edit method
Second: isAuthorized() is not for this. With this method you should just define who could acces what functions, there should not be any application business logic.
Third: If you want to limit normal users to edit just their own profile you should change the dit() method to something like this.
public function edit() {
$this->request->data['User']['id'] = $this->Auth->User('id');
if ($this->request->is('post') || $this->request->is('put')) {
if ($this->User->save($this->request->data)) {
$this->Session->setFlash(__('The user has been saved.'));
return $this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash(__('The user could not be saved. Please, try again.'));
}
} else {
$this->request->data = array('User' => $this->Auth->User());
}
}
And remove echo $this->Form->input('id'); from your edit view.
I am writing a book about this topic. It will be available here soon: https://leanpub.com/CakePHPUserAuthentication/

Related

CakePHP unittest mocked Auth component

Code
class AclRowLevelsController extends AppController {
public $components = array(
// Don't use same name as Model
'_AclRowLevel' => array('className' => 'AclRowLevel')
);
public function view() {
$this->_AclRowLevel->checkUser();
...
}
}
class AclRowLevelComponent extends Component {
public function initialize(Controller $controller) {
$this->controller = $controller;
$this->AclRowLevel = ClassRegistry::init('AclRowLevel');
}
public function checkUser($permission, $model) {
$row = $this->AclRowLevel->find('first', array(
'conditions' => array(
'model' => $model['model'],
'model_id' => $model['model_id'],
'user_id' => $this->controller->Auth->user('id')
)
));
}
}
class AclRowLevelsControllerTest extends ControllerTestCase {
public function testViewAccessAsManager() {
$AclRowLevels = $this->generate('AclRowLevels', array(
'components' => array(
'Auth' => array(
'user'
),
'Session',
)
));
$AclRowLevels->Auth
->staticExpects($this->any())
->method('user')
->with('id')
->will($this->returnValue(1));
$this->testAction('/acl_row_levels/view/Task/1');
}
Problem
The query in the AclRowLevel component requires the Auth user id. I want to simulate user_id value '1' for the unit test.
The mocked Auth method 'user' in my test is not working for the call from the component. So the user id in that query has value null.
How should this be done?
Do a debug($AclRowLevels->Auth); to check if it really was mocked. It should be a mock object. If it is not for some reason try:
$AclRowLevels->Auth = $this->getMock(/*...*/);
The code inside checkUser() should go into the model by the way. Also I doubt this has to be a component at all. This seems to be used for authorization, so why not making it a proper authorization adapter?
This is what I was looking for:
$AclRowLevels->Auth
->staticExpects($this->any())
->method('user')
->will($this->returnCallback(
function($arg) {
if ($arg === 'id') {
return 1;
}
return null;
}
));

query_builder on sonataAdminBundle is ignored

I am using sonataAdminBundle and symfony2
I made this script.
it filter the search box candidates.
but this query_builder is ignored.
Are there any other points to check ?
public function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('user',null,
array(
'query_builder' =>
function (\Doctrine\ORM\EntityRepository $rep) {
return $rep->
createQueryBuilder('s')
->where('s.id','1');
})
adding...
I am using this script for new entry,it works
$formMapper
->with('General')
->add('teacher',
null,
array(
'query_builder' =>
function (\Doctrine\ORM\EntityRepository $rep) {
return $rep->
createQueryBuilder('s')
->join('s.groups', 'g') // Assuming the association on your user entity is 'groups'
->where('g.name = :group')->setParameter('group','TeacherGroup');
})
)
but for datagridMapper ,it doesnt work.
$datagridMapper
->add('teacher',null,
array(
'query_builder' =>
function (\Doctrine\ORM\EntityRepository $rep) {
return $rep->
createQueryBuilder('s')
->join('s.groups', 'g') // Assuming the association on your user entity is 'groups'
->where('g.name = :group')
->setParameter('group','TeacherGroup');
}))
I have achieved this in the following way
$datagridMapper
->add('user',null,
array(
'field_type' => 'entity',
'field_options => array(
'query_builder' => function (\Doctrine\ORM\EntityRepository $rep) {
return $rep->
createQueryBuilder('s')
->where('s.id','1');
}
)
});
you should use 'query_builder' in in fourth parameter, something like:
$datagridMapper
->add('teacher',null,null,
array(
'query_builder' =>
function (\Doctrine\ORM\EntityRepository $rep) {
return $rep->
createQueryBuilder('s')
->join('s.groups', 'g') // Assuming the association on your user entity is 'groups'
->where('g.name = :group')
->setParameter('group','TeacherGroup');
}))
Check out the documentation about this. You have the wrong syntax.
http://sonata-project.org/bundles/doctrine-orm-admin/master/doc/reference/filter_field_definition.html#callback

zend framework 2 Unable to render template resolver could not resolve to a file

I'm learning how to use Zend Framework2. According to some tutorials available on the Net I've wrote some pieces of code . The most important tutorial for me is this one: https://github.com/psamatt/zf2-doctrine-example It covers most of the basics that i've planned to write. I've stuck on one problem that looks strange to me. On my summary page, that display all the records from DB I have a links to add new record, edit existing record, and delete record. Routing is covered by module.config.php:
'router' => array(
'routes' => array(
'incident' => array(
'type' => 'segment',
'options' => array(
'route' => '/incident[/][:action][/:id]',
'constraints' => array(
'action' => '[a-zA-Z][a-zA-Z0-9_-]*',
'id' => '[0-9]+',
),
'defaults' => array(
'controller' => 'Helpdesk\Controller\Incident',
'action' => 'index',
),
),
),
),
),
When I use a link to a new record (h.t.t.p://helpdesk/incident/add) everything works correctly. But when I use a link to edit my record (h.t.t.p://helpdesk/incident/edit/1 - where 1 is example record ID) I receive an error:
Zend\View\Renderer\PhpRenderer::render: Unable to render template "helpdesk/incident/edit"; resolver could not resolve to a file
This is my IncidentController.php:
<?php
namespace Helpdesk\Controller;
use Application\Controller\EntityUsingController;
use DoctrineModule\Stdlib\Hydrator\DoctrineObject;
use Doctrine\ORM\EntityManager;
use Zend\View\Model\ViewModel;
use Helpdesk\Form\IncidentForm;
use Helpdesk\Entity\Incident;
class IncidentController extends EntityUsingController
{
/**
* Index action
*
*/
public function indexAction()
{
$em = $this->getEntityManager();
$incidents = $em->getRepository('Helpdesk\Entity\Incident')->findAll();
return new ViewModel(array(
'incidents' => $incidents
));
}
/**
* Edit action
*
*/
public function editAction()
{
$incident = new Incident();
if ($this->params('id') > 0) {
$incident = $this->getEntityManager()->getRepository('Helpdesk\Entity\Incident')->find($this->params('id'));
}
$form = new IncidentForm($this->getEntityManager());
$form->bind($incident);
$form->setHydrator(new DoctrineObject($this->getEntityManager(),'Helpdesk\Entity\Incident'));
$request = $this->getRequest();
if ($request->isPost()) {
$form->setInputFilter($incident->getInputFilter());
$form->setData($request->getPost());
if ($form->isValid()) {
$em = $this->getEntityManager();
$em->persist($incident);
$em->flush();
$this->flashMessenger()->addSuccessMessage('Incident saved');
// Redirect to list of incidents
return $this->redirect()->toRoute('incident');
}
}
return array(
'incident' => $incident,
'form' => $form,
);
}
/**
* Add action
*
*/
public function addAction()
{
return $this->editAction();
}
/**
* Delete action
*
*/
public function deleteAction()
{
$id = (int)$this->getEvent()->getRouteMatch()->getParam('id');
if (!$id) {
return $this->redirect()->toRoute('incident');
}
$request = $this->getRequest();
if ($request->isPost()) {
$del = $request->post()->get('del', 'No');
if ($del == 'Yes') {
$id = (int)$request->post()->get('id');
$incident = $this->getEntityManager()->find('Helpdesk\Entity\Incident', $id);
if ($incident) {
$this->getEntityManager()->remove($incident);
$this->getEntityManager()->flush();
}
}
// Redirect to list of incidents
return $this->redirect()->toRoute('default', array(
'controller' => 'incident',
'action' => 'index',
));
}
return array(
'id' => $id,
'incident' => $this->getEntityManager()->find('Helpdesk\Entity\Incident', $id)->getArrayCopy()
);
}
}
What is the difference between these two? Why one works fine, while the second one generates an error?
Thanks for your help
Smok.
Most likely helpdesk/incident/edit.phtml does not exist, while add action is rendering an existing helpdesk/incident/add.phtml.
You can reuse the existing helpdesk/incident/add.phtml or create a new one.

Testing redirections CakePHP 2.0

I have been looking at some examples at the cookbook but i dont get it:
http://book.cakephp.org/2.0/en/development/testing.html#a-more-complex-example
How can i test a redirection in a delete action like this one?
public function delete($id = null){
$this->Comment->id = $id;
if (!$this->Comment->exists()) {
throw new NotFoundException(__('Invalid comment'));
}
if ($this->Comment->delete()) {
$this->Session->setFlash(__('Comment deleted'));
return $this->redirect(array('controller' => 'posts', 'action' => 'view', $idPost));
}
$this->Session->setFlash(__('Comment was not deleted'));
return $this->redirect(array('controller' => 'posts', 'action' => 'view', $idPost));
}
}
The test stops after the redirect call, so it doesn't even print this echo:
public function testDelete(){
$result = $this->testAction("/comments/delete/1");
echo "this is not printed";
print_r($this->headers);
}
Testing your delete action should be relatively the same as testing any other action. Your test case might look something like this.
// notice it extends ControllerTestCase
class PostsControllerTest extends ControllerTestCase {
function testDelete() {
$this->testAction('/posts/delete/1');
$results = $this->headers['Location'];
// your OP code redirected them to a view, which I assume is wrong
// because the item would be deleted
$expected = '/posts/index';
// check redirect
$this->assertEquals($results, $expected);
// check that it was deleted
$this->Posts->Post->id = 1;
$this->assertFalse($this->Posts->Post->exists());
}
}
Of course, this just checks the obvious. You can also check the session and write a test that expects the exception. If it's still not reaching the end of the test case or continuing on, something else is going on.
You can generate easy mocks by using the generate method on ControllerTestCase.
function testDelete() {
$Posts = $this->generate('Posts', array(
'components' => array(
'Email' => array('send'),
'Session'
)
));
// set ControllerTestCase to use this mock
$this->controller = $Posts;
$this->testAction('/posts/some_action_that_sends_email');
}
The above would first generate a mock of the PostsController to use during testing. It also mocks the send() method on the EmailComponent, and the entire SessionComponent.
For more information on mocking: http://www.phpunit.de/manual/3.0/en/mock-objects.html
For more information on generate(): http://book.cakephp.org/2.0/en/development/testing.html#using-mocks-with-testaction
Possible you have an error because $idPost is undefined.
I would write something like this:
public function delete($id = null){
$this->Comment->id = $id;
if (!$this->Comment->exists()) {
throw new NotFoundException(__('Invalid comment'));
}
if ($this->Comment->delete()) {
$this->Session->setFlash(__('Comment deleted'));
} else {
$this->Session->setFlash(__('Comment was not deleted'));
}
$this->redirect(array('controller' => 'posts', 'action' => 'view', $id));
}
}
And test it's like this:
public function testDeleteWithSuccess() {
$Controller = $this->generate('Comments', array(
'components' => array(
'Session'
),
'models' => array(
'Comment' => array('exists')
)
));
$Controller->Comment->expects($this->once())
->method('exists')
->will($this->returnValue(true));
$Controller->Session->expects($this->once())
->method('setFlash')
->with('Comment deleted');
$this->testAction("/comments/delete/ID");
$this->assertEquals($this->headers['Location'], 'http://'. $_SERVER['HTTP_HOST'] . '/posts/view/ID');
}
The echo never be printed, your function "delete" always calls a redirect before ends.
public function delete($id = null){
$this->Comment->id = $id;
if (!$this->Comment->exists()) {
throw new NotFoundException(__('Invalid comment'));
}
if ($this->Comment->delete()) {
$this->Session->setFlash(__('Comment deleted'));
} else {
$this->Session->setFlash(__('Comment was not deleted'));
}
$this->redirect(array('controller' => 'posts', 'action' => 'view', $id));
}
}
Plenix,
Isn't that totally wrong?
You are deleting a comment and passing the comment's id to the post's view controller ? So, you're viewing the post or the comment by doing so?? Any other suggestion ?

How can i test an Add function on CakePHP2.0

I have been told that we have to test also the functions created by Cake like add/delete...
If i have a function like this one, how can i test it if it doesn't have any return, redirect or even a view? ( i use ajax to execute it)
public function add() {
if ($this->request->is('post')) {
$this->Comment->create();
if ($this->Comment->save($this->request->data)) {
$this->Session->setFlash(__('The comment has been saved'));
} else {
$this->Session->setFlash(__('The comment could not be saved. Please, try again.'));
}
}
}
Thanks
Here's a sort of generic way to test it.
function testAdd() {
$Posts = $this->generate('Posts', array(
'components' => array(
'Session',
'RequestHandler' => array(
'isAjax'
)
)
));
// simulate ajax (if you can't mock the magic method, mock `is` instead
$Posts->RequestHandler
->expects($this->any())
->method('isAjax')
->will($this->returnValue(true));
// expect that it gets within the `->is('post')` block
$Posts->Session
->expects($this->once())
->method('setFlash');
$this->testAction('/posts/add', array(
'data' => array(
'Post' => array('name' => 'New Post')
)
));
// check for no redirect
$this->assertFalse(isset($this->headers['Location']));
// check for the ajax layout (you'll need to change
// this to check for something in your ajax layout)
$this->assertPattern('/<html/', $this->contents);
// check for empty view (I've never had an empty view but try it out)
$this->assertEqual('', $this->view);
}
public function add() {
$this->autoRender = false;
if ($this->request->is('post')) {
$this->Comment->create();
if ($this->Comment->save($this->request->data)) {
echo json_encode(array('status' => 'ok'));
} else {
echo json_encode(array('status' => 'fail'));
}
}
}