Laravel - Unit Testing with SoftDeletes - php

I have a basic test which verifies that a user can only see its own posts.
In my test_database I whip up a couple posts, attach some of them to the user with the ModelFactory - which I then test that this user can only see the posts that are linked to the user according to the 'user_id' column in the post records. The test basically asserts that the output of a query is as expected.
I'm using the SoftDeletes trait in my Post model.
I have 'deleted_at' in the 'protected $dates' property on Post Model.
The problem is that the 'deleted_at' field is NOT returned in the factory()->create() output - but it IS present in the query where I fetch all the user's posts.
I was able to solve this by adding the 'deleted_at' field to the factory with value null, but it seems silly to me to have to add this field to every factory.
Why do I explicitly need to do this for 'deleted_at' but not for example for 'created_at' and 'updated_at'?
Code
Post schema
Schema::create('posts', function (Blueprint $table) {
$table->increments('id');
$table->integer('company_id')->unsigned();
$table->integer('locale_id')->unsigned();
$table->string('name');
$table->text('description')->nullable();
$table->boolean('active')->default(false);
$table->datetime('start_date')->nullable()->default(null);
$table->datetime('end_date')->nullable()->default(null);
$table->softDeletes();
$table->timestamps();
});
Post factory
$factory->define(App\Post::class, function (Faker\Generator $faker) {
return [
'company_id' => function () {
return factory(App\Company::class)->create()->id;
},
'locale_id' => function () {
return factory(App\Locale::class)->create()->id;
},
'title' => $faker->sentence(rand(1,3)),
'body' => $faker->paragraph(1),
'active' => rand(0,1),
'start_date' => $faker->dateTimeBetween('now', '+2 years')->format('Y-m-d H:i:s'),
'end_date' => $faker->dateTimeBetween('+2 years', '+4 years')->format('Y-m-d H:i:s')
];
});
My unit test
public function test_scope_foruser_only_returns_posts_for_authenticated_user()
{
// Given there are 2 companies
$companies = factory(Company::class, 2)->create();
// With 1 post per company
$postX = factory(Post::class)->create(['company_id' => $companies->first()->id]);
$postY = factory(Post::class)->create(['company_id' => $companies->last()->id]);
// Given I am logged in as user of the first company
$user = factory(User::class)->create([
'typeable_type' => Company::class,
'typeable_id' => $companies->first()->id
]);
// Login
$this->be(User::first());
// When I fetch the posts for User (via company)
$posts = Post::forUser()->get()->toArray();
// Then It returns only posts which the user's company
// is associated with in a properly formatted array
$this->assertCount(1, $posts);
$this->assertEquals([$postX->toArray()], $posts);
}
This is the query scope applied to the Post model as seen above
public function scopeForUser($query)
{
if ($company = auth()->user()->company) {
return $query->where(['company_id' => $company->id]);
}
return $query;
}

Related

Laravel 8: Relationships Between User and Posts. Outputting Data and Storing into Database

This is a continuation of my last question.
I like to create a relationship between a user (with an account type that’s equal to a “profile”) and my job posts. What I did was create a relationship like this in my models (not sure if correct tho)
User.php
public function jobposts()
{
$this->hasMany(JobPost::class)->where('account_type', 'profile');
}
JobPost.php
public function userprofile()
{
$this->belongsTo(User::class)->where('account_type', 'profile');
}
JobPostController.php
public function store(Request $request)
{
$this->validate($request, [
'job_name' => 'required|max:100',
'describe_work' => 'required|max:800',
'job_category' => 'required|not_in:0',
'city' => 'required|not_in:0',
'state' => 'required|not_in:0',
'zip' => 'required|regex:/\b\d{5}\b/',
]);
dd(auth()->user()->jobpost()->job_name);
}
2021_11_20_211922_create_job_posts_table.php
Schema::create('job_posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->contrained()->onDelete('cascade');
$table->string('job_name');
$table->text('describe_work');
$table->string('job_category');
$table->timestamps();
});
Got 2 questions about what I can do in the JobPostController.php.
How do I dd() to test the output?
This seems wrong
dd(auth()->user()->jobpost()->job_name);
How do I add it correctly into the DB like this?
public function store(Request $request)
{
$request->user()
->jobpost()
->create([
'job_name' => $request->job_name
]);
}

Laravel exists custom validation rule unable to validate user id with phpunit

I have a validation rule taken from the Laravel Documentation which checks if the given ID belongs to the (Auth) user, however the test is failing as when I dump the session I can see the validation fails for the exists, I get the custom message I set.
I have dumped and died the factory in the test and the given factory does belong to the user so it should validate, but it isn't.
Controller Store Method
$ensureAuthOwnsAuthorId = Rule::exists('authors')->where(function ($query) {
return $query->where('user_id', Auth::id());
});
$request->validate([
'author_id' => ['required', $ensureAuthOwnsAuthorId],
],
[
'author_id.exists' => trans('The author you have selected does not belong to you.'),
]);
PHPUnit Test
/**
* #test
*/
function adding_a_valid_poem()
{
// $this->withoutExceptionHandling();
$user = User::factory()->create();
$response = $this->actingAs($user)->post(route('poems.store'), [
'title' => 'Title',
'author_id' => Author::factory()->create(['name' => 'Author', 'user_id' => $user->id])->id,
'poem' => 'Content',
'published_at' => null,
]);
tap(Poem::first(), function ($poem) use ($response, $user)
{
$response->assertStatus(302);
$response->assertRedirect(route('poems.show', $poem));
$this->assertTrue($poem->user->is($user));
$poem->publish();
$this->assertTrue($poem->isPublished());
$this->assertEquals('Title', $poem->title);
$this->assertEquals('Author', $poem->author->name);
$this->assertEquals('Content', $poem->poem);
});
}
Any assistance would be most appreciated, I'm scratching my head at this. My only guess is that the rule itself is wrong somehow. All values are added to the database so the models are fine.
Thank you so much!
In your Rule::exists(), you need to specify column otherwise laravel takes the field name as column name
Rule::exists('authors', 'id')
Since column was not specified, your code was basically doing
Rule::exists('authors', 'author_id')

Is $post->comments()->create() more expensive than Comment::create() in Laravel?

In Laravel PHP Framework, when you have, let's say, a relationship between two tables e.g. one post can have one or multiple comments, you can create the comments of the post these ways:
// Option 1
$post->comments()->create(['text' => 'Greate article...']);
or
// Option 2
Comment::create([
'post_id' => 1,
'text' => 'Greate article...',
]);
Of course, it depends on the cases. Below are my cases.
For both options, the Post ID 1 has already been validated in the form request whether the post with the ID 1 does exist or not.
For some reasons, I already need to retrieve the post from the database first, thus I already have the post model.
In these above cases, is the Option 1 more expensive than the Option 2?
You can test the queries your application makes by listening to the queries using DB::listen().
I have setup the following as a test:
Migrations:
Schema::create('posts', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('title');
$table->string('content');
$table->timestamps();
});
Schema::create('comments', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('post_id');
$table->string('text');
$table->timestamps();
});
Models:
class Post extends Model
{
protected $guarded = [];
public function comments()
{
return $this->hasMany(Comment::class);
}
}
class Comment extends Model
{
protected $guarded = [];
public function post()
{
return $this->belongsTo(Post::class);
}
}
Test:
$post = Post::create([
'title' => 'Hello World',
'content' => 'Here I am!',
]);
$queries = collect();
DB::listen(function ($query) use ($queries) {
$queries->push($query->time);
});
for ($i = 1; $i <= 3000; $i += 1) {
Comment::create([
'post_id' => $post->id,
'text' => 'Facade '.$i,
]);
$post->comments()->create([
'text' => 'Relation '.$i,
]);
}
$totalTime = [0, 0];
foreach ($queries as $idx => $time) {
$totalTime[$idx%2] += $time;
}
return [
'facade' => $totalTime[0],
'relation' => $totalTime[1],
];
This outputs:
array:2 [▼
"facade" => 1861.3
"relation" => 1919.9
]
So you can see the relation way of create is actually approximately 3% slower in my test scenario.
I have prepared this implode if you want to experiment further.
If it is "cheaper", it won't be by a noticeable amount. You're creating an instance of a Query builder, but you're not actually querying anything. So in this case it's only adding the post_id on the new model you're creating.
I don't think it's something you worry about too much.

How to call variables in controller Laravel

I'm trying to use Auth::user()->id; and post them with another model under 'user_id' so I don't have to manually give users a 'user_id'.
I've checked and included the required files and I'm getting the users "id" from Users Table
$user_id = Auth::user()->id;
echo $user_id; //this is returning right user "id"
I'm having trouble calling variables and posting it to DB in controller functions any help would be fine.
public function store(Request $request)
{
$user_id = Auth::user()->id;
$this->validate($request, [
'token1' => 'required',
'token2' => 'required'
]);
$tokens = new Tokens([
'user_id' => $user_id,
'token1' => $request->get('token1'),
'token2' => $request->get('token2')
]);
$tokens->save();
return view('/home');
}
Post the User "id" from User table into Tokens table's "user_id" so I can work with models
Getting this error:
SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL
constraint failed: tokens.user_id (SQL: insert into "tokens"
("token1", "token2", "updated_at", "created_at") values (asddsadf,
sdfasdf, 2019-09-24 11:53:57, 2019-09-24 11:53:57))
My migration is:
$table->bigIncrements('id');
$table->integer('user_id');
$table->string('token1');
$table->string('token2');
$table->timestamps();
You should avoid filling the user_id in this way.
The error is thrown because user_id is not a fillable property. But the correct way to handle this is with the relationships.
So I suppose that Tokens has a belongsTo relation because of its user_id foreign key (that anyway should be an unsignedInteger column) and in your model you have something like:
public function user() {
return $this->belongsTo(User::class)
}
If you have a look at the offical documentation you will see that you should change your code in this way:
public function store(Request $request)
{
$this->validate($request, [
'token1' => 'required',
'token2' => 'required'
]);
$tokens = new Tokens([
// 'user_id' => $user_id, This is useless
'token1' => $request->get('token1'),
'token2' => $request->get('token2')
]);
// With this method you're going to set the user_id column
$token->user()->associate(auth()->user());
$tokens->save();
return view('/home');
}

Eloquent automatically selects unwanted columns upon eager-loading model relationship

I am converting an internal API from HTML (back-end) processing to JSON (using Knockout.js) processing on the client-side to load a bunch of entities (vehicles, in my case).
The thing is our database stores sensitive information that cannot be revelead in the API since someone could simply reverse engineer the request and gather them.
Therefore I am trying to select specifically for every relationship eager-load the columns I wish to publish in the API, however I am having issues at loading a model relationship because it seems like Eloquent automatically loads every column of the parent model whenever a relationship model is eager loaded.
Sounds like a mindfuck, I am aware, so I'll try to be more comprehensive.
Our database stores many Contract, and each of them has assigned a Vehicle.
A Contract has assigned an User.
A Vehicle has assigned many Photo.
So here's the current code structure:
class Contract
{
public function user()
{
return $this->belongsTo('User');
}
public function vehicle()
{
return $this->belongsTo('Vehicle');
}
}
class Vehicle
{
public function photos()
{
return $this->hasMany('Photo', 'vehicle_id');
}
}
class Photo
{
[...]
}
Since I need to eager load every single relationship listed above and for each relationship a specific amount of columns, I need to do the following:
[...]
$query = Contract::join('vehicles as vehicle', 'vehicle.id', '=', 'contract.vehicle_id')->select([
'contract.id',
'contract.price_current',
'contract.vehicle_id',
'contract.user_id',
'contract.office_id'
]);
[...]
$query = $query->with(['vehicle' => function ($query) {
$query->select([
'id',
'trademark',
'model',
'registration',
'fuel',
'kilometers',
'horsepower',
'cc',
'owners_amount',
'date_last_revision',
'date_bollo_expiration',
'bollo_price',
'kilometers_last_tagliando'
]);
}]);
$query = $query->with(['vehicle.photos' => function ($query) {
$query->select([
'id',
'vehicle_id',
'order',
'paths'
])->where('order', '<=', 0);
}]);
$query = $query->with(['user' => function ($query) {
$query->select([
'id',
'firstname',
'lastname',
'phone'
]);
}]);
$query = $query->with(['office' => function ($query) {
$query->select([
'id',
'name'
]);
}]);
[...]
return $this->response->json([
'error' => false,
'vehicles' => $vehicles->getItems(),
'pagination' => [
'currentPage' => (integer) $vehicles->getCurrentPage(),
'lastPage' => (integer) $vehicles->getLastPage(),
'perPage' => (integer) $vehicles->getPerPage(),
'total' => (integer) $vehicles->getTotal(),
'from' => (integer) $vehicles->getFrom(),
'to' => (integer) $vehicles->getTo(),
'count' => (integer) $vehicles->count()
],
'banner' => rand(0, 2),
'filters' => (count($input) > 4),
'filtersHelpText' => generateSearchString($input)
]);
The issue is: if I do not eager load vehicle.photos relationship, columns are loaded properly. Otherwise, every single column of Vehicle's model is loaded.
Here's some pictures so you can understand:
Note: some information have been removed from the pictures since they are sensitive information.
You can set a hidden property on your models which is an array of column names you want to hide from being output.
protected $hidden = ['password'];

Categories