I'm trying to write some PHPUnit tests for my small slim framework app, but don't see anywhere in the docs that point to a way to do a full request and assert on the response (either containing text or a 200 status, or anything, really).
Is there any way to do this that anyone has found/used?
Here is example how you may test your Slim application:
https://github.com/mac2000/SlimTestable
Suppose we have simple application:
<?php
use Slim\Slim;
require_once 'vendor/autoload.php';
$app = new Slim();
$app->get('/', function(){
echo 'home';
})->name('home');
$app->get('/hello/:name', function($name){
echo "hello $name";
})->name('hello');
$app->map('/login', function() use($app) {
if($app->request()->params('login')) {
$app->flash('success', 'Successfully logged in');
$app->redirect($app->urlFor('hello', array('name' => $app->request()->params('login'))));
} else {
$app->flash('error', 'Wrong login');
$app->redirect($app->urlFor('home'));
}
})->via('GET', 'POST');
$app->run();
How do we test it?
Create App class:
<?php // src/App.php
use Slim\Slim;
class App extends Slim {
function __construct(array $userSettings = array())
{
parent::__construct($userSettings);
$this->get('/', function(){
echo 'home';
})->name('home');
$this->get('/hello/:name', function($name){
echo "hello $name";
})->name('hello');
$this->map('/login', function() {
if($this->request()->params('login')) {
$this->flash('success', 'Successfully logged in');
$this->redirect($this->urlFor('hello', array('name' => $this->request()->params('login'))));
} else {
$this->flash('error', 'Wrong login');
$this->redirect($this->urlFor('home'));
}
})->via('GET', 'POST');
}
/**
* #return \Slim\Http\Response
*/
public function invoke() {
$this->middleware[0]->call();
$this->response()->finalize();
return $this->response();
}
}
Notice that we move all our routes to new class constructor, also notice new invoke method, which do the same as run method except it returns response rather than echoing it out.
Now your index.php file might be like this one:
<?php
require_once 'vendor/autoload.php';
$app = new App();
$app->run();
And now it is time for tests:
<?php // tests/ExampleTest.php
use Slim\Environment;
class ExampleTest extends PHPUnit_Framework_TestCase {
private $app;
public function setUp()
{
$_SESSION = array();
$this->app = new App();
}
public function testHome() {
Environment::mock(array(
'PATH_INFO' => '/'
));
$response = $this->app->invoke();
$this->assertContains('home', $response->getBody());
}
public function testHello() {
Environment::mock(array(
'PATH_INFO' => '/hello/world'
));
$response = $this->app->invoke();
$this->assertTrue($response->isOk());
$this->assertContains('hello world', $response->getBody());
}
public function testNotFound() {
Environment::mock(array(
'PATH_INFO' => '/not-exists'
));
$response = $this->app->invoke();
$this->assertTrue($response->isNotFound());
}
public function testLogin() {
Environment::mock(array(
'PATH_INFO' => '/login'
));
$response = $this->app->invoke();
$this->assertTrue($response->isRedirect());
$this->assertEquals('Wrong login', $_SESSION['slim.flash']['error']);
$this->assertEquals('/', $response->headers()->get('Location'));
}
public function testPostLogin() {
Environment::mock(array(
'REQUEST_METHOD' => 'POST',
'PATH_INFO' => '/login',
'slim.input' => 'login=world'
));
$response = $this->app->invoke();
$this->assertTrue($response->isRedirect());
$this->assertEquals('Successfully logged in', $_SESSION['slim.flash']['success']);
$this->assertEquals('/hello/world', $response->headers()->get('Location'));
}
public function testGetLogin() {
Environment::mock(array(
'PATH_INFO' => '/login',
'QUERY_STRING' => 'login=world'
));
$response = $this->app->invoke();
$this->assertTrue($response->isRedirect());
$this->assertEquals('Successfully logged in', $_SESSION['slim.flash']['success']);
$this->assertEquals('/hello/world', $response->headers()->get('Location'));
}
}
You should notice few things:
While setting up test we are creating $_SESSION array for test purposes and instantiate our App class object.
In tests rather than run we are calling invoke which do the same, but returns response object.
Environment::mock used to mock requests which are processed with our application.
Ok, so I was able to rough it and make it work. Here's an example of an endpoint test class.
Assuming you're working in a development environment, you can execute curl requests to your own localhost, thus testing before committing to a repo.
First, create your class:
class ApiEndpointsTest extends PHPUnit_Framework_TestCase
{
protected $api_url = "http://localhost/api/v1";
//create a function that will allow you to call API endpoints at-will.
private function loadEndpoint($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
$info = curl_getinfo($ch);
curl_close($ch);
return array(
'body' => $output,
'info' => $info
);
}
//this allows you to write messages in the test output
private function printToConsole($statement) {
fwrite(STDOUT, $statement."\n");
}
Using this, you can write a test function for a particular endpoint response:
//this will test the actual body of the response against something expected.
public function testGetUserResponse() {
$this->printToConsole(__METHOD__);
$url = $this->api_url."/users/124";
$response = $this->loadEndpoint($url);
$expected = '[{"name":"John Smith","email":"john#acme.com"}]';
$this->assertEquals($response['body'], $expected);
}
In a separate test, you can test any other property of the API call's response:
public function testGetUserMimeType() {
$this->printToConsole(__METHOD__);
$url = $this->api_url."/users/124";
$response = $this->loadEndpoint($url);
$this->assertEquals($response['info']['content_type'], 'application/json');
}
Your info property options can be found here: http://php.net/manual/en/function.curl-getinfo.php
Side note: if anyone reading this is an expert at PHPUnit and knows a better way, I'm interested in learning about it -- I'm new to PHPUnit.
Related
Good morning all , I did a complete migration of my symfony 2.8 application to version 5.4. I am now at the unit testing stage. I copied the tests to my new project, however I'm having some difficulties with the API authentication. I launched the unit tests without modification with the classic configuration of rest bundle. In my tests, I test the authentication of a user upstream in order to recover the rights necessary to test the different endpoints. When I want to authenticate with _username, and _password, I get the following error in my response content:
<!-- Invalid JSON. (400 Bad Request) -->
Here is the content of my authUser function of my abstract class which allows to authenticate the user :
<?php
namespace WORD\UserBundle\Tests\Model;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\FrameworkBundle\Client;
abstract class AbstractAuthTestCase extends WebTestCase
{
/**
* #var Client
*/
protected $client = null;
/**
* ContainerInterface
*/
//protected $container;
// public function setUp(): void
// {
// self::ensureKernelShutdown();
// $this->client = static::createClient();
// // $this->container = $this->client->getContainer();
// $this->client->setServerParameter('HTTPS', true);
// $this->client->setServerParameter('HTTP_HOST', self::$container->getParameter('api_host'));
// $this->client->setServerParameter('HTTP_CONTENT_TYPE', 'application/json');
// }
public function authUser($username='stack.fr', $password='overflow', $referer='https://stack.over.local/')
{
$this->newClient();
$this->client->setServerParameter('HTTP_REFERER', $referer);
$this->client->request('POST', '/api/login_check', array(
'_username' => $username,
'_password' => $password,
));
$response = json_decode($this->client->getResponse()->getContent(), true);
if (isset($response['token'])) {
$this->client->setServerParameter('HTTP_AUTHORIZATION', 'Bearer ' . $response['token']);
}
return $response;
}
private function newClient()
{
$this->client = static::createClient();
//$this->container = $this->client->getContainer();
$this->client->setServerParameter('HTTPS', true);
$this->client->setServerParameter('HTTP_HOST', self::$container->getParameter('api_host'));
$this->client->setServerParameter('HTTP_CONTENT_TYPE', 'application/json');
}
}
Could you tell me if I forgot something please? I'm working on Symfony 5.4 with the FOS/RESTBundle 3.4
According to the documentation, this test method is still functional on symfony. 5 version. Thank you for your help
I finally found the solution to my problem.
I had to change the format of the query:
$this->client->request(
'POST',
'/api/login_check',
[],
[],
['CONTENT_TYPE' => 'application/json'],
'{"_username":"'.$username.'","_password": "'.$password.'" }'
);
In a Laravel project (Laravel 8 on PHP 8.0) I have a feature test in which I test an internal endpoint. The endpoint has a Controller calls a method on a Service. The Service then tries to call a third-party endpoint. It is this third-party endpoint that I would like to mock. The situation currently looks like this:
Internal Endpoint Feature Test
public function testStoreInternalEndpointSuccessful(): void
{
// arrange, params & headers are not important in this problem
$params = [];
$headers = [];
// act
$response = $this->json('POST', '/v1/internal-endpoint', $params, $headers);
// assert
$response->assertResponseStatus(Response::HTTP_OK);
}
Internal Endpoint Controller
class InternalEndpointController extends Controller
{
public function __construct(protected InternalService $internalService)
{
}
public function store(Request $request): InternalResource
{
$data = $this.internalService->fetchExternalData();
return new InternalResource($data); // etc.
}
}
Internal Service
use GuzzleHttp\ClientInterface;
class InternalService
{
public function __construct(protected ClientInterface $client)
{
}
public function fetchExternalData()
{
$response = $this->httpClient->request('GET', 'v1/external-data');
$body = json_decode($response->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR);
return $body;
}
}
I have looked at Guzzle's documentation, but it seems like the MockHandler strategy requires you to execute the http request inside of the test, which is not wat I want in my test. I want Guzzle's http client to be mocked and to return a custom http response that I can specify in my test. I have tried to mock Guzzle's http client like this:
public function testStoreInternalEndpointSuccessful(): void
{
// arrange, params & headers are not important in this problem
$params = [];
$headers = [];
$mock = new MockHandler([
new GuzzleResponse(200, [], $contactResponse),
]);
$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' => $handlerStack]);
$mock = Mockery::mock(Client::class);
$mock
->shouldReceive('create')
->andReturn($client);
// act
$response = $this->json('POST', '/v1/internal-endpoint', $params, $headers);
// assert
$response->assertResponseStatus(Response::HTTP_OK);
}
But the InternalService does not seem to hit this mock in the test.
I have also considered and tried to use Http Fake, but it didn't work and I assume Guzzle's http client does not extend Laravel's http client.
What would be the best way to approach this problem and mock the third-party endpoint?
Edit
Inspired by this StackOverflow question, I have managed to solve this problem by injecting a Guzzle client with mocked responses into my service. The difference to the aforementioned StackOverflow question is that I had to use $this->app->singleton instead of $this->app->bind because my DI was configured differently:
AppServiceProvider.php
namespace App\Providers;
use App\Service\InternalService;
use GuzzleHttp\Client;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// my app uses ->singleton instead of ->bind
$this->app->singleton(InternalService::class, function () {
return new InternalService(new Client([
'base_uri' => config('app.internal.base_url'),
]));
});
}
}
Depending on your depending injection, you want to bind or singleton-ify your InternalService with a custom Guzzle http client that returns mocked responses, e.g. like this:
public function testStoreInternalEndpointSuccessful(): void
{
// depending on your DI configuration,
// this could be ->bind or ->singleton
$this->app->singleton(InternalService::class, function($app) {
$mockResponse = json_encode([
'data' => [
'id' => 0,
'name' => 'Jane Doe',
'type' => 'External',
'description' => 'Etc. you know the drill',
]
]);
$mock = new GuzzleHttp\Handler\MockHandler([
new GuzzleHttp\Psr7\Response(200, [], $mockResponse),
]);
$handlerStack = GuzzleHttp\HandlerStack::create($mock);
$client = new GuzzleHttp\Client(['handler' => $handlerStack]);
return new InternalService($client);
});
// arrange, params & headers are not important in this problem
$params = [];
$headers = [];
// act
$response = $this->json('POST', '/v1/internal-endpoint', $params, $headers);
// assert
$response->assertResponseStatus(Response::HTTP_OK);
}
See also: Unit Testing Guzzle inside of Laravel Controller with PHPUnit
I am trying to build my first no-framework PHP application and I am following this tutorial.
I am relatively new to some concepts described in the tutorial. Despite this, I decided to use, as Dependency Injector, PHP-DI instead of the suggested one (rdlowrey/auryn).
I have created everything according to the tutorial except for the file Bootstrap.php (and the file Dependencies.php:
<?php declare(strict_types = 1);
require(__DIR__ . '/../vendor/autoload.php');
...
$container = include('Dependencies.php');
$request = $container->make('Http\HttpRequest');
$response = $container->make('Http\HttpResponse');
...
switch ($routeInfo[0]) {
...
case \FastRoute\Dispatcher::FOUND:
$className = $routeInfo[1][0];
$method = $routeInfo[1][1];
$vars = $routeInfo[2];
$class = $container->make($className);
$class->$method($vars); // (**)
break;
}
echo $response->getContent(); // (*)
$class can be only an instance of a Homepage class which has only one method (show()), called in (**):
class Homepage
{
private $request;
private $response;
private $renderer;
public function __construct(
Request $request,
Response $response,
Renderer $renderer
) {
$this->request = $request;
$this->response = $response;
$this->renderer = $renderer;
}
public function show() {
$data = [
'name' => $this->request->getParameter('name', 'stranger'),
];
$html = $this->renderer->render('Homepage', $data);
$this->response->setContent($html); // (***)
}
}
With all that said, the application returns a 200 HTTP response with an empty body [here (*)]
but if I try to print the content of the HTTP response after (***) I get the correct response.
This could mean that there are two different instances of an HttpResponse class. (Is that right?)
By using rdlowrey/auryn, the author of the tutorial, used the method share() to share the same HttpReponse instance among classes, as shown in the "original" Dependencies.php file:
<?php declare(strict_types = 1);
use \Auryn\Injector;
...
$injector = new Injector;
$injector->alias('Http\Response', 'Http\HttpResponse');
$injector->share('Http\HttpResponse');
...
return $injector;
Is there a way to get the same behavior using PHP-DI (with PHP definitions)?
Here's my version of Dependencies.php:
<?php declare(strict_types = 1);
$definitions = [
'Http\Request' => DI\create('Http\HttpRequest')->constructor(
$_GET, $_POST, $_COOKIE, $_FILES, $_SERVER),
'Http\HttpRequest' => function () {
$r = new Http\HttpRequest($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
return $r;
},
'Http\Response' => DI\create('Http\HttpResponse'),
'Twig\Environment' => function () {
$loader = new Twig\Loader\FilesystemLoader(
dirname(__DIR__) . '/templates');
$twig = new Twig\Environment($loader);
return $twig;
},
'Example\Template\TwigRenderer' => function (Twig\Environment $renderer) {
return new Example\Template\TwigRenderer($renderer);
},
'Example\Template\Renderer' => DI\create(
'Example\Template\TwigRenderer')->constructor(
DI\get('Twig\Environment')),
];
$containerBuilder = new DI\ContainerBuilder;
$containerBuilder->addDefinitions($definitions);
$container = $containerBuilder->build();
return $container;
In Bootstrap.php, getting (get()) HttpRequest/HttpResponse instances, instead of making (make()) them, solved the problem.
...
$container = include('Dependencies.php');
$request = $container->get('Http\HttpRequest');
$response = $container->get('Http\HttpResponse');
...
As clearly stated in the documentation:
The make() method works like get() except it will resolve the entry
every time it is called. [..] if the entry is an object, an new instance will be created every time [..]
Hello I try to setup cakephp for rest client (with login auth) for ionic (angular) app.
Ok, I configure CakePhp like this setup tutorial and for example I get data that:
public function projects()
{
$projects = $this->Projects->find('all');
$this->set([
'projects' => $projects,
'_serialize' => ['projects']
]);
}
and get data via $.http in Ionic
This work perfectly but I try to configure cake auth for mobile client.
I don't know how I do this. In my Resttest Controller i wrote code where set session Id for ionic app, but ionic not cache this session and I think is my cakePhp code is wrong.
CakePHP controller:
<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Controller\Component\RequestHandlerComponent;
// use Cake\View\Helper\SessionHelper;
class ResttestController extends AppController
{
public function initialize()
{
parent::initialize();
$this->loadComponent('RequestHandler');
$this->loadModel('Projects');
$this->loadModel('Task');
$this->loadModel('User');
$this->viewBuilder()->layout(false);
$this->response->header('Access-Control-Allow-Origin', '*');
$this->loadComponent('Auth', [
'loginAction' => [
'controller' => $this->name,
'action' => 'login',
// '_ext'=>'json'
],
'authorize'=>['Controller'],
]);
// Basic setup
$this->Auth->config('authorize', ['Controller']);
}
public function login(){
header('Access-Control-Allow-Headers: Content-Type, x-xsrf-token');
$this->response->header('Access-Control-Allow-Methods', '*');
if($this->request->is('post')){
$postdata = file_get_contents("php://input");
$d = json_decode($postdata);
if($this->Auth->user()){
$response =array("success"=>2,'msg'=>'logged After');
}
// $d = $this->request->data;
if(!$d->password || !$d->login){
$response = array("success"=>0,'msg'=>'n');
}
$u = $this->User->find()
->where(['email'=>$d->login])
->first();
if($u){
$salt = $u->salt;
$input_password = crypt($d->password, '$2y$12$' . $salt);
$password = $u->password;
if($password == $input_password){
$tok = self::getToken();
$u->token = $tok;
$out = $this->Auth->setUser($u);
$response = array("success"=>1,'msg'=>'logged', 'token'=>$tok, 'out'=>$out,'sadga'=>$this->Auth->identify,'asf'=>$this->Auth,'adsafsfq'=>$d,'$this->request'=>$this->request,'$this->response'=>$this->response,'apache_request_headers '=>apache_request_headers());
}else{
$response = array("success"=>0,'msg'=>'n');
}
}else{
$response = array("success"=>0,'msg'=>'n');
}
}else{
$response =array("success"=>0,'msg'=>'n');
}
$this->set([
'response' => $response,
'_serialize' => ['response']
]);
}
private function getToken(){
return crypt(sha1(md5(uniqid(rand(), true))));
}
public function testAuth(){
}
}
This code return session and user data but not work and I think is not good method for mobile auth. Do you have any idea for auth for cakephp ?
How I make my code more security ?
When we split application to backend api and frontend, we should consider backend as stateless application. This mean you can't use session for auth.
Instead you should implements auth/login and auth/register rest endpoints that will return some token for example JWT.
For cakephp2 you can easely find such library: https://github.com/t73biz/cakephp2-jwt-auth
Use this authenticator instead of Form when you configure Auth component.
From front end side pass token like it is described in the plugin.
I want to test my basic auth protected pages. The test for unauthorization works fine. But I struggle on the authorized login, as I do not know how to set the headers on in the test.
I could not find a hint, how to set headers on $this->call(). The only information I could find was:
$this->call($method, $uri, $parameters, $cookies, $files, $server, $content);
and there are the headers missing.
How do I easily test basic auth on laravel. Concrete: How do I set the basic auth header for the test request?
What I currently have:
class ExampleTest extends TestCase {
public function test401UnauthorizedOnMe() {
$response = $this->call('GET', '/api/me');
$this->assertResponseStatus( 401);
}
public function testCorrectLoginOnMe() {
// http://shortrecipes.blogspot.de/2009/12/testing-basic-http-authentication-using.html
//send header with correct user and password i.e.
////YWRtaW46YWRtaW4xMg== is equal to base64_encode( "admin:admin12")
$this->request->setHeader( 'Authorization','Basic YWRtaW46YWRtaW4xMg==');
$response = $this->call('GET', '/api/me');
$this->assertResponseStatus(200);
}
}
I tried $this->$request->setHeader(); but with this I only get an error:
1) ExampleTest::testCorrectLoginOnMe
ErrorException: Undefined property: ExampleTest::$request
Found the solution with HTTP authentication with PHP. This can be used in the $server parameter of $this->call().
Here's my working function:
public function testCorrectLoginOnMe() {
// call( $method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null)
$this->call('GET', '/api/me', [], [], [], ['PHP_AUTH_USER' => 'admin', 'PHP_AUTH_PW' => 'admin12']);
$this->assertResponseStatus( 200 );
}
Basic Auth is usually achieved with a header key of 'Authorization'. For convenience, I have the following method in my base TestCase class:
protected function withBasicAuth(User $user, $password = 'password'): self
{
return $this->withHeaders([
'Authorization' => 'Basic '. base64_encode("{$user->email}:{$password}")
]);
}
Then in any of my test cases I can run a HTTP test with a user authenticated over basic auth like so:
$user = User::factory()->create();
$this->withBasicAuth($user)
->get('/');
->assertStatus(Response::HTTP_OK);
Note: the default password for a user created from the factory is 'password'.
$encoded_details = base64_encode('admin:admin12');
$headers = [
'HTTP_Authorization' => 'Basic '. $encoded_details
];
$response = $this->withHeaders($headers)->json('GET', '/api/me');
Just another solution which worked for me
protected function basicAuthenticate($detailsEncoded, $user, $password): self
{
$_SERVER['HTTP_AUTHORIZATION'] = $detailsEncoded;
$_SERVER['PHP_AUTH_USER'] = $user;
$_SERVER['PHP_AUTH_PW'] = $password;
return $this;
}
$response = $this->basicAuthenticate($detailsEncoded, $user, $password)->get($url);
$response->assertStatus(200);