Using Laravel 9 to update and delete test not passing the test - laravel

I have have created a feature test to make sure that my controllers work as expexted, but for some reason the test keeps on failing, im getting this error message
Missing required parameter for [Route: admin/suppliers/destroy] [URI: admin/suppliers/{supplier}] [Missing parameter: supplier].
i have added the parameter to the route: route('admin/suppliers/destroy', $supplier),
but i still get the same error, Does anyone have a idea on what could create this error
Here is my test
public function test_if_a_user_can_delete_a_supplier()
{
//Make fake data, don't persist to database.
$supplier = Supplier::factory()->make()->setAppends([])->makeHidden(['created_at', 'updated_at']);
//Create an Admin User and assign the Administrator role to this new user
$adminUser = factory(AdminUser::class)->create();
$adminUser->roles()->sync(Role::where('name', 'Administrator')->first());
$this->actingAs($adminUser, config('admin-auth.defaults.guard'))
->json(
'DELETE',
route('admin/suppliers/destroy', $supplier),
$supplier->toArray()
)
->assertStatus(302)
->assertRedirectToRoute('admin/suppliers/index');
$this->assertDatabaseMissing(
'suppliers',
$supplier->toArray()
);
}

//Make fake data, don't persist to database.
$supplier = Supplier::factory()->make()->setAppends([])->makeHidden(['created_at', 'updated_at']);
You are not persisting the Supplier model to the database.
If route model binding is used in the controller method for the route 'admin/suppliers/destroy', then route('admin/suppliers/destroy', $supplier) will return HTTP 404 Not Found, which would your test fail because you are expecting an HTTP 302 Found response.
Also, when passing a variable to the route, it tries to get the model's id. Since you didn't persist $supplier to the database, it has no id. This probably causes the error you see
Missing required parameter for [Route: admin/suppliers/destroy] [URI: admin/suppliers/{supplier}] [Missing parameter: supplier].
Even without the $this->actingAs(...) statement, your $this->assertDatabaseMissing(...) would pass, so your test isn't actually testing what it's supposed to test.
I'd rewrite the test like this:
public function test_if_an_admin_user_can_delete_a_supplier()
{
// ARRANGE
$supplier = Supplier::factory()->create(['name' => 'Fake Supplier']);
$admin_role = Role::where('name', 'Administrator')->first();
$admin_user = AdminUser::factory()->has($admin_role)->create();
// Some people like to place a "pre-assertion" to make sure the Act phase is the reason changes occurred. In this case, it would look like this
// $this->assertDatabaseHas('suppliers', ['name' => 'Fake Supplier']);
// ACT
$response = $this->actingAs($admin_user, config('admin-auth.defaults.guard'))
->deleteJson(route('admin/suppliers/destroy', $supplier), [
'name' => 'Fake Supplier', /* using this or $supplier->name comes down to choice */
]);
// ASSERT
$response->assertStatus(302)
->assertRedirectToRoute('admin/suppliers/index');
$this->assertDatabaseMissing('suppliers', [
'name' => 'Fake Supplier', /* using this or $supplier->name comes down to choice */
]);
}
And some things could still be refactored. Like for example, making this
$admin_role = Role::where('name', 'Administrator')->first();
$admin_user = AdminUser::factory()->has($admin_role)->create();
into one line using factory states. And if it's a line that repeats in a lot of tests in your test class, then making it a property as part of the setUp() method.

Related

Laravel Phpunit, AssertJsonFragment not working as expected

I've done this thousands of times on past projects, but I feel since moving to laravel 8 on our latest application something has changed.
I used to be able to do something as simple as:
$response = $this->post('/api/team', []);
$response->assertJsonFragment([
"The team name field is required."
]);
However when running the test I get the following error:
1) Tests\Feature\Controllers\Team\CreateTest::teamNameRequired
Unable to find JSON fragment:
[["The team name field is required."]]
within
[["{\"team_name\":[\"The team name field is required.\"]}"]].
Failed asserting that false is true.
I've tried swapping to assertJson and a couple others but ideally this is how I'd like to assert, I also could create a separate function or use some other helpers but I want to assert not just that there has been a validation error, but a specific one.
I'm using the standard laravel validator and response object for context:
$validator = Validator::make($request->all(), [
'name' => 'required|string|between:2,100'
]);
if($validator->fails()){
return response()->json($validator->errors()->toJson(), 400);
}
This was caused because I was json encoding the errors twice.
Within my controller the following if statement should have been used:
if($validator->fails()){
return response()->json($validator->errors(), 400);
}

Issue with Integers passing data from Vue to Laravel using FormData

I am successfully updating a database using Vue 2 to a Laravel 8 Controller using Axios. However, I am stuck when attempting to pass an integer to my database.
My database has a column, 'number_of_searches' and it must be an integer.
Laravel Migration looks like this:
$table->integer('number_of_searches')->nullable();
And the model looks something like this:
class Product extends Model
{
protected $fillable = [
'product_title',
'number_of_searches' => 'integer',
];
}
My Vue updateProduct() function used FormData and appends the values coming from the form. It looks like this:
updateProduct(product){
let data = new FormData();
data.append('_method', 'PATCH');
data.append('product_title', product.product_title);
data.append('number_of_searches', product.number_of_searches);
axios.post('/api-route-to-update/product_id/', data)
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
}
My update controller looks like this:
public function update(Request $request, $product_id){
$product = Product::findOrFail($product_id);
$product->update($request->all());
$product->save();
}
I can have as many input fields as I need and it works perfectly as long as they are strings. However, when I use a number input field in my component such as:
<input v-model="product.number_of_searches" type="number" min="1" max="999">
The generated json that will pass from axios into my controller looks like this:
{ "id": 5, "product_title": "The Product Title will work great", "number_of_searches": "222"}
You will notice that 'number_of_searches' is passed as a string, hence my database fails because it is the wrong datatype, it requires an integer. After reading the documentation and other threads, it seems that FormData will always return strings and that I must just deal with this on the server side.
So what I did is, I went into my back-end updateProduct() method and attempted to modify the Request.
First I tried a few methods such as:
//this one
$requestData = $request->all();
$requestData['number_of_searches'] = 123;
//also this one
$request->merge(['number_of_searches' => 123]);
//and this
$data = $request->all();
$data['number_of_searches'] = 123;
After countless hours, I am unable to modify the original request. After doing some research, it seems that requests are protected and cannot be modified, which makes sense. Therefore I attempted to create a new request that clones $request->all(), like this:
$new_request = new Request($request->all());
$new_request->merge(['number_of_searches' => 123]);
But I have failed to force to override 'number_of_searched'
My question is:
Should I stay away from FormData completely in this case? What method do you suggest to pass forms that have integers or floats or other datatypes through axios or fetch? Or what am I doing wrong? I find it hard to believe that FormData would only send strings (making parseInt useless before using axios). I'm sure I am doing something wrong from origin.
On the other hand, maybe I need to completely change my approach in my Controller when receiving the data. I am working on an app with a lot of fields and I love $request->all() because it simplifies what I am trying to do. I wouldn't mind using intval on the server side and that's it, but it seems overly complicated.
On the Vue side, you can use the number modifier on v-model to make sure it's not casting the value to a string:
v-model.number="product.number_of_searches"
On the request side, you can use $request->merge to override the value in a request
$request->merge([
'number_of_searches' => (int) $request->get('number_of_searches');
]);
At the model side in the updating hook within the boot method you can ensure the value is being casted as an int when saving:
static::updating(function ($model) {
$model->number_of_searches = (int) $model->number_of_searches;
});
This should give you the end to end.

How can I validate GET controller params in CakePHP 2?

Given this on the model:
public $validate = [
'amount' => array(
'rule' => array('comparison', '>=', 0),
'message' => 'You must buy over 0 of this item!'
)
];
How can I validate param #2 of the below?
public function buy(int $item, int $amount) {
Validation seems to be built only for POST, which I'd like to opt out of here.
First things first, modifying the database with GET requests is an anti-pattern for many different reasons. Even if you assume a friendly user agent (which you never should!), browsers can behave quirky and do unexpected stuff like for example sending GET request multiple times (that is perfectly valid as GET is not ment to modify data), which they usually won't for POST/PUT/DELETE.
I would strongly suggest to change your endpoint to handle POST requests instead.
That being said, you can generally validate whatever you want, the validation mechanisms first and foremost just validate data, they don't know or care where it stems from. You can hand over whatever data you want to your model, and let it validate it:
$data = array(
'item' => $item,
'amount' => $amount,
);
$this->ModelName->set($data);
if ($this->ModelName->validates()) {
// data is valid
} else {
// data is invalid
$errors = $this->ModelName->validationErrors;
}
Moreover you can use CakePHP's validation methods completely manually too:
App::uses('Utility', 'Validation');
$isValid = Validation::comparison($amount, '>' 0);
This example of course doesn't make too much sense, given that $isValid = $amount > 0 would do the same, however it should just show that you can validate anything everywhere without models being involved.
See also
Cookbook > Models > Data Validation > Validating Data from the Controller
Cookbook > Models > Data Validation > Core Validation Rules

Laravel 5.3 dynamic routing to multiple controllers

I'm using Laravel 5.3. I have a bunch of urls that I'd like to handle with a single route, to multiple controllers.
e.g.
GET /admin/foo => FooController#index
GET /admin/foo/edit/1 => FooController#edit($id)
GET /admin/bar => BarController#index
GET /admin/bar/edit/1 => BarController#item($id)
GET /admin/baz => BazController#index
GET /admin/baz/edit/1 => BazController#item($id)
etc.
I want to be able to detect if the controller exists, and if not throw a 404 or route to a default controller (which may throw a 404).
Below is what I've got so far, but I'm not sure what I'm doing. Shouldn't I be instantiating the controller using the service container? I don't think I should be hardcoding namespaces like this. And my handling of the id parameter is sketchy. Perhaps I should have two routes for these two patterns or something?
Route::get('/admin/{entityType}/{action?}/{id?}', function ($entityType, $action = 'index', $id = null) {
$controllerClass = 'App\Http\Controllers\\' . ucfirst($entityType) . 'Controller';
$controller = new $controllerClass;
$route = app(\Illuminate\Routing\Route::class);
$container = app(\Illuminate\Container\Container::class);
return (new Illuminate\Routing\ControllerDispatcher($container))->dispatch($route, $controller, $action);
abort(404);
});
I'd recommend you to define a route for every controller explicitly. This is the best way to build a maintainable app.
Also, if using one route and one method is an option (with right architecure it is) use one route:
Route::get('/admin/{entityType}/{action?}/{id?}', 'Controller#method');
And one entry point:
public function method($entity, $action = null, $id = null)
{
// Handle request here.
https://laravel.com/docs/5.3/routing#parameters-optional-parameters

Controller and Routes in Laravel

What basically is the difference between Controller and Routes. We can control our data using routes file, then why do we need controllers?
Like:
<?php
// app/routes.php
// route to process the ducks form
Route::post('ducks', function()
{
// process the form here
// create the validation rules ------------------------
$rules = array(
'name' => 'required', // just a normal required validation
'email' => 'required|email|unique:ducks', // required and must be unique in the ducks table
'password' => 'required',
'password_confirm' => 'required|same:password' // required and has to match the password field
);
// do the validation ----------------------------------
// validate against the inputs from our form
$validator = Validator::make(Input::all(), $rules);
// check if the validator failed -----------------------
if ($validator->fails()) {
// get the error messages from the validator
$messages = $validator->messages();
// redirect our user back to the form with the errors from the validator
return Redirect::to('ducks')
->withErrors($validator);
} else {
// validation successful ---------------------------
// our duck has passed all tests!
// let him enter the database
// create the data for our duck
$duck = new Duck;
$duck->name = Input::get('name');
$duck->email = Input::get('email');
$duck->password = Hash::make(Input::get('password'));
// save our duck
$duck->save();
// redirect ----------------------------------------
// redirect our user back to the form so they can do it all over again
return Redirect::to('ducks');
}
});
Well, this is not my code, I read it somewhere, But, here this person has used the validation in routes.php file, and in my project, I used the validation technique in a controller named UserController, what difference does it make?
Routes translate each incoming HTTP request to an action call, for example to a method of a controller, whereas controller is the place where business logic are written. There is nothing wrong in handling all in one file, but once your projects gets bigger it would be nightmare to manage such code. It's like responsibility, route, route the request to specific controller, controller process it, pass result to view. Mostly it's design pattern.
We can even have all the code in one huge file without using any classes at all, but we know that is not a good idea. The current best practice is to separate the code depending on responsibilities (single responsibility principle) to make it easier to other developers to read and understand the code. Often the next developer is yourself in some months, so having a clean structure don't only benefit others but also your sanity when going back to your old code.
The name router imply that the class routs data, in this case from an URI to a controller and the controller handle the business rules for that particular controller
Routes in laravel is a place where you define your application end points and controller is where you write your business logic.
I had the same problem understanding Laravel when I started to learn and to make it simple, I have created some project in MCV style please check this
https://github.com/jagadeshanh/understanding-laravel

Resources