Post request for CI4 Unit testing - codeigniter

I have the following code running in CI 4.1.2:
public function testForgotten(){
fwrite(STDERR,"\n\nProcess Forgotten:\n");
$_COOKIE[csrf_token()] = csrf_hash();
$result = $this->post('site/forgotten',array(
'Username' => 'my#email.com',
csrf_token() => csrf_hash(),
));
print'<pre>';print_r($result->response());print'</pre>';
$result->assertRedirect();
}
As you can see I simply want to check if the forgotten password form/page is working. However the output of $result->response() includes the unposted to page along with <form action="http://example.com/site/forgotten" id="Config\Forms::reset" enctype="multipart/form-data" method="post" accept-charset="utf-8"> as part of the form (noting I've not put in the example.com - Codeigniter did that! So just wondering what I'm missing in terms of getting this test to run/work?
FYI I'm running under a PHAR file with php punit.phar tests/app/Controllers/SiteTests.php which works fine when running simple get('/path/to/page'); calls.
I have since found the example.com can be changed in phpunit.xml.dist but this has still not fixed the assertRedirect issue.

So the answer appears to be that CI4 does not actually populate the $_GET or $_POST variables rather its own internal mechanism. This becomes apparent when you use:
$request = \Config\Services::request();
print_r($request->getPost('Username'));
Instead of $_POST.
The solution is either a) use the getPost() type functions or b) amend the framework (yes shock/horror!) in system/HTTP/RequestTrait.php and setGlobal to include:
switch ($method){
case 'post': $_POST = $value; break;
case 'get': $_GET = $value; break;
}

It is a working solution for Testing Controllers ONLY. Not for HTTP Testing.
Your code can be rewritten as:
/**
* #throws \Exception
*/
public function testForgotten()
{
fwrite(STDERR, "\n\nProcess Forgotten:\n");
$_COOKIE[csrf_token()] = csrf_hash();
$request = $this->request
->withMethod('post')
// parameter 'post' for post method parameters
->setGlobal('post', [
'Username' => 'my#email.com',
csrf_token() => csrf_hash()
]);
$result = $this
->withRequest($request)
->controller(YourSiteController::class)
->execute('forgotten');
print'<pre>';
print_r($result->response());
print'</pre>';
$result->assertRedirect();
}
Key-points to note:
Use $request = $this->request;, then chain it by withMethod() and setGlobal() for adapting POST request method and accepting POST request parameters;
Bind the $request variable by $this->withRequest($request);
Chain $this->withRequest($request) by controller() and execute() to get the result.

Related

Laravel 5.7: prevent auto load injection in controller constructor

I have a HomeController with his constructor that takes a Guzzle instance.
/**
* Create a new controller instance.
*
* #param \GuzzleHttp\Client|null $client
*
* #return void
*/
public function __construct(Client $client = null)
{
$this->middleware('auth');
$this->middleware('user.settings');
if ($client === null) {
$param = [
'base_uri' => 'http://httpbin.org/',
'defaults' => [
'exceptions' => false,
'verify' => false
]
];
$client = new Client($param);
}
$this->setClient($client);
}
I would use via __constructor() to be able to mock it in tests.
My issues is that Laravel automatically auto-load the injection and the Guzzle Client injected has blank defaults (and cannot anymore edit it). In other words: at first call of HomeController Client is not null. And I need as null.
How can I stop this behaviour (only for the __construct() for HomeController)? I really use the DI in every part of my webapp.
EDIT
I just find that if I don't type-hints the Client, of course Laravel cannot auto-load. Is this the right mode to work?
New constructor:
public function __construct($client = null)
Thank you
I had a simular situation when testing apis. I ended up binding an instance of GuzzleClient to the service container (see documentation). Something like:
$this->app->instance('GuzzleHttp\Client', new MockClient);
To successfully mock the instance, I then checked to see whether or not it had a certain property value (in my case base_url being set). That determined whether or not the instance was a test as base_url would be set.
Along side this method, GuzzleHttp\Client does have a MockHandler you may want to explore. This can be used to fake response bodies, headers and status codes.

Laravel 5: Calling routes internally

Is there a way, in Laravel 5, to call routes internally/programmatically from within the application? I've found a lot of tutorials for Laravel 4, but I cannot find the information for version 5.
Using laravel 5.5, this method worked for me:
$req = Request::create('/my/url', 'POST', $params);
$res = app()->handle($req);
$responseBody = $res->getContent();
// or if you want the response to be json format
// $responseBody = json_decode($res->getContent(), true);
Source:
https://laracasts.com/discuss/channels/laravel/route-dispatch
*note: maybe you will have issue if the route you're trying to access
has authentication middleware and you're not providing the right credentials.
to avoid this, be sure to set the correct headers required so that the request is processed normally (eg Authorisation bearer ...).
UPDATE: i've tried this method with laravel 8 and it works but if you're using PHP version 8.0 you might need to call opcache_reset(); before this line $req = Request::create('/my/url', 'POST', $params); to avoid an error.
see guzzlehttp/guzzle dosn't work after update php to php 8 for more info
You may try something like this:
// GET Request
$request = Request::create('/some/url/1', 'GET');
$response = Route::dispatch($request);
// POST Request
$request = Request::create('/some/url/1', 'POST', Request::all());
$response = Route::dispatch($request);
You can actually call the controller that associates to that route instead of 'calling' the route internally.
For example:
Routes.php
Route::get('/getUser', 'UserController#getUser');
UserController.php
class UserController extends Controller {
public function getUser($id){
return \App\User::find($id);
};
}
Instead of calling /getUser route, you can actually call UserController#getUser instead.
$ctrl = new \App\Http\Controllers\UserController();
$ctrl->getUser(1);
This is the same as calling the route internally if that what you mean. Hope that helps
// this code based on laravel 5.8
// I tried to solve this using guzzle first . but i found guzzle cant help me while I
//am using same port. so below is the answer
// you may pass your params and other authentication related data while calling the
//end point
public function profile(){
// '/api/user/1' is my api end please put your one
//
$req = Request::create('/api/user/1', 'GET',[ // you may pass this without this array
'HTTP_Accept' => 'application/json',
'Content-type' => 'application/json'
]);
$res = app()->handle($req);
$responseBody = json_decode($res->getContent()); // convert to json object using
json_decode and used getcontent() for getting content from response
return response()->json(['msg' =>$responseBody ], 200); // return json data with
//status code 200
}
None of these answers worked for me: they would either not accept query parameters, or could not use the existing app() instance (needed for config & .env vars).
I want to call routes internally because I'm writing console commands to interface with my app's API.
Here's what I did that works well for me:
<?php // We're using Laravel 5.3 here.
namespace App\Console;
use App\MyModel;
use App\MyOtherModel;
use App\Http\Controllers\MyController;
use Illuminate\Console\Command;
class MyCommand extends Command
{
protected $signature = 'mycommand
{variable1} : First variable
{variable2} : Another variable';
public function handle()
{
// Set any required headers. I'm spoofing an AJAX request:
request()->headers->set('X-Requested-With', 'XMLHttpRequest');
// Set your query data for the route:
request()->merge([
'variable1' => $this->argument('variable1'),
'variable2' => $this->argument('variable2'),
]);
// Instantiate your controller and its dependencies:
$response = (new MyController)->put(new MyModel, new MyOtherModel);
// Do whatever you want with the response:
var_dump($response->getStatusCode()); // 200, 404, etc.
var_dump($response->getContent()); // Entire response body
// See what other fun stuff you can do!:
var_dump(get_class_methods($response));
}
}
Your Controller/Route will work exactly as if you had called it using curl. Have fun!

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
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());
}
While this question is very old, it still pops up as first result on Google, so I'd though I'd update it with my findings using Symfony 5.4 / 6.x.
Short answer: use the result of your Form type's getBlockPrefix() method as the tokenId:
$csrfToken = self::getContainer()->get('security.csrf.token_manager')->getToken('your_blockprefix');
Long answer:
This is the place where Symfony creates the CSRF Token within it's form system:
https://github.com/symfony/symfony/blob/6.3/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php#L81
The tokenId will be determined in the following order:
the form type's option csrf_token_id if present
the form type's block prefix as returned by the getBlockPrefix() method if present (see documentation)
the Entity's class name, converted to lowercase with underscores (see source)
Since I didn't want to add the csrf_token_id option to every single Form Type, I wrote the following method to obtain the CSRF Token based on the fully qualified name of a Form Type:
protected function generateCSRFToken(string $formTypeFQN): string
{
$reflectionClass = new \ReflectionClass($formTypeFQN);
$constructor = $reflectionClass->getConstructor();
$args = [];
foreach ($constructor->getParameters() as $parameter) {
$args[] = $this->createMock($parameter->getType()->getName());
}
/** #var FormTypeInterface $instance */
$instance = $reflectionClass->newInstance(... $args);
return self::getContainer()->get('security.csrf.token_manager')->getToken($instance->getBlockPrefix());
}
It instantiates an object of the Form Type mocking every required constructor parameter and then creates a CSRF token based on the instance's block prefix.

symfony2 crud & http cache

I'm using Symfony2 and I have a ReaderBundle that has an Rss entity.
I'm created CRUD for this entity.
php app/console generate:doctrine:crud --entity=RSSReaderBundle:Rss --format=annotation --with-write
All was well, before I connected Cache.
$loader = require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppCache.php';
require_once __DIR__.'/../app/AppKernel.php';
Debug::enable();
$kernel = new AppKernel('dev' , true);
$kernel->loadClassCache();
$kernel = new AppCache($kernel); // THAT STRING IS MAIN PROBLEM
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
And then when i'm trying to delete some record, i take this error:
No route found for "POST /rss/delete/30": Method Not Allowed (Allow: DELETE)
405 Method Not Allowed
I created a form that clearly indicates the method:
private function createDeleteForm($id)
{
return $this->createFormBuilder()
->setAction($this->generateUrl('rss_delete', array('id' => $id)))
->setMethod("DELETE")
->add('submit', 'submit', array('label' => 'Delete'))
->getForm()
;
}
I have not found the problem. Help please
This problem occurred since symfony2.2, see https://github.com/symfony/symfony-standard/commit/1970b831f8843a5cf551e9d88404cb62f21b90f9
You need to modify the Symfony\Component\HttpFoundation\Request::$httpMethodParameterOverride boolean manually in your app.php file:
// When using the HttpCache, you need to call the method in your front controller instead of relying on the configuration parameter
Request::enableHttpMethodParameterOverride();
There is no method="DELETE" in html forms ... at least not supported in almost all browsers - only in ajax requests.
Work around this by allowing DELETE and POST requests to the route.
we must remove the string with comment "THAT STRING IS MAIN PROBLEM". Cache is working all the same. And CRUD working right
I have the same problem that you had (with PUT and DELETE HTTP method) but I don't understand your solution.
Did you:
a) just get rid of // THAT STRING IS MAIN PROBLEM
or
b) get rid of $kernel = new AppCache($kernel); // THAT STRING IS MAIN PROBLEM
The solution b) is the same as not using cache so in my case, it's not helping as the page take a very long time to load.
#Nifr:
I thought there were method PUT and DELETE. I use them in my forms following the instructions on this link: http://symfony.com/fr/doc/current/cookbook/routing/method_parameters.html
So in fact, Symfony2 is able to tell whether a method is PUT, DELETE, POST or GET. But somehow, the cache can't...
Any help on that?
Edit:
I found a solution that doesn't involve changing anything in the app.php file.
Basically, the problem comes from the fact that the getMethod method of the request object of symfony2 doesn't know about PUT or DELETE method. The point is to change that in the AppCache.php file.
We just have to override the method invalidate of the AppCache.php file:
protected function invalidate(Request $request, $catch = false)
{
$request->setMethod($request->request->get('_method'));
return parent::invalidate($request, $catch);
}
Here, I just change the method of the request by the method posted from the form.

Call a controller in Laravel 4

In Laravel 3, you could call a controller using the Controller::call method, like so:
Controller::call('api.items#index', $params);
I looked through the Controller class in L4 and found this method which seems to replace the older method: callAction(). Though it isn't a static method and I couldn't get it to work. Probably not the right way to do it?
How can I do this in Laravel 4?
You may use IoC.
Try this:
App::make($controller)->{$action}();
Eg:
App::make('HomeController')->getIndex();
and you may also give params:
App::make('HomeController')->getIndex($params);
If I understand right, you are trying to build an API-centric application and want to access the API internally in your web application to avoid making an additional HTTP request (e.g. with cURL). Is that correct?
You could do the following:
$request = Request::create('api/items', 'GET', $params);
return Route::dispatch($request)->getContent();
Notice that, instead of specifying the controller#method destination, you'll need to use the uri route that you'd normally use to access the API externally.
Even better, you can now specify the HTTP verb the request should respond to.
Like Neto said you can user:
App::make('HomeController')->getIndex($params);
But to send for instance a POST with extra data you could use "merge" method before:
$input = array('extra_field1' => 'value1', 'extra_field2' => 'value2');
Input::merge($input);
return App:make('HomeController')->someMethodInController();
It works for me!
bye
This is not the best way, but you can create a function to do that:
function call($controller, $action, $parameters = array())
{
$app = app();
$controller = $app->make($controller);
return $controller->callAction($app, $app['router'], $action, $parameters);
}
Route::get('/test', function($var = null) use ($params)
{
return call('TestController', 'index', array($params));
});
Laurent's solution works (though you need a leading / and the $params you pass to Request::create are GET params, and not those handled by Laravel (gotta put them after api/items/ in the example).
I can't believe there isn't an easier way to do this though (not that it's hard, but it looks kinda hackish to me). Basically, Laravel 4 doesn't provide an easy way to map a route to a controller using a callback function? Seriously? This is the most common thing in the world...
I had to do this on one of my projects:
Route::controller('players', 'PlayerController');
Route::get('player/{id}{rest?}', function($id)
{
$request = Request::create('/players/view/' . $id, 'GET');
return Route::dispatch($request)->getContent();
})
->where('id', '\d+');
Hope I'm missing something obvious.
$request = Request::create('common_slider', 'GET', $parameters);
return Controller::getRouter()->dispatch($request)->getContent();
For laravel 5.1
It's an Old question. But maybe is usefull. Is there another way.
In your controller: You can declare the function as public static
public static function functioNAME(params)
{
....
}
And then in the Routes file or in the View:
ControllerClassName::functionNAME(params);

Resources