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.'" }'
);
Related
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
Objective
I am writing phpunits tests, and I want to get the code coverage.
Problem
To do End To End tests, I have a Php build in server to setup my API, then the tests call this api.
But everything that is being executed by the server is not in the report.
For exemple I tested a controller with api calls, and on my report there is 0%.
All the file that are tested without the Php build int server are ok, it's only the tests that works with that are not counted.
Is there a way to count it ?
Some code
I am using a php that create an http server to do end to end tests on my api.
Here is my class
<?php
use GuzzleHttp\Client;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Process\Process;
class ApiTestCase extends \PHPUnit\Framework\TestCase
{
protected static string $public_directory;
protected static Process $process;
const ENVIRONMENT = 'test';
const HOST = '0.0.0.0';
const PORT = 9876; // Adjust this to a port you're sure is free
public static function setUpBeforeClass(): void
{
$command = [
'php',
'-d',
'variables_order=EGPCS',
'-S',
self::HOST . ':' . self::PORT,
'-t',
self::$public_directory,
self::$public_directory.'/index.php'
];
// Using Symfony/Process to get a handler for starting a new process
self::$process = new Process($command, null, [
'APP_ENV' => self::ENVIRONMENT
]);
// Disabling the output, otherwise the process might hang after too much output
self::$process->disableOutput();
// Actually execute the command and start the process
self::$process->start();
// Let's give the server some leeway to fully start
usleep(100000);
}
public static function tearDownAfterClass(): void
{
self::$process->stop();
}
/**
* #param array<string,mixed>|null $data
* #param string $path
* #param string $method
* #return ResponseInterface
* #throws \GuzzleHttp\Exception\GuzzleException
*/
protected function dispatch(string $path, string $method = 'POST', ?array $data = null): ResponseInterface
{
$params = [];
if ($data) {
$params['form_params'] = $data;
}
$client = new Client(['base_uri' => 'http://127.0.0.1:' . self::PORT]);
return $client->request($method, $path, $params);
}
}
So in my test i can use it like that and it works fine
class MyApiTest extends \App\Tests\ApiDbTestCase
{
public function testAuthClientSuccess()
{
// creation of $parameters
$res = $this->dispatch('/v1/user/my/url', 'POST', $parameters);
// My asserts are done after that
}
}
I am using github action to create
name: Phpunit coverage
on: [push]
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout#v2
- uses: php-actions/composer#v6
- uses: php-actions/phpunit#v3
with:
php_extensions: pdo_pgsql xdebug
args: --coverage-html
env:
XDEBUG_MODE: coverage
- name: Archive code coverage results
uses: actions/upload-artifact#v2
with:
name: code-coverage-report
path: output/code-coverage
I have a login issue when performing tests on some controller for an API with Symfony 5.2.
All my endpoints are behind a firewall, and to test everything works fine, I need to login before I make a request.
I use a JWT authentication system with https://github.com/lexik/LexikJWTAuthenticationBundle
So I have this private method on my test class :
protected function logInAsUser(string $username): User
{
$tokenStorage = static::$client->getContainer()->get('security.token_storage');
$firewallName = 'api_area';
$userRepository = static::$container->get(UserRepository::class);
$user = $userRepository->findOneBy(['username' => $username]);
$token = new PostAuthenticationGuardToken($user, $firewallName, ['ROLE_USER']);
$tokenStorage->setToken($token);
return $user;
}
Then in my test methods, I call this method before making a request on a protected endpoint, like this :
public function testSomething() {
$this->logInAsUser('my-username');
static::$client->request('GET', '/api/protected/endpoint/');
$this->assertEquals(200, static::$client->getResponse()->getStatusCode());
}
And it works fine
(without calling $this->logInAsUser('my-username); I got a 401 response as intended)
But when I try to make two requests in the same test method, the second one fails with a 401, and the error message is : JWT Token not found
Example:
public function testSomething() {
$this->logInAsUser('my-username');
static::$client->request('GET', '/api/protected/endpoint/');
$this->assertEquals(200, static::$client->getResponse()->getStatusCode()); // OK
static::$client->request('GET', '/api/another/protected/endpoint/');
$this->assertEquals(200, static::$client->getResponse()->getStatusCode()); // FAILURE
}
I tried to re login before the second call, but it doesn't change anything.
Since Symfony 5.1 the Client used in WebTestcases provides a built-in method to mock a logged in user:
public function loginUser($user, string $firewallContext = 'main'): self
You should try using that one instead of doing it yourself.
public function testSomething()
{
$client = static::createClient();
$testUser = $userRepository->findOneByUsername('my-username');
$client->loginUser($testUser);
$client->request('GET', '/api/protected/endpoint/');
$this->assertEquals(200, $client->getResponse()->getStatusCode()); // OK
$client->request('GET', '/api/another/protected/endpoint/');
$this->assertEquals(200, $client->getResponse()->getStatusCode()); // FAILURE
}
Hi I'm trying to set JWT token after the login test is complete in a global var and then extend all tests off of that class, now I'm not sure if I am even doing it right so any help is appreciated or documentation I could look up
<?php
use Illuminate\Support\Facades\Config;
class AuthenticationTest extends TestCase {
/**
* #var string
*/
public $token = '';
/**
* Register test user
*
* #return void
*/
public function testRegister()
{
$this->json('post', '/auth/register', [
'email' => Config::get('tests.email'),
'password' => Config::get('tests.password'),
'companyName' => Config::get('tests.companyName'),
'firstname' => Config::get('tests.firstname'),
'lastname' => Config::get('tests.lastname')
]);
$this->assertEquals(200, $this->response->status());
$this->seeJsonEquals([
'message' => "Registration successful, please confirm email to login",
'status' => true,
]);
}
/**
* Login test user
*
* #return void
*/
public function testLogin()
{
$email = Config::get('tests.email');
$password = Config::get('tests.password');
$encryptedToken = base64_encode($email . ":" . $password);
$this->json('post', 'auth/login', ['token' => $encryptedToken]);
$this->assertEquals(200, $this->response->status());
$this->seeJsonStructure([
'token',
'refresh',
'status'
]);
$content = json_decode($this->response->getContent());
$this->assertObjectHasAttribute('token', $content);
$this->token = $content->token;
$this->token;
}
}
?>
The login test works as it should but the token isn't being set for other tests that extend off of this test case ad use $this->token to send the JWT token
PHPUnit creates a new instance of AuthenticationTest for every test method to run each test in isolation.
For tests that need an authenticated user, you have to obtain the token somehow. You could make an api request like you did in testLogin. A bit faster and less brittle way would be to create the token programatically using the same service as in your production code.
To reduce boilerplate, create a test case for tests with an authenticated and create the token in the setup method. Tests that extend from this class have the token property. Alternatively, you could also make this functionality a trait.
EDIT:
Here's a simple code example.
abstract class AuthenticatedUserTestCase extends TestCase
{
protected function token(): string
{
$email = Config::get('tests.email');
$password = Config::get('tests.password');
$encryptedToken = base64_encode($email . ':' . $password);
$response = $this->json('post', 'auth/login', ['token' => $encryptedToken]);
$content = json_decode($response->getContent());
if (!isset($content->token)) {
throw new RuntimeException('Token missing in response');
}
return $content->token;
}
}
final class UserProfileTest extends AuthenticatedUserTestCase
{
public function test_that_user_can_access_their_profile_page()
{
$response = $this->json('get', '/user/profile', [], ['token' => $this->token()]);
// ... assert stuff
}
}
I want to test the next method of my controller
function index(){
if(Auth::User()->can('view_roles'))
{
$roles = Role::all();
return response()->json(['data' => $roles], 200);
}
return response()->json(['Not_authorized'], 401);
}
it is already configured for authentication (tymondesigns / jwt-auth) and the management of roles (spatie / laravel-permission), testing with postman works, I just want to do it in an automated way.
This is the test code, if I remove the conditional function of the controller the TEST passes, but I would like to do a test using a user but I have no idea how to do it.
public function testIndexRole()
{
$this->json('GET', '/role')->seeJson([
'name' => 'admin',
'name' => 'secratary'
]);
}
Depends on what kind of app are you building.
A - Using Laravel for the entire app
If your using Laravel for frontend/backend, well to simulate a logged-in user you could use the awesome Laravel Dusk package, made by the Laravel team. You can check the documentation here.
This package has some helpful methods to mock login sessions amongs a lot more of other things, you can use:
$this->browse(function ($first, $second) {
$first->loginAs(User::find(1))
->visit('/home');
});
That way you hit an endpoint with a logged-in user of id=1. And a lot more of stuff.
B - Using Laravel as a backend
Now, this is mainly how I use Laravel.
To identify a user that hits an endpoint, the request must send an access_token. This token helps your app to identify the user. So, you will need to make and API call to that endpoint attaching the token.
I made a couple of helper functions to simply reuse this in every Test class. I wrote a Utils trait that is being used in the TestCase.php and given this class is extended by the rest of the Test classes it will be available everywhere.
1. Create the helper methods.
path/to/your/project/ tests/Utils.php
Trait Utils {
/**
* Make an API call as a User
*
* #param $user
* #param $method
* #param $uri
* #param array $data
* #param array $headers
* #return TestResponse
*/
protected function apiAs($user, $method, $uri, array $data = [], array $headers = []): TestResponse
{
$headers = array_merge([
'Authorization' => 'Bearer ' . \JWTAuth::fromUser($user),
'Accept' => 'application/json'
], $headers);
return $this->api($method, $uri, $data, $headers);
}
protected function api($method, $uri, array $data = [], array $headers = [])
{
return $this->json($method, $uri, $data, $headers);
}
}
2. Make them available.
Then in your TestCase.php use the trait:
path/to/your/project/tests/TestCase.php
abstract class TestCase extends BaseTestCase
{
use CreatesApplication, Utils; // <-- note `Utils`
// the rest of the code
3. Use them.
So now you can do API calls from your test methods:
/**
* #test
* Test for: Role index
*/
public function a_test_for_role_index()
{
/** Given a registered user */
$user = factory(User::class)->create(['name' => 'John Doe']);
/** When the user makes the request */
$response = $this->apiAs($user,'GET', '/role');
/** Then he should see the data */
$response
->assertStatus(200)
->assertJsonFragment(['name' => 'admin'])
->assertJsonFragment(['name' => 'secretary']);
}
Side note
check that on top of the test methods there is a #test annotation, this indicates Laravel that the method is a test. You can do this or prefix your tests names with test_