Laravel unit-test failing, but working in 'real life'? - laravel

I'm working on a new app. Most of my unit-tests work as I expect them to, except one where I'm updating an existing item:
/** #test */
function can_edit_thought()
{
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/submit', [
'thought' => 'My first thought'
]);
$thought_id = $user->thoughts->first()->id;
$response = $this->actingAs($user)->post('/submit', [
'thought' => 'something different',
'thought_id' => "1"
]);
// should get redirected to 'thoughts'
$response
->assertStatus(302)
->assertHeader('Location', url('/thoughts'));
// should still be 1 thought...
$this->assertEquals($user->thoughts->count(), 1);
// the first thought should now containt "different"
$thought = $user->thoughts->first();
//fwrite(STDERR, print_r($user->thoughts, TRUE));
$this->assertTrue(str_contains($thought->thought, "different"));
This last assert fails (and printing the 'thought' also shows it's not changed).
But when I do exactly the same in my app, the changes do happen.
Any ideas? (I'm leaning toward something with authentication, or the fact that test uses lite and real-app uses MySQL)

Found the bug, I needed to do a refresh:
// the first thought should now containt "different"
$thought = $user->thoughts->first();
$thought->refresh();
$this->assertTrue(str_contains($thought->thought, "different"));
(leaving this here for future bug hunters)

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.

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

Noticeable time increase when checking permissions after updating from laravel 5.7 to laravel 5.8 using silber/bouncer-rc5

I am using bouncer for my ACL needs and ever since upgrading my project from laravel 5.7 to 5.8 I've noticed a significant increase in the time it takes for my requests to process.
I'm dealing with two models (let's call them Parent and Child), as well as the permissions the authenticated user has over them.
// Takes about 110ms. Eager loads various nested relationships and counters with specific constraints
$parents = Parent::myScope(...)->get();
// Bottleneck. Takes 5 minutes (!). Used to take about 40 seconds on laravel 5.7
$parents->each(function ($parent) {
$parent->permissions = [
'edit' => auth()->user()->can('edit', $parent),
'delete' => auth()->user()->can('delete', $parent),
'restore' => auth()->user()->can('restore', $parent)
];
$parent->children()->each(function ($child) {
$child->permissions = [
'edit' => auth()->user()->can('edit', $child),
'delete' => auth()->user()->can('delete', $child),
'restore' => auth()->user()->can('restore', $child)
];
}
}
I'm appending the permissions like this because the $parents variable will be sent as json to the front-end. I'm pretty sure this implementation is wrong, and must have a better alternative but the real issue is this inexplicable five-fold increase in loading time.
The times were obtained using Debugbar measures.
Using the monitor command in redis-cli (I'm using Redis to cache the permissions), I've noticed the GET requests come more slowly than before. In fact, even after I stop a page from loading (ESC), the GET requests to Redis don't stop immediately. I'm not sure if this is normal behavior or not.
I tried to check the issues at the bouncer repo but I haven't found anything.
You're calling auth()->user() hundreds of times. Can you try calling it only once?
$user = auth()->user();
$parents->each(function ($parent) use ($user) {
$parent->permissions = [
'edit' => $user->can('edit', $parent),
'delete' => $user->can('delete', $parent),
'restore' => $user->can('restore', $parent)
];
$parent->children()->each(function ($child) {
$child->permissions = [
'edit' => $user->can('edit', $child),
'delete' => $user->can('delete', $child),
'restore' => $user->can('restore', $child)
];
}
}
Also, since you're eager-loading the children, you shouldn't fetch them all again within each loop iteration:
$parent->children()->each(function ($child) {
// ^^ remove these parentheses
$child->permissions = [
'edit' => $user->can('edit', $child),
'delete' => $user->can('delete', $child),
'restore' => $user->can('restore', $child)
];
}
After some testing a solution was found. It turns out, there was no problem with the code at all.
Something is wrong with the server. We do not know exactly what but trying to run the project on freshly installed machines got rid of those awful processing times. (Now times are at 15s on first request)
The server's problem got worse coincidentally after migrating from laravel 5.7 to 5.8 which led me to this wild goose chase.
ADDENDUM
The culprit was Xdebug. We used it to get code-coverage analysis but the performance was so bad we ended up switching to phpdbg.

Contentful/Laravel Delivery API setInclude does not work as expected

While using the contentful laravel sdk the set include is not fetching any of the assets that belong to the entries I fetch. The code below (mine):
public function index()
{
$query = $this->query
->setContentType('gallery')
->setInclude(10);
$entry = $this->client->getEntries($query);
if (!$entry) {
abort(404);
}
$data = ['galleries' => $entry];
return view('welcome', ['data' => $data]);
}
The contenful example:
// contentful.php 3.0
$client = new Contentful\Delivery\Client(
'<content_delivery_api_key>',
'<space_id>',
'<environment_id>' // Defaults to "master" if ommitted
);
$query = (new Contentful\Delivery\Query())
->setInclude(2);
$entries = $client->getEntries($query);
my results:
[]
I expect for the previewImage and Images array to include the 'fields' that contain the location of the file. I only get 'sys'. I can't see why the include is not fetching this data. If i set setInclude to 20, over the limit i get an error. Below
What should i do differently? I achieved the desired results in a javascript frontend project, but with laravel i get nothing.
The include is actually working fine. Internally, linked entries/assets are represented using a Link object, which is resolved to an actual entry/asset as soon as you access the field (in your case, that's done with $entry->getPreviewImage(), which will return the Asset object that was loaded in your previous query).
This means that if you dump the entry you won't see the actual object you're expecting in the previewImage field, but everything will work fine if in regular use.

Codeigniter form validation failing when it should succeed

I'm building an admin utility for adding a bulk of images to an app I'm working on. I also need to to log certain properties that are associated with the images and then store it all into the database.
So basically the script looks into a folder, compares the contents of the folder to records in the database. All of the info must be entered in order for the database record to be complete, hence the form validation.
The validation is working, when there are no values entered it prompts the entry of the missing fields. However it happens even when the fields ARE filled.
I'm doing something a bit funny which may be the reason.
Because I'm adding a bulk of images I'm creating the data within a for loop and adding the validation rules within the same for loop.
Here is the results:
http://s75151.gridserver.com/CI_staging/index.php/admin_panel/bulk_emo_update
Right now I have default test values in the form while testing validation. The submit button is way at the bottom. I'm printing POST variable for testing purposes.
Here is the code:
function bulk_emo_update() {
$img_folder_location = 'img/moodtracker/emos/';//set an image path
$emo_files = $this->mood_model->get_emo_images('*.{png,jpg,jpeg,gif}', $img_folder_location); //grab files from folder
$emo_records = $this->mood_model->get_all_emos(); //grab records from db
$i=1; //sets a counter to be referenced in the form
$temp_emo_info = array(); //temp vairable for holding emo data that will be sent to the form
//loop through all the files in the designated folder
foreach($emo_files as $file) {
$file_path = $img_folder_location.$file;//builds the path out of the flder location and the file name
//loops through all the database reocrds for the pupose of checking to see if the image file is preasent in the record
foreach($emo_records as $record) {
//compairs file paths, if they are the
if($record->picture_url != $file_path) {
//FORM VALIDATION STUFF:
$rules['segment_radio['.$i.']'] = "required";
$rules['emo_name_text_feild['.$i.']'] = "required";
//populating the temp array which will be used to construct the form
$temp_emo_info[$i]['path'] = $file_path;
$temp_emo_info[$i]['name'] = $file;
}
}
$i++;
}
//sets the reference to validation rules
$this->validation->set_rules($rules);
//checks to see if the form has all it's required fields
if ($this->validation->run() == FALSE) { //if validation fails:
print_r($_POST);
//prepairs the data array to pass into the view to build the form
$data['title'] = 'Bulk Emo Update';
$data['intro_text'] = 'fill out all fields below. hit submit when finished';
$data['emos_info'] = $temp_emo_info;
$this->load->view('admin_bulk_emo_update_view',$data);
} else { // if it succeeds:
//printing for test purposes
print_r($_POST);
$this->load->view('form_result');
}
}
I'm new to codeigniter and php in general so if anything looks outrageously weird please tell me, don't worry about my feelings I've got thick skin.
if ($this->validation->run() == FALSE)
if you are calling the run() method of the validation class every time the script is run, will it ever return TRUE and run the else? Maybe a different return?
I'm a little cornfused by what's going on. Generally, if I'm having a problem like this, I will figure out a way to force the result I'm looking for. e.g. in your code, I'd force that else to run... once I get it to run, break down what happened to make it run. Rudimentary, but it has served me well.
You use array of rules in
$this->form_validation->set_rules()
wrong.
If you want to pass the rules in array you must stick to the key names like described here http://codeigniter.com/user_guide/libraries/form_validation.html#validationrulesasarray
So instead of
$rules['input_name'] = "required"
try this:
array(
'field' => 'input_name',
'label' => 'Name that you output in error message',
'rules' => 'required'
)

Resources