Laravel - input not passing over through unit test - php

I'm receiving the below error when running my unit tests. Seems that it doesn't like passing in the Input::get to the constructor, however when running the script within the browser the action works fine so I know it's not the controller code. If I take out any of the 'task_update' code the test passes with just the find even with the Input - so not sure why it accepts the Input for one method.
ErrorException: Argument 1 passed to Illuminate\Database\Eloquent\Model::__construct() must be of the type array, null given, called
My Controller is:
public function store()
{
$task_update = new TaskUpdate(Input::get('tasks_updates'));
$task = $this->task->find(Input::get('tasks_updates')['task_id']);
$output = $task->taskUpdate()->save($task_update);
if (!!$output->id) {
return Redirect::route('tasks.show', $output->task_id)
->with('flash_task_update', 'Task has been updated');
}
}
And the test is - I'm setting the input for task_updates array but just isn't being picked up:
Input::replace(['tasks_updates' => array('description' => 'Hello')]);
$mockClass = $this->mock;
$mockClass->task_id = 1;
$this->mock->shouldReceive('save')
->once()
->andReturn($mockClass);
$response = $this->call('POST', 'tasksUpdates');
$this->assertRedirectedToRoute('tasks.show', 1);
$this->assertSessionHas('flash_task_update');

I believe the "call" function is blowing away the work done by Input::replace.
the call function can actually take a $parameters parameter which should fix your problem.
if you look in \Illuminate\Foundation\Testing\TestCase#call, you'll see the function:
/**
* Call the given URI and return the Response.
*
* #param string $method
* #param string $uri
* #param array $parameters
* #param array $files
* #param array $server
* #param string $content
* #param bool $changeHistory
* #return \Illuminate\Http\Response
*/
public function call()
{
call_user_func_array(array($this->client, 'request'), func_get_args());
return $this->client->getResponse();
}
If you do:
$response = $this->call('POST', 'tasksUpdates', array('your data here'));
I think it should work.

I do prefer to do both Input::replace($input) and $this->call('POST', 'path', $input).
Example AuthControllerTest.php:
public function testStoreSuccess()
{
$input = array(
'email' => 'email#gmail.com',
'password' => 'password',
'remember' => true
);
// Input::replace($input) can be used for testing any method which
// directly gets the parameters from Input class
Input::replace($input);
// Here the Auth::attempt gets the parameters from Input class
Auth::shouldReceive('attempt')
->with(
array(
'email' => Input::get('email'),
'password' => Input::get('password')
),
Input::get('remember'))
->once()
->andReturn(true);
// guarantee we have passed $input data via call this route
$response = $this->call('POST', 'api/v1/login', $input);
$content = $response->getContent();
$data = json_decode($response->getContent());
//... assertions
}

Related

Symfony 5.4 - how to filter/sanitize/validate request parameter in Rest API action

I am fairly new to Symfony 5.4 and recently created my first API using that version
For my specific API endpoint one of the parameters is an array of IDs.
I need to validate this array in the following way:
make sure that this IS an array;
make sure that IDs in the array actually refer to database records;
I implemented it in a straightforward way where I check the array before persisting the entity using typecasting and existing Repository:
$parentPropertyIds = (array)$request->request->get('parent_property_ids');
if ($parentPropertyIds) {
$parentCount = $doctrine->getRepository(Property::class)->countByIds($parentPropertyIds);
if ($parentCount !== count($parentPropertyIds)) {
return $this->json([
'status' => 'error',
'message' => 'parent_property_id_invalid'
], 422);
}
foreach ($parentPropertyIds as $parentPropertyId) {
$parentProperty = $doctrine->getRepository(Property::class)->find($parentPropertyId);
$property->addParent($parentProperty);
}
}
However, this makes my controller action become too "body-positive" and also feels like something that could be implemented in a more elegant way.
I was unable to find anything in Symfony 5.4 docs.
At the moment I am wondering if:
there is a way to filter/sanitize request parameter available in Symfony;
there is an elegant built-in way to apply custom validator constraint to a request param (similar to well-documented entity field validation);
Full endpoint code:
/**
* #Route("/property", name="property_new", methods={"POST"})
*/
public function create(ManagerRegistry $doctrine, Request $request, ValidatorInterface $validator): Response
{
$entityManager = $doctrine->getManager();
$property = new Property();
$property->setName($request->request->get('name'));
$property->setCanBeShared((bool)$request->request->get('can_be_shared'));
$parentPropertyIds = (array)$request->request->get('parent_property_ids');
if ($parentPropertyIds) {
$parentCount = $doctrine
->getRepository(Property::class)
->countByIds($parentPropertyIds);
if ($parentCount !== count($parentPropertyIds)) {
return $this->json([
'status' => 'error',
'message' => 'parent_property_id_invalid'
], 422);
}
foreach ($parentPropertyIds as $parentPropertyId) {
$parentProperty = $doctrine->getRepository(Property::class)->find($parentPropertyId);
$property->addParent($parentProperty);
}
}
$errors = $validator->validate($property);
if (count($errors) > 0) {
$messages = [];
foreach ($errors as $violation) {
$messages[$violation->getPropertyPath()][] = $violation->getMessage();
}
return $this->json([
'status' => 'error',
'messages' => $messages
], 422);
}
$entityManager->persist($property);
$entityManager->flush();
return $this->json([
'status' => 'ok',
'id' => $property->getId()
]);
}
You could use a combination of Data Transfer Object (DTO) with Validation service. There is a number of predefined constraints or you could create a custom one.
For expamle, how to use simple constraint as an annotation:
class PropertyDTO {
/**
* #Assert\NotBlank
*/
public string $name = "";
public bool $shared = false;
}
Then assign data to DTO:
$propertyData = new PropertyDTO();
$propertyData->name = $request->request->get('name');
...
In some cases it is a good idea to define a constructor in the DTO, then get all data from the request and pass it to DTO at once:
$data = $request->getContent(); // or $request->getArray(); depends on your content type
$propertyData = new PropertyDTO($data);
Then validate it:
$errors = $validator->validate($propertyData);
if (count($errors) > 0) {
/*
* Uses a __toString method on the $errors variable which is a
* ConstraintViolationList object. This gives us a nice string
* for debugging.
*/
$errorsString = (string) $errors;
return $this->json([
'status' => 'error',
'message' => 'parent_property_id_invalid'
], 422);
}
//...

Laravel TDD ignored URL query params when trying to catch them in Controller with request

I would like to write a test to be sure my query parameters in URL are working fine. But whenever I do a request()->all(), I got an empty array.
Here is my test part:
public function test_it_should_paginate_the_results_when_perPage_query_is_used()
{
// Given
factory('App\Order', 17)->create();
// When
// $request = $this->call('GET', route('api.orders.index'), ['perPage' => '5']);
// $request = $this->call('GET', route('api.orders.index') . '?perPage=5', ['perPage' => '5']);
$request = $this->get(route('api.orders.index') . '?perPage=5');
// Then
$request->assertJsonCount(5);
}
And here is the basic method in OrderController:
public function index()
{
dd(request()->all());
$orders = Order::all();
return $orders;
}
Did I miss something ?
I've forgot to mention that it picks up the parameters in the browser, but not in PHPUnit test tool
UPDATED QUESTION
So I found out a problem. It does not working when you are testing with php artisan test command. It works only when you are using phpunit command.
No idea why.
You can pass the data as the second argument to route() or as the third argument to call():
$request = $this->get(route('api.orders.index', ['perPage' => 5]));
$request = $this->call('GET', route('api.orders.index', ['perPage' => 5]));
$request = $this->call('GET', route('api.orders.index'), ['perPage' => 5]);
Here are the defintions:
function route($name, $parameters = [], $absolute = true)
public function call($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null)

MercadoLibre API gives error "getAuthUrl() should not be called statically"

I have the following 2 codes from library https://github.com/javiertelioz/mercadolibre to connect with MercadoLibre's API:
Class Meli.php:
<?php
namespace App\Sources;
use App\Sources\MercadoLibre\Utils;
class Meli extends Utils {
/**
* #version 1.0.0
*/
const VERSION = "1.0.0";
/**
* Configuration for urls
*/
protected $urls = array(
'API_ROOT_URL' => 'https://api.mercadolibre.com',
'AUTH_URL' => 'http://auth.mercadolibre.com.ar/authorization',
'OAUTH_URL' => '/oauth/token'
);
/**
* Configuration for CURL
*/
protected $curl_opts = array(
CURLOPT_USERAGENT => "MELI-PHP-SDK-1.0.0",
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_TIMEOUT => 60
);
protected $client_id;
protected $client_secret;
/**
* Constructor method. Set all variables to connect in Meli
*
* #param string $client_id
* #param string $client_secret
* #param string $access_token
*/
public function __construct($client_id, $client_secret, $urls = null, $curl_opts = null) {
$this->client_id = $client_id;
$this->client_secret = $client_secret;
$this->urls = $urls ? $urls : $this->urls;
$this->curl_opts = $curl_opts ? $curl_opts : $this->curl_opts;
}
/**
* Return an string with a complete Meli login url.
*
* #param string $redirect_uri
* #return string
*/
public function getAuthUrl($redirect_uri) {
$params = array("client_id" => $this->client_id, "response_type" => "code", "redirect_uri" => $redirect_uri);
$auth_uri = $this->urls['AUTH_URL'] . "?" . http_build_query($params);
return $auth_uri;
}
}
and the Controller MeliController.php with the following code:
class MeliController extends Controller
{
/**
* Login Page (Mercado Libre)
*/
public function login() {
session()->regenerate();
return view('auth/melilogin')->with('auth', [
'url' => meli::getAuthUrl(env('ML_AUTHENTICATION_URL', '')),
]);
}
public function logout() {
if(session('profile')) {
session()->forget('profile');
session()->flush();
}
return \Redirect::to('/auth/melilogin');
}
}
But Im receiving error:
Non-static method App\Sources\Meli::getAuthUrl() should not be called
statically
Three procedures I made with no success:
1- using facade (meli) as in the first example
meli::getAuthUrl
2- replacing code:
public function getAuthUrl($redirect_uri) {
$params = array("client_id" => $this->client_id, "response_type" => "code", "redirect_uri" => $redirect_uri);
$auth_uri = $this->urls['AUTH_URL'] . "?" . http_build_query($params);
return $auth_uri;
}
}
with public static function and $self instead of $this but with no success.
3- Making the call dynamic using:
'url' => (new \App\Sources\Meli)->getAuthUrl(env('ML_AUTHENTICATION_URL', '')),
But receiving error
Too few arguments to function App\Sources\Meli::__construct(), 0
passed in
/Applications/MAMP/htdocs/price2b/app/Http/Controllers/MeliController.php
any help appreciated.
The error tells you the problem: you are calling the method statically (meli::getAuthUrl(...)), but it's not a static method. You have to call it on an instance of the class. This means that your third approach:
'url' => (new \App\Sources\Meli)->getAuthUrl(env('ML_AUTHENTICATION_URL', '')),
is the right one.
But, as you pointed out, you get a "too few arguments" error. This is because you are passing no arguments when you instantiate the Meli class. That is, new \App\Sources\Meli is equivalent to new \App\Sources\Meli(), passing zero arguments to the constructor.
But the constructor for the Meli class, which you posted above, looks like this:
public function __construct($client_id, $client_secret, $urls = null, $curl_opts = null)
So, you need to pass at least 2 arguments, not zero. In other words, at a minimum, something like this:
'url' => (new \App\Sources\Meli($someClientId, $someClientSecret))->getAuthUrl(env('ML_AUTHENTICATION_URL', '')),

Laravel phpunit testing get with parameters

I am writing some tests for my controllers but one of my tests doesn't work. It's supossed to search and get the results back to the page. But it's actually redirecting to the home page. Here is my code:
use DatabaseMigrations;
protected $user;
public function setUp()
{
parent::setUp();
$this->seed();
$this->user = factory(User::class)->create(['role_id' => 3]);
}
/** #test */
public function test_manage_search_user()
{
$response = $this->followingRedirects()->actingAs($this->user)->get('/manage/users/search', [
'choices' => 'username',
'search' => $this->user->username,
]);
$response->assertViewIs('manage.users');
$response->assertSuccessful();
$response->assertSee($this->user->email);
}
The URL you should get to make it work look like this:
http://localhost/manage/users/search?choices=username&search=Test
I checked again and it looks like it's not given in the parameters with the get request. How can I fix this?
I had the same issue trying to test GET Requests, you actually can't pass parameter with the $this->get('uri', [header]) but you can by using $this->call, if you check in MakesHttpRequests.php you can see that this->get() is actually using call method.
By adding an array to get method, you are changing the request headers, this is why you are not getting your parameters.
public function get($uri, array $headers = [])
{
$server = $this->transformHeadersToServerVars($headers);
return $this->call('GET', $uri, [], [], [], $server);
}
public function call($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null)
{
$kernel = $this->app->make(HttpKernel::class);
$files = array_merge($files, $this->extractFilesFromDataArray($parameters));
$symfonyRequest = SymfonyRequest::create(
$this->prepareUrlForRequest($uri), $method, $parameters,
$cookies, $files, array_replace($this->serverVariables, $server), $content
);
$response = $kernel->handle(
$request = Request::createFromBase($symfonyRequest)
);
if ($this->followRedirects) {
$response = $this->followRedirects($response);
}
$kernel->terminate($request, $response);
return $this->createTestResponse($response);
}
So if you want to test a GET Request you will have to do this:
$request = $this->call('GET', '/myController', ["test"=>"test"]);
In your controller you should be able to get theses parameters like so:
public function myController(Request $request)
{
$requestContent = $request->all();
$parameter = $requestContent['test'];
}
I'm using Laravel 5.X (more precisely 5.6), you can pass custom parameters using:
$response = $this->json('GET', '/url/endpoint',['params'=>'value']);
You can use the route helper to build a url with query string. in your case i would do something like this. Assuming the route name is manage.users.search
$route = route('manage.users.search', [
'choices'=> 'username',
'search' => $this->user->username,
]);
$response = $this->followingRedirects()
->actingAs($this->user)
->get($route);
In order to send parameters with GET requests.
If you use the route() method then you can pass the data as the second parameter.
$response = $this->get(route('route_name', ['key' => value]));
If you using URL directly, you could use like this
$response = $this->get('url?' . Arr::query(['key' => value]));
Do whatever you want to do with $response.
You could use the request helper to merge in http get parameters as such:
/** #var \Illuminate\Http\Request $request */
$request = request();
$request->merge([
'choices' => 'username',
'search' => 'Test'
]);
This worked for me simply pass the parameter as part of the url as follows:
$response = $this->get('api/endpoint?parameter1='.$this->dynamicParam);
Add a helper function:
if (!function_exists('extend_url_with_query_data')) {
function extend_url_with_query_data(string $url, array $queryData): string
{
if ($queryData == []) {
return $url;
}
$glue = mb_strpos($url, '?') === false ? '?' : '&';
$queryString = http_build_query($queryData);
return "{$url}{$glue}{$queryString}";
}
}
Usage:
$queryData = [
'works' => true,
];
$this->get(
extend_url_with_query_data('/api/v1/example', $queryData)
);
I would do it like this:
$this->actingAs($this->user);
$response = $this->get('/manage/users/search', [
'choices' => 'username',
'search' => $this->user->username,
]);
$response->assertViewIs('manage.users');
$response->assertSuccessful();
$response->assertSee($this->user->email);

Laravel testing with JWT-Auth

I'm trying to test my api that's made with JWT_auth: https://github.com/tymondesigns/jwt-auth
class UpdateTest extends TestCase
{
use DatabaseTransactions;
public $token;
public function signIn($data = ['email'=>'mail#gmail.com', 'password'=>'secret'])
{
$this->post('api/login', $data);
$content = json_decode($this->response->getContent());
$this->assertObjectHasAttribute('token', $content);
$this->token = $content->token;
return $this;
}
/** #test */
public function a_user_updates_his_account()
{
factory(User::class)->create([
'name' => 'Test',
'last_name' => 'Test',
'email' => 'mail#gmail.com',
'mobile' => '062348383',
'function' => 'ceo',
'about' => 'About me.....',
'corporation_id' => 1
]);
$user = User::first();
$user->active = 2;
$user->save();
$this->signIn();
$url = '/api/user/' . $user->slug . '?token=' . $this->token;
$result = $this->json('GET', $url);
dd($result);
}
}
Result is always:
The token could not be parsed from the request
How do I get this t work!?
Source (https://github.com/tymondesigns/jwt-auth/issues/206)
One way to test your API in this situation is to bypass the actual token verification, but still log your user in (if you need to identify the user). Here is a snippet of the helper method we used in our recent API-based application.
/**
* Simulate call api
*
* #param string $endpoint
* #param array $params
* #param string $asUser
*
* #return mixed
*/
protected function callApi($endpoint, $params = [], $asUser = 'user#example.org')
{
$endpoint = starts_with($endpoint, '/')
? $endpoint
: '/' . $endpoint;
$headers = [];
if (!is_null($asUser)) {
$token = auth()->guard('api')
->login(\Models\User::whereEmail($asUser)->first());
$headers['Authorization'] = 'Bearer ' . $token;
}
return $this->json(
'POST',
'http://api.dev/' . $endpoint,
$params,
$headers
);
}
And is used like this:
$this->callApi('orders/list', [
'type' => 'customers'
])
->seeStatusOk()
Basically, there is not really a way for now. The fake request that is created during testing and is passed to Laravel to handle, somehow drops the token data.
It has alredy been reported in an issue (https://github.com/tymondesigns/jwt-auth/issues/852) but as far as I know, there is no solution yet.

Categories