Laravel Phpunit, AssertJsonFragment not working as expected - laravel

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);
}

Related

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

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.

Laravel validation: exclude size check when field not required

I have validation in Laravel application, but I can't seem to make it work.
I am making validation on Supplier model not request.
public function requestOpening(Supplier $supplier)
{
$validator = Validator::make($supplier->toArray(), $this->fullValidationRules());
if ($validator->fails()) {
return redirect("open-new/supplier/{$supplier->id}")
->withErrors($validator);
}
// ...
}
I want hfm_code to be required only when is_hfm == 1. I tried both:
'hfm_code' => 'size:6|exclude_if:is_hfm,0',
and
'hfm_code' => 'size:6|required_if:is_hfm,1',
In both scenarios I get validation error:
The hfm code must be 6 characters.
How do I ignore size check when is_hfm == 0 ?
The order seems to matter here:
'hfm_code' => 'exclude_if:is_hfm,0|size:6'
should work. I think this is because it evaluates rules in order and will ignore the remaining ones if exclude_if passes

Test api returns 201 instead 200

I do not understand what happens on an API test (laravel 8).
This call (a very simple put) returns a response 200 , using postman.
The same test using phpunit, returns 201 :
public function testPutOrganizationOk()
{
$organization = Organization::factory()->create();
$superAdmin = User::factory()->create([
'organization_id' => $organization->id,
'role_id' => 'SUPERADMIN'
]);
Sanctum::actingAs($superAdmin);
$organizationToModify = [
'name' => 'mon organization moif',
'contact' => 'contact name modif',
'comment' => 'comment comment comment modif',
'ads_max' => 12345,
'state_id' => 'VALIDATED'
];
$response = $this->putJson($this->getUrl() . '/organizations/' . $organization->id, $organizationToModify);
$response->assertStatus(200);
}
The error is :
Tests\Feature\OrganizationTest::testPutOrganizationOk Expected status code 200 but received 201. Failed asserting that 200 is
identical to 201.
I tried a lot of things , without success. I really do not understand what happens. Any suggestions will be appreciated. Thanks.
EDIT :my controller
public function update(StoreOrganizationRequest $request, Organization $organization)
{
$this->authorize('update', Organization::class);
$organizationUpdated = $this->organizationRepository->updateOrganization($organization, $request->only(['name', 'contact', 'comment', 'ads_max', 'state_id']));
return new OrganizationResource($organizationUpdated);
}
EDIT 7 hours later ;-)
When I replace , in the controller, the return of the resource by a return of a simple json, then I have the same behaviour between postman and phpunit . The api call receives a 200 for the update.
Strange, it means that the problem is around the resource ?
Why a different behavior between postman and phpunit ? Who is right : postman or phpunit ?
The http code 201, it mean created success.
see here developer.mozilla.org
and you able to customize the header code by:
return Response::json(new OrganizationResource($organizationUpdated), 200);
201 Status Code says that you just create an Instance, and
200 Status Code says that already existing Instance has been update
The PUT method requests that the enclosed entity be stored under the supplied Request-URI. If the Request-URI refers to an already existing resource, the enclosed entity SHOULD be considered as a modified version of the one residing on the origin server. If the Request-URI does not point to an existing resource, and that URI is capable of being defined as a new resource by the requesting user agent, the origin server can create the resource with that URI."
I might be wrong but seems like you have created the instance first and trying to modifying it then
Finally, I give up!
I will write the response with a status code like that:
return (new OrganizationResource($organization))->response()->setStatusCode(200);
instead of:
return new OrganizationResource($organization);
it's longer to write, but at least my tests are OK.

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

Form input values not remain after flash error message

How to solve above issue? I want to keep test even after a validation failure in the submission. But after session error message is passed all the entered data will be gone.
To re-fill the form with the input data again, check the input validation then if validation fails, redirect the user back along with his input data and validation errors.
$validator = Validator::make($request->all(), [
// your validation rules.
'name' => 'required',
]);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
// Continue your app logic.
You'll find more information in Laravel validation docs
You should send back the input also to the view.
Like Sameh Salama already mentioned, use the following code:
function () {
return redirect()->back()->withErrors($validator)->withInput();
}
Notice the withInput() function, it returns the old input.
Use it in the View as
<input type="something" value="{{$input->first_something}}" />

Resources