CakePHP Controller Testing: Mocking $this->referer() - cakephp

I have code in place so that if the "Add User" page is accessed from anywhere in the "Posts" section of the website, the user will be taken to the "Users" index after adding the user. But if the "Add User" page is accessed from any other section of the website, the user will be taken back to where they were after adding the user. I want to test this, but I don't know how. This is what I have so far:
Controller Code
<?php
App::uses('AppController', 'Controller');
class UsersController extends AppController {
public function add() {
if ($this->request->is('post')) {
$this->User->create();
if ($this->User->save($this->request->data)) {
$this->Session->setFlash(__('The user has been saved'));
return $this->redirect($this->request->data['User']['redirect']);
} else {
$this->Session->setFlash(__('The user could not be saved. Please, try again.'));
}
}
else {
if ($this->referer() == '/' || strpos($this->referer(), '/posts') !== false) {
$this->request->data['User']['redirect'] = Router::url(array('action' => 'index'));
}
else {
$this->request->data['User']['redirect'] = $this->referer();
}
}
}
public function index() {
$this->User->recursive = 0;
$this->set('users', $this->paginate());
}
}
Test Code
<?php
App::uses('UsersController', 'Controller');
class UsersControllerTest extends ControllerTestCase {
public function testAdd() {
$this->Controller = $this->generate('Users');
// The following line is my failed attempt at making $this->referer()
// always return "/posts".
$this->Controller->expects($this->any())->method('referer')->will($this->returnValue('/posts'));
$this->testAction('/users/add/', array('method' => 'get'));
$this->assertEquals('/users', $this->Controller->request->data['User']['redirect']);
}
}
What am I doing wrong?

You aren't mocking any methods
This line
$this->Controller = $this->generate('Users');
Only generates a test controller, you aren't specifying any methods to mock. To specify that some controller methods need to be mocked refer to the documentation:
$Posts = $this->generate('Users', array(
'methods' => array(
'referer'
),
...
));
The expectation is never triggered
Before asking this question, you probably had an internal conversation a bit like: "why is it saying that my expectation is never called? I'll just use $this->any() and ignore it.."
Don't use $this->any() unless it really doesn't matter if the mocked method is called at all. Looking at the controller code, you're expecting it to be called exactly once - so instead use $this->once():
public function testAdd() {
...
$this->Controller
->expects($this->once()) # <-
->method('referer')
->will($this->returnValue('/posts'));
...
}
The full list of available matchers is available in PHPUnit's documentation.

Related

Cake: Access the controller in a static method in a component

we have a static method in a Cake component. The task was to redirect the user to the login page, if this component throws a specific error. The current (working) solution is:
class SomeComponent extends Component {
static $controllerObject;
function startup(&$controller) {
SomeComponent::$controllerObject =& $controller;
}
(...)
public static function decodeResponse($response) {
if($response == null || trim($response) == '') {
return null;
}
$result = json_decode($response, true);
if($result == null) {
throw new FatalErrorException('Could not parse api server response: ' . $response);
}
if(isset($result['error'])) {
if ($result['error'] === "IncorrectCredentialsException") {
self::$controllerObject->Session->destroy();
self::$controllerObject->Session->setFlash(__('Your session has ended. Please log in again.', true), 'default', array(), 'error');
self::$controllerObject->redirect(array(
'language' => Configure::read('Config.language'),
'controller' => 'users',
'action' => 'login'
));
}
else { throw new ApiServerException($result); }
}
return $result;
}
However, my team colleague, who is responsible for the software quality, doesn't find this solution satisfying. He says: "pls find a better way to pass the controller to the decode method. Setting the controller as static variable is not the best way".
Is there any better way to do this?
I think the problem is that your method does two different things: decoding and error handling. Instead of handling the IncorrectCredentialsException inside your method, I would move this functionality to where you handle the other exceptions and just throw an IncorrectCredentialsException in your method. With this change you no longer need to access the controller in your static method.

Change CakePHP settings for tests

The Code
AppController.php
<?php
App::uses('Controller', 'Controller');
class AppController extends Controller {
public $components = array(
'Auth' => array(
'authorize' => array('Controller')
),
'Session'
);
}
PostsController.php
<?php
App::uses('AppController', 'Controller');
class PostsController extends AppController {
public function isAuthorized() {
return $this->Auth->user('role') == 'admin';
}
public function add() {
$this->set('some_var', true);
}
}
PostsControllerTest.php
<?php
App::uses('PostsController', 'Controller');
class PostsControllerTest extends ControllerTestCase {
public function setUp() {
parent::setUp();
CakeSession::write('Auth.User', array(
'id' => 2,
'username' => 'joe_bloggs',
'role' => 'user',
'created' => '2013-05-17 10:00:00',
'modified' => '2013-05-17 10:00:00'
));
}
public function testAddWhileLoggedInAsNonAdminFails() {
$this->testAction('/posts/add/', array('method' => 'get'));
$this->assertTrue($this->vars['some_var']);
}
public function tearDown() {
parent::tearDown();
CakeSession::destroy();
}
}
The Problem
Right now, the "testAddWhileLoggedInAsNonAdminFails" test passes. It should fail. The issue is that redirects do not exit/halt the simulated request.
Partial Solution
I can fix the problem by modifying "AppController.php" and "PostsControllerTest.php" like so:
Modified AppController.php
<?php
App::uses('Controller', 'Controller');
class AppController extends Controller {
public $components = array(
'Auth' => array(
'authorize' => array('Controller'),
// ***** THE FOLLOWING LINE IS NEW *****
'unauthorizedRedirect' => false
),
'Session'
);
}
Modified PostsControllerTest.php
<?php
App::uses('PostsController', 'Controller');
class PostsControllerTest extends ControllerTestCase {
public function setUp() {
parent::setUp();
CakeSession::write('Auth.User', array(
'id' => 2,
'username' => 'joe_bloggs',
'role' => 'user',
'created' => '2013-05-17 10:00:00',
'modified' => '2013-05-17 10:00:00'
));
}
// ***** THE FOLLOWING 3 LINES ARE NEW *****
/**
* #expectedException ForbiddenException
*/
public function testAddWhileLoggedInAsNonAdminFails() {
$this->testAction('/posts/add/', array('method' => 'get'));
}
public function tearDown() {
parent::tearDown();
CakeSession::destroy();
}
}
The problem with this solution is it modifies the behavior of the real website too. I'm looking for a way to set the Auth component's unauthorizedRedirect property to false only when tests are being run. How can I do this?
Changing the behavior of your code to make tests work right is not really a good idea.
The correct answer to this question is that it's not a very good question, and what you really should do is test each function separately.
For the isAuthorized function, you should do:
<?php
class PostsControllerTest extends ControllerTestCase {
public function testIsAuthorized() {
$Posts = $this->generate('Posts');
$user = array('role' => 'admin');
$this->assertTrue($Posts->isAuthorized($user));
$anotherUser = array('role' => 'saboteur');
$this->assertFalse($Posts->isAuthorized($user));
}
public function testAdd() {
$this->testAction('/posts/add/', array('method' => 'get'));
$this->assertTrue($this->vars['some_var']);
}
}
The core concept behind unit testing is breaking down your app into the smallest pieces possible, and testing each in isolation. Once you have your unit tests sorted out, you can work on integration tests that cover more than one function, but many projects never reach that stage, and that's okay. The redirect issue can be interesting to work with, but you can mock out controller::redirect as described in this blog post. It's a bit old but still useful.
Did you check the book? http://book.cakephp.org/2.0/en/development/testing.html#testing-controllers
When testing actions that contain redirect() and other code following
the redirect it is generally a good idea to return when redirecting.
The reason for this, is that redirect() is mocked in testing, and does
not exit like normal. And instead of your code exiting, it will
continue to run code following the redirect.
It exactly describes your problem.
I haven't tested this but try it, the manual says the controller is already mocked when using ControllerTestCase so you should be able to expect it:
$this->controller->expects($this->at(0))
->method('redirect')
->with('/your-expected-input');
Taking a look at the ControllerTestCase class might reveal how the controller is exactly mocked and set up. Alternatively you could just fall back to the regular CakeTestCase and set the controller mocks up by yourself.
Another alternative would be to extend your controller you want to test and override the redirect() method, not calling the parent but setting the first arg to a property like Controller::$redirectUrl. After your action call you can then assertEqual the properties value. But this still requires you to return after the redirect call in your controller. Also this won't work either when using ControllerTestCase because it would mock your overriden method.

CakePHP 2.1 HTTP cache

I'm trying to speed up my site by taking advantage of the new HTTP cache features in CakePHP 2.1:
class ArticlesController extends AppController {
public function view($id) {
$article = $this->Article->find(
'first',
array('conditions' => array('Article.id' => $id))
);
$this->response->modified($article['Article']['modified']);
$this->set(compact('article'));
}
}
Caching works fine, but does not distinguish between different users (i.e. if a user logs in and visits a page that was already cached, the previously cached page is displayed, and user-specific content is not shown). I'd like one of the following to happen:
Cache discriminates between different users and stores a separate cache for each user
Caching is disabled if a user is logged in (the user login is only used for admin purposes)
I've tried adding
if (AuthComponent::user('id')) {
$this->disableCache();
}
But this doesn't seem to solve the problem
Does anyone know how to get this to work, or am I doing something fundamentally wrong?
You could try the etag caching method and generate a hash based on the article id and user id.
See http://book.cakephp.org/2.0/en/controllers/request-response.html#the-etag-header
The Etag header (called entity tag) is string that uniquely identifies the requested resource. It is very much like the checksum of a file, caching will compare checksums to tell whether they match or not.
To actually get advantage of using this header you have to either call manually CakeResponse::checkNotModified() method or have the RequestHandlerComponent included in your controller:
<?php
public function index() {
$articles = $this->Article->find('all');
$this->response->etag($this->Article->generateHash($articles));
if ($this->response->checkNotModified($this->request)) {
return $this->response;
}
...
}
I thought I'd post the solution(s) I eventually used, in case it helps anyone.
To disable caching completely for logged in users:
class ArticlesController extends AppController {
public function view($id) {
$article = $this->Article->find(
'first',
array('conditions' => array('Article.id' => $id))
);
if (!AuthComponent::user('id')) {
$this->response->etag($this->Article->generateHash($article));
}
$this->set(compact('article'));
}
}
To have a separate cache for each user (and for the case when no-one is logged in):
class Article extends AppModel {
public function generateHash($article) {
if (AuthComponent::user('id')) {
return md5(AuthComponent::user('id') . '-' . $article['Article']['modified']);
} else {
return md5($article['Article']['modified']);
}
}
}
class ArticlesController extends AppController {
public function view($id) {
$article = $this->Article->find(
'first',
array('conditions' => array('Article.id' => $id))
);
$this->response->etag($this->Article->generateHash($article));
$this->set(compact('article'));
}
}

CakePHP Auth Deny Admin Routing Pages

I have been reading Stack Overflow questions all afternoon trying to figure this out..
I have a users controller with index/login/logout/register functions but also has admin_index/admin_add/admin_edit/admin_delete etc.
I have Auth component enabled and in my users_controller i am trying to deny access to the admin_* pages if the Auth.User.role != 'admin', when i enable the $this->Auth->authorize = 'controller'; it denies access to the site.com/admin/users/ page and also seems to kill the logout function even tho my account has the role set to admin.
However if i type the url in i get redirected back to the main homepage.
users_controller.php
<?php
class UsersController extends AppController {
var $name = 'Users';
function beforeFilter(){
parent::beforeFilter();
$this->Auth->authorize = 'controller';
$this->Auth->allow('register');
}
function isAuthorized() {
if ($this->Auth->user('role') != 'admin') {
$this->Auth->deny('admin_index','admin_view', 'admin_add', 'admin_edit','admin_delete');
}
}
app_controller.php
<?php
class AppController extends Controller {
var $components = array('Auth', 'Session');
function beforeFilter() {
$this->Auth->loginAction = array('controller'=>'users','action'=>'login', 'admin'=>false);
$this->Auth->logoutRedirect = array('controller'=>'users','action'=>'logout');
$this->Auth->loginRedirect = array('controller'=>'shows', 'action'=>'index');
$this->Auth->autoRedirect = false;
$this->Auth->allow('home');
}
My Second question relates to the way $this->Auth->deny('page'); redirects the user, as far as i can tell it redirects to / but i need it to redirect back to the users controller.
Hope it all makes sense and i have provided enough info..
The root of your problem is probably your isAuthorized() method. This should simply return true or false, and indicates whether an authenticated user is AUTHORIZED to access a particular action.
It's difficult to say why you'd be redirected to the home page instead of the login page. But it's possible that you have other code somewhere that's messing things up.
Try modifying your code as below and see if that doesn't help get things working:
app_controller.php
<?php
class AppController extends Controller {
var $components = array('Session', 'Auth' => array(
'loginAction' => array('controller'=>'users','action'=>'login', 'admin'=>false),
'logoutRedirect' => array('controller'=>'users','action'=>'logout'),
'loginRedirect' => array('controller'=>'shows', 'action'=>'index'),
'autoRedirect' => false,
'authorize' => 'controller'
);
function beforeFilter() {
$this->Auth->allow('home');
}
function isAuthorized() {
if (!empty($this->params['prefix']) && $this->params['prefix'] == 'admin') {
if ($this->Auth->user('role') != 'admin') {
return false;
}
}
return true;
}
?>
users_controller.php
<?php
class UsersController extends AppController {
var $name = 'Users';
function beforeFilter(){
parent::beforeFilter();
$this->Auth->allow('register');
}
?>
I moved all the Auth settings to the declaration in the $components variable because it seems cleaner and to make more sense to declare default values there. But this is more a matter of personal preference and it shouldn't have a real effect on the code's functioning.
Also, note that if you set autoRedirect to false, you'll have to redirect logged-in users manually in your Users::login() action, getting the loginRedirect value with $this->Auth->redirect().
I don't see any reason why you should be sent to / when you're not logged in and you try to access a blocked action, but maybe it will be easier to figure out after you fix the above. **
you should do this like...
function beforeFilter()
{
if($this->Auth->user('role')=='admin'){
$this->Auth->allow('admin_view','admin_controls');//put your all admin actions separated by comma
}
else
{
$this->Auth->allow('home');//put your all non-admin actions separated by comma
}
}
hope it will work... if any problem let me know....

CakePHP: Prevent Auth component's "authError" message on homepage

I have a CakePHP project where I modified "app/config/routes.php" so that the root points to the "Users" controller's "dashboard" action. In other words, these two URLs go to the same place:
http://example.com/
http://example.com/users/dashboard
I have the "Auth" component set up in my "App" controller like so:
class AppController extends Controller {
var $components = array('Auth', 'Session');
function beforeFilter() {
$this->Auth->authorize = 'controller';
$this->Auth->loginRedirect = array('controller' => 'users', 'action' => 'dashboard');
if ($this->Auth->user()) {
$this->set('logged_in', true);
}
else {
$this->set('logged_in', false);
}
}
}
I want it so that if a non-authenticated user goes straight to http://example.com/users/dashboard , they are taken to the login page with the "Auth" component's "authError" message showing, but if they go to http://example.com/ , they are taken to the login page without the "Auth" component's "authError" message showing. Is this possible?
I resolved this by putting the following code in my "Users" controller's "login" action:
if ($this->Session->read('Auth.redirect') == $this->webroot && $this->Session->read('Message.auth.message') == $this->Auth->authError) {
$this->Session->delete('Message.auth');
}
been looking for somthing like that for a long time! Thank you.
I had to make a little change then $this->webroot is not "/":
if (str_replace("//","/",$this->webroot.$this->Session->read('Auth.redirect')) == $this->webroot && $this->Session->read('Message.auth.message') == $this->Auth->authError) {
$this->Session->delete('Message.auth');
}
Well, I don't understand why sometimes you show the error and why sometimes not.. but you can afford this creating an isAuthorized method and modifying all the logic of the default AuthComponent behavior.
Open your Auth component and check for method "startup()". There, at it's last line, you will se this:
$this->Session->setFlash($this->authError, $this->flashElement, array(), 'auth');
$controller->redirect($controller->referer(), null, true);
This is the part responsible for displaying the error.
Before it, you will se...
if ($this->isAuthorized($type)) {
return true;
}
So you can change your isAuthorized method to change this message when you want.
Is a lot of work for (I think..) nothing.
PS. There may be a simpler way to be ignoring me
If you really wants to prevent authError message on homepage and simple redirect to login page then you have to put false as parameter of authError
class AppController extends Controller {
public function initialize() {
parent::initialize();
$this->loadComponent('RequestHandler');
$this->loadComponent('Flash');
$this->loadComponent('Auth', [
'authError' => false
]);
}
}

Resources