How to structure HTTP test for Laravel relationship? - laravel

Using the blog scenario, I have a Post and an Author model. There is a many-to-many relationship with additional attributes on the relationship. In the application, the pivot attributes are saved using
$post->author()->save($author, ['review' => 'Pending']);
How do I format that type of request in a test?
$post = Post::factory()->create();
$author = Author::factory()->create();
$response = $this->actingAs($this->user_update)->patch(**request data**);
I'd like to have a test for each type of user.

When writing tests for an endpoint, you should mostly be testing how it responds to different types of data. For example:
If I send a request with a valid body or parameters, I expect to receive a status code 200 and maybe some data depending on your use case.
If I send a request while being unauthenticated, I expect to receive a status code of 401.
If I send invalid data, I expect to receive a status code of 422 and some error messages for the invalid fields.
If the entity I'm trying to fetch/update/delete does not exits, I expect to
receive get a status code of 404.
With status code 200, or as I like to call them "happy cases", if we can easily identify a new/updated record, it doesn't hurt to test it's working correctly. The majority of testing for the business logic should happen on the service layer.
public function testPostCanBeCreatedForAuthor() {
// arrange
$user = User::factory()->create();
$author = Author::factory()->create(['user_id' => $user->id]);
// act
$response = self::actingAs($user)->postJson('/api/posts', [
'title' => 'A very good title',
'content' => 'Lorem ipsum dolor...'
]);
// assert
$response->assertOk();
$post = Post::where('author_id', $author->id)->first();
self::assertNotNull($post);
self::assertSame('A very good title', $post->title);
// ...
}
public function testPostUpdateRespondsNotFoundWithInvalidPostId() {
$user = User::factory()->create();
$response = $this->actingAs($user)->patchJson('/api/posts/invalid-post-id', [
'title' => 'A very good title',
'content' => 'Lorem ipsum dolor...'
]);
$response->assertNotFound();
}
Edit:
If you want to test the pivot table values, do this:
// App\Models\Post
public function author() {
return $this->belongsToMany(Author::class)->withPivot('review');
}
// ...
// In your test
self::assertSame('Pending', $post->author()->first()->pivot->review);

Related

laravel testing web routes

I have a web route for manufactures module (Backed route that handle resource)
Route::resource('/admin/manufactures', App\Http\Controllers\Back\ManufacturerController::class);
I have create a create a ManufacturerRequest with a simple rule name=>required
and i want to use Laravel Test to test the resource (CRUD)
In my controller the store method is as follow
public function store(ManufacturerRequest $request)
{
//db
$request->validate();
Manufacturer::create($request->all());
}
I have a simple test
$response = $this->post('/admin/manufactures' ,
[
'_token' => csrf_token(),
'name' => 'test'
]);
$response->assertStatus(200);
which is return 403, but besides that the store Method takes ManufacturerRequest object that handles validation, and in the case of the test i pass an array because it only accepts array.
So how can I create a Test that "simulate" form and pass request to controller in order to test validation and CRUD
What you want to do is very easy and is also explained on the Documentation, it is very important that you fully read the documentation so you have a rough idea of what you can do with the framework, specially because you are new with it.
As you did not specify which version you are using, I will be using Laravel 8, but it is roughly the same across the board.
Based on your code:
Resource route
Route::resource('/admin/manufactures', ManufacturerController::class);
Controller
public function store(ManufacturerRequest $request)
{
//db
$request->validate();
Manufacturer::create($request->all());
}
You need to change your controller to:
public function store(ManufacturerRequest $request)
{
//db
Manufacturer::create($request->all());
}
Yes, just remove the $request->validate(); as the framework will automatically resolve the FormRequest and authorize and validate. If you read part of the validate explanation you will see this:
So, how are the validation rules evaluated? All you need to do is type-hint the request on your controller method. The incoming form request is validated before the controller method is called, meaning you do not need to clutter your controller with any validation logic.
So, when the first line of the controller is run, it means the FormRequest passed the authorization check and validated the input.
What you can also update on your controller is:
public function store(ManufacturerRequest $request)
{
//db
Manufacturer::create($request->validated());
}
See I have changed $request->all() with $request->validated(), validated will only return the fields you have a key on the FormRequest's rules, if you use all you will be passing everything you have on the request (also passing non-validated data and that is not good).
Before you try anything, I recommend you read my answer on this post, so you can have a clearer picture about testing.
So, you are getting a 403 maybe because you have a middleware asking for you to be logged in, and you did not use $this->actingAs().
Just because you did not share the FormRequest rules, I will just give a super small example. If you have a this rule inside:
'name' => ['required', 'string'],
What you can do to test that is:
public function test_manufacturer_is_created(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->post('/admin/manufactures', ['name' => $name = 'Manufacturer 1']);
$response->assertSuccessful();
$this->assertDatabaseHas(
'manufacturers',
[
'name' => $name
]
);
}
/**
* #depends test_manufacturer_is_created
*/
public function test_unauthorized_error_is_thrown_when_the_user_is_not_logged_in(): void
{
$response = $this->post('/admin/manufactures', ['name' => 'Manufacturer 1']);
$response->assertUnauthorized();
}
/**
* #depends test_manufacturer_is_created
* #dataProvider invalidDataProvider
*/
public function test_error_should_be_returned_when_invalid_data_is_sent($value, bool $deleteField): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->post(
'/admin/manufactures',
! $deleteField ? ['name' => $value] : []
);
$response->assertInvalid(['name']);
}
public function invalidDataProvider(): array
{
return [
'Missing name' => [null, true],
'Empty name' => ['', false],
'Null name' => [null, false],
'Array name' => [[], false],
'Number name' => [123, false],
'Boolean name' => [true, false],
];
}
Have in mind I used a lot of things in here:
I have tested if the normal insertion works, if it is checking the a valid name is input (FormRequest rules) and that if the user is not logged in it should throw an unauthorized exception.
I have used #depends, that is used to run tests ONLY if the dependant test passes, that way we can prevent running the "negative" tests just because the normal flow did not succeed, so it makes no sense to run the other ones and also get a "the test did not pass".
I have also used #dataProvider, that is used to share data with a test, so instead of copy-pasting the test a lot of times with data variation, you just vary what data you want the test to use.

What are the benefits of using Laravel Request Class in API's development?

Is there any benefit of using laravel requests classes for store and update methods in developing restful API's? Or do I have to make custom Validator::make response?
I have been facing difficulty in modifying the response format from failed requests as to follow some development standards requirements.
Can we modify the failed responses format from request class for API's?
I prefer to use independent from request class because there is at least one benefit: more clear code.
you can generate response as you wish like this (this is my solution, maybe there are more better solutions. i use this solution to return only one validation error not all. you can modify it as you wish):
in your Form request class add this method:
protected function failedValidation(Validator $validator)
{
$this->validator = $validator;
foreach ($validator->messages()->getMessages() as $key => $value) {
$first_messages_only[$key] = $value[0];
}
throw new ValidationException($first_messages_only);
}
and then in your Exception handler class, write this block of code in your render() method:
if ($exception instanceof ValidationException) {
$response = [
'status' => false,
'message' => trans('api.general.validation_not_passed'), // $exception->getMessage()
'data' => ['validation_errors' => $exception->validator]
];
return response()->json($response);
}
Since you asked its usage in API development then you can easily tell request class that you want json response by adding application/json header in your request then it will return json response.
Request class is best approach to validate incoming input from user which provides a lot of other features as well.
In Request class you can write validation rules for all request types e.g. get,post,put|patch or delete
You can allow or disallow anyone using authorize method based on your project logic.
You can write custom messages and send them custom error message bags.
If you write whole thing in a controller method then that will not be a good approach and difficult to manage while request class will make you comfortable while dealing with validations only.
protected $errorBag = 'custom_errors_bag'
public function authorize()
{
return true; //or any other logic here to authorize the user
}
public function rules()
{
switch ($this->method()){
case 'POST':
return [
'username' => 'required|string|max:20|unique:users',
//...
];
case 'PUT':
case 'PATCH':
return [
'username' => 'required|string|max:20|unique:users,id',
//...
];
case 'DELETE':[
'id' => 'required'
//...
];
default:break;
}
}
public function messages()
{
return [
'username.required' => 'Please provide username',
'username.unique' => 'Username must be unique',
//...
];
}

Laravel Return control to caller and continue working

I have two APIs that should communicate with each other. The first one sends the second files, which must be processed. However the processing time is unknown and may take a while.
A solution I thought of is the following - when the processing is ready, the second API sends a request to the first one with the result.
The problem is that in all that time, the first API's request is waiting for a response, because I haven't returned yet the control, which leads into a deadlock.
My question is how can I return a result to the first API (something like received: true/false) as soon as possible, continue with the processing and send the result (from the processing) in a separate request.
I am using Laravel 5.2 and Guzzle for the requests.
Firstly, this is proof of concept and not working code and may require some rework.
From your first api, you should be making a call to the second api telling it to process something.
The second api should then respond once it has received the data (not once processed).
First API
use GuzzleHttp\Client;
public function callApiTwo(Request $request)
{
$client = new Client()
$data = [];
$res = $client->post('http://apitwoexample.com', [
'body' => json_encode($data)
]);
if ($res->getStatus() !== 203) {
// error in processing
}
$resData = json_decode($res->getBody()->getContents());
$token = $resData['token'];
return response()->json([
'success' => 'Data is being processed',
'token' => $token
], 202);
}
Second API
The second api should then receive data (files links, images, data etc) and dispatch a job (as mentioned by #jonathon mentioned).
You then need some sort of identifier for the process/job/record. A id or token of some sort. This is sent back to the first api in its response.
public function executeProcess(Request $request)
{
// Perform logic
$token = str_rand(10);
Job::dispatch('/job/which/needs/time/to/run', [
'data' => $data,
'token' => $token
]);
return response()->json([
'success' => 'Data is being processed',
'token' => $token
], 202);
}
public function progressStatus(Request $request)
{
$model = Model::whereToken($request->token)->first();
return response()->json([
'status' => $model->status
], 200);
}
You'll notice the progressStatus method, which i've put as an example to check the progess of a process/job/record by some identifier.
From the first api, this can be fired off within an artisan command or a job which is re-queued to fire again (after a delay) based on whether the second api has confirmed the process completed.
Let me know if this requires more clarity.

laravel phpunit testing expecting status code 201 but received 302?

I have a simple PostController with a store method that receives data, validates it, stores it and then redirects to /posts with a success message.
I have written a test to ensure that the post is created and saved as follows:
/** test */
public function testStore()
{
$user = factory('App\User')->create();
$post = factory('App\Post')->create(['user_id' => $user->id]);
$response = $this->actingAs($user)
->json('POST', '/notice', $post->toArray())
->assertStatus(201)
->assertJson([
'created' => true,
]);
}
But I am receiving 301(redirection) instead of 201. I do not understand this.

How can I use Laravel Validation in the API, it's returning my view now?

I have this route in route/api.php:
Route::post('/register', 'LoginController#register');
My LoginController
class LoginController extends Controller {
public function register(Request $request) {
$this->validate($request, // <-- using this will return the view
// from **web.php** instead of the expected json response.
[
'email' => 'required|email',
'firstName' => 'required|alpha_dash',
'lastName' => 'required',
'password' => 'required|confirmed',
]);
$input = $request->all();
//$plain_password = $input['password'];
$input['uuid'] = uuid();
$input['password'] = Hash::make($input['password']);
$user = User::create($input);
dd($errors);
$response['succes'] = true;
$response['user'] = $user;
return response($response);
}
}
Why does adding a validation call change the behaviour to returning my view / the wrong route. I want the api to validate my request too, not just my "frontend".
When you use Laravel's validate method from controller it automatically handles/takes the step if the validation fails. So, depending on the required content type/request type, it determines whether to redirect back or to a given url or sending a json response. Ultimately, something like thos following happens when your validation fails:
protected function buildFailedValidationResponse(Request $request, array $errors)
{
if ($request->expectsJson()) {
return new JsonResponse($errors, 422);
}
return redirect()->to($this->getRedirectUrl())
->withInput($request->input())
->withErrors($errors, $this->errorBag());
}
So, if the first if statement is true then you'll get a json response and it'll be true if you either send an ajax request or if you attach a accept header with your request to accept json response (When requesting from a remote server). So, make sure your request fulfills the requirements.
Alternatively, you can manually validate the request using the Validator component and return a json response explicitly if it fails.

Resources