Laravel not booting trait/authing user in unit tests - php

I can't figure out if Laravel is failing to boot my model trait, or is simply not seeing the user as being authed within the trait.
All of this code works perfectly fine when I test my app manually, but when I run unit tests I am getting a bunch of errors.
Here is the trait I've added to App\User and a few other models:
trait HasCompany
{
public static function bootHasCompany()
{
if (auth()->check()) {
static::creating(function ($model) {
$model->company_id = auth()->user()->company_id;
});
static::addGlobalScope('company_id', function (Builder $builder) {
$builder->where('company_id', auth()->user()->company_id);
});
}
}
public function company()
{
return $this->belongsTo('App\Company');
}
}
The purpose of this trait is to automatically add the logged in users company_id to any models they create, and restrict their access only to models they have created. I should mention that all App\User's have a company_id set in the database.
So as I've said, when I attempt to create a model when logged into my app everything works great. The trait works perfectly. However, unit tests don't seem to care for this trait. Here is an example test that does not work:
class RoleTest extends TestCase
{
use WithFaker;
public $user;
public function setup()
{
parent::setUp();
$this->user = App\User::create([
'company_id' => $faker->randomNumber(),
'name' => $this->faker->firstName,
'email' => $this->faker->email,
'password' => $this->faker->password,
]);
}
public function tearDown()
{
parent::tearDown();
$this->user->delete();
}
public function testAdd()
{
$response = $this->actingAs($this->user)->json('POST', 'roles/add', [
'_token' => csrf_token(),
'name' => $this->faker->word,
]);
$response->assertStatus(200)->assertJson(['flash' => true]);
}
}
I'm getting a 500 response instead of a 200 response because the model should automatically be obtaining the company_id from $this->user, but it is not. This is only happening for unit tests.
Here is the model code:
class Role extends Model
{
use HasCompany;
protected $fillable = ['company_id', 'name'];
}
Why aren't the unit tests booting the trait properly? It seems like actingAs doesn't work for authorization within traits, or is failing to boot it's traits entirely.

In your unit tests, the user model is first booted when the user is created by the setup function. At that time, no user is authenticated (as actingAs follows later). So, the auth()->check() only happens once when the user is created.
I think instead of checking if authenticated once (during boot), you should check if authenticated during the user creation.
Remove if(auth()->check()) from bootHasCompany and add it inside the Eloquent event closures like so:
static::creating(function ($model) {
if(auth()->check())
{
$model->company_id = auth()->user()->company_id;
}
});
static::addGlobalScope('company_id', function (Builder $builder) {
if(auth()->check())
{
$builder->where('company_id', auth()->user()->company_id);
}
});

Related

Why does my Laravel policy always return false?

The code for the policy is here:
class userOwnedClassPolicy
{
use HandlesAuthorization;
...
public function create(User $user)
{
return ($user->userType == 'teacher');
}
...
}
This policy is registered thusly in the AuthServiceProvider.php file:
class AuthServiceProvider extends ServiceProvider
{
//Map models to authorization policies.
protected $policies = [
App\Models\classMember::class => App\Policies\classMemberPolicy::class,
App\Models\evaluation::class => App\Policies\evaluationPolicy::class,
App\Models\group::class => App\Policies\groupPolicy::class,
App\Models\groupMember::class => App\Policies\groupMemberPolicy::class,
App\Models\sharedClass::class => App\Policies\sharedClassPolicy::class,
App\Models\slg::class => App\Policies\slgPolicy::class,
App\Models\spreadsheet::class => App\Policies\spreadsheetPolicy::class,
App\Models\spreadsheetValue::class => App\Policies\spreadsheetValuePolicy::class,
App\Models\teacher::class => App\Policies\teacherPolicy::class,
App\Models\test::class => App\Policies\testPolicy::class,
App\Models\userOwnedClass::class => App\Policies\userOwnedClassPolicy::class
];
public function boot()
{
$this->registerPolicies();
}
}
(I have tried registering the policies using strings of the file paths as well, but this accomplishes nothing.)
The relevant section of controller code is here:
class ClassController extends Controller
{
...
public function store(Request $postReq)
{
$this->authorize('create', Auth::user());
userOwnedClass::create([
'name' => $postReq->input('className'),
'ownerId' => Auth::user()->id
]);
}
...
}
I have tried substituting the code in the policy's create method with return true, but even that fails. What have I done wrong, and why does the controller always return a 403 error when called?
As you created policy userOwnedClassPolicy and set it for userOwnedClass model in AuthServiceProvider here:
App\Models\userOwnedClass::class => App\Policies\userOwnedClassPolicy::class
you cannot just run policy method:
$this->authorize('create', Auth::user());
When you run this line above, you tell - check create method for policy for \App\Models\User object, but you don't have any policy created for this model.
So in this case you should run it like so:
$this->authorize('create', \App\Models\userOwnedClass::class);
Then Laravel will know that it should run create method from userOwnedClassPolicy policy and it will automatically pass currently authenticated user into $user variable in policy method.

Laravel Unit testing Eloquent insertion

I'm currently having some troubles in testing a function in Laravel. This function is a simple save user function.
The current structure involves a User
class User extends Authenticatable
Then I have a UserController
class UserController extends Controller
{
protected $user;
public function __construct(User $user)
{
$this->user = $user;
$this->middleware('admins');
}
The save function is defined on the UserController class, this class only assigns the request variables and uses Eloquent save function to save to database.
The function signature is the following:
public function storeUser($request)
{
$this->user->name = $request->name;
$this->user->email = $request->email;
$this->user->country_id = $request->country_id;
return $this->user->save();
}
The NewAccountRequest object extends from Request and has the validation rules for the request.
class NewAccountRequest extends Request
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:user',
'password' => 'required|min:6|max:60',
];
}
}
My problem is how can I unit test this storeUser function.
I have the current test:
public function testSaveUserWithEmptyRequest()
{
$user = $this->createMock(User::class);
$controller = new UserController($user);
$request = $this->createMock(NewAccountRequest::class);
$store = $controller->storeUser($request);
$this->assertFalse($store);
}
I'm mocking both User and NewAccountRequest, the problem is that the assertion should be false, from the Eloquent save. Instead I'm getting Null. Any idea on how can I correctly test the function?
<?php
namespace Tests\Unit;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class ExampleTest extends TestCase
{
use DatabaseTransactions; // Laravel will automatically roll back changes that happens in every test
public function testSaveUserWithEmptyRequest()
{
$user = new User();
$controller = new UserController($user);
$request = $this->createMock(NewAccountRequest::class);
$store = $controller->storeUser($request);
$this->assertFalse($store);
}
}
This is exactly what you are trying to do, but unfortunately this will fail due to database exceptions...
Mocking a request or even manually crafting it will not do the data input validation.. and in your example password field is not nullable and will cause PDOException: SQLSTATE[HY000]: General error: 1364 Field 'password' doesn't have a default value
The recommended way to test functions depending on request, is to use http test helpers provided by laravel like $response = $this->post('/user', ['name' => 'Sally']);
A much better approach is to use the repository design pattern.. this simply means collate your database functions into separate classes and call it from controllers ..

Laravel 5.1 - How to get updates to relationships in static::updating event

I'm trying to create a trait (for models) that would automatically write all changes made to the model in 'adjustments' table. It would save changes in before and after jsons.
This is the code so far (from Laracasts):
trait LoggingTrait
{
public static function boot()
{
parent::boot();
static::updating(function($action){
$action->adjustments()->create(
[
'user_id' => Auth::id(),
'before' => json_encode(array_intersect_key($action->getOriginal(), $action->getDirty())),
'after' => json_encode($action->getDirty())
]);
});
}
public function adjustments()
{
return $this->morphMany(Adjustment::class, 'adjustable');
}
}
This is working very well, except it doesn't save changes to related models.
To make it more clear, this is my Action model:
class Action extends Model
{
use LoggingTrait;
public function actionTypes()
{
return $this->belongsToMany(ActionType::class);
}
public function users()
{
return $this->belongsToMany(User::class);
}
public function spreadingMaterial()
{
return $this->belongsTo(SpreadingMaterial::class);
}
}
The trait logs all the changes made to the actual Action model, but doesn't care for the changes made to the $action->users(), $action->spreadingMaterials() and $action->actionTypes(). How would I get these changes within the static::updating(...) event?
Or if that is not possible, any other idea on how to tackle this problem is more than welcome.

Adding Relations to Laravel Factory Model

I'm trying to add a relation to a factory model to do some database seeding as follows - note I'm trying to add 2 posts to each user
public function run()
{
factory(App\User::class, 50)->create()->each(function($u) {
$u->posts()->save(factory(App\Post::class, 2)->make());
});
}
But its throwing the following error
Argument 1 passed to Illuminate\Database\Eloquent\Relations\HasOneOrMany::s
ave() must be an instance of Illuminate\Database\Eloquent\Model, instance
of Illuminate\Database\Eloquent\Collection given
I think its something to do with saving a collection. If re-write the code by calling each factory model for the post separately it seems to work. Obviously this isn't very elegant because if I want to persist 10 or post to each user then I'm having to decalare 10 or lines unless I use some kind of for loop.
public function run()
{
factory(App\User::class, 50)->create()->each(function($u) {
$u->posts()->save(factory(App\Post::class)->make());
$u->posts()->save(factory(App\Post::class)->make());
});
}
* UPDATED *
Is there any way to nest the model factory a 3rd level deep?
public function run()
{
factory(App\User::class, 50)
->create()
->each(function($u) {
$u->posts()->saveMany(factory(App\Post::class, 2)
->make()
->each(function($p){
$p->comments()->save(factory(App\Comment::class)->make());
}));
});
}
Since Laravel 5.6 there is a callback functions afterCreating & afterMaking allowing you to add relations directly after creation/make:
$factory->afterCreating(App\User::class, function ($user, $faker) {
$user->saveMany(factory(App\Post::class, 10)->make());
});
$factory->afterMaking(App\Post::class, function ($post, $faker) {
$post->save(factory(App\Comment::class)->make());
});
Now
factory(App\User::class, 50)->create()
will give you 50 users with each having 10 posts and each post has one comment.
Try this. It worked for me:
factory(\App\Models\Auth\User::class, 300)->create()->each(function ($s) {
$s->spots()->saveMany(factory(\App\Models\Spots\Parking::class, 2)->create()->each(function ($u) {
$u->pricing()->save(factory(\App\Models\Payment\Pricing::class)->make());
}));
$s->info()->save(factory(\App\Models\User\Data::class)->make());
});
For a 3rd level nested relationship, if you want to create the data with the proper corresponding foreign keys, you can loop through all the results from creating the posts, like so:
factory(App\User::class, 50)->create()->each(function($u) {
$u->posts()
->saveMany( factory(App\Post::class, 2)->make() )
->each(function($p){
$p->comments()->save(factory(App\Comment::class)->make());
});
});
To answer the initial problem / error message:
The problem indeed has to do with saving the data. Your code:
$u->posts()->save(factory(App\Post::class, 2)->make());
... should be changed to
$u->posts()->saveMany(factory(App\Post::class, 2)->make());
From the laravel docs:
You may use the createMany method to create multiple related models:
$user->posts()->createMany(
factory(App\Post::class, 3)->make()->toArray()
);
That means:
when only creating one model with the factory, you should use save() or create()
when creating multiple models with the factory, you should use saveMany() or createMany()
$factory->define(User::class, function (Faker $faker) {
return [
'name' => $faker->name,
'email' => $faker->unique()->safeEmail,
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', //
password
'remember_token' => Str::random(10),
];
});
$factory->define(Post::class, function ($faker) use ($factory) {
return [
'title' => $faker->sentence(3),
'content' => $faker->paragraph(5),
'user_id' => User::pluck('id')[$faker->numberBetween(1,User::count()-1)]
];
});
Let me, explain How to add multi level of relationship of call factory in Laravel 9 By concept of new school and old school.
The new school is:
\App\Models\Author::factory()
->has(
\App\Models\Article::factory(1)
->has(
\App\Models\Comment::factory(9)
->has(
\App\Models\Reply::factory(2)
)
))->create();`enter code here`
That's for Laravel 9 . There's anthor way call Magic method. let me explain that:
\App\Models\Author::factory()->hasArticles(1)->hasComments(9)->hasReplies(2)->create();
this hasArticles() is the name of method of relationship in parent model should convert the name with has. for example: comments() convert to hasComments().
Now lets explain old school that's still prefect in some cases and still works good with Laravel 9.
\App\Models\Author::factory(1)->create()
->each(function($a) {
$a->articles()->saveMany( \App\Models\Article::factory(2)->create() )
->each(function($p){
$p->comments()->saveMany(\App\Models\Comment::factory(5))->create()
->each(function($r){
$r->replies()->saveMany(\App\Models\Reply::factory(5))->create();
});
});
});
of course you can replace method saveMany() by save() as your relationship you have.
also you can replace method create() by make() if you want to doesn't save in database for test purposes.
Enjoy.
In version laravel 9, use like this
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* #extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Post>
*/
class PostFactory extends Factory
{
/**
* Define the model's default state.
*
* #return array<string, mixed>
*/
public function definition()
{
return [
'user_id' => User::factory(),
'title' => fake()->paragraph()
];
}
}```

Should I re-use controllers in laravel between admin & api ? or have my admin consume my API?

New to laravel and trying to work out the best way to structure my app.
It has both an admin interface and an API (JSON, angularjs front-end).
my routes currently look like:
Route::group(array('prefix' => 'admin', 'before' => 'auth.admin'), function()
{
Route::any('/', array('as' => 'admin.index', function() {
return View::make('admin.index');
}));
Route::resource('countries.products', 'ProductsController');
Route::resource('countries', 'CountriesController');
Route::resource('orders', 'OrdersController');
});
// Route group for API versioning
Route::group(array('prefix' => 'api/v1'), function()
{
Route::resource('products', 'APIProductsController', array('only' => array('index', 'show')));
Route::resource('orders', 'APIOrdersController', array('only' => array('store', 'update')));
});
There is a lot of duplicated logic in eg, the OrdersController & APIOrdersController. Should I re-use a single controller somehow, maybe with content-negotation? or is it better to modify OrdersController to query the API routes instead of using eloquent?
or is there another, better way?
As I see it, I would extract all object creation logic to a proper class (sounds like a good case for a repository). This class should only know about the parameters it has to receive, and respond accordingly. For example:
class EloquentOrder implements OrderRepositoryInterface {
// Instance of OrderValidator,
// assuming we have one
protected $validator;
public function create($params)
{
// Pseudo-code
$this->validator = new Ordervalidator($params);
if ($this->validator->passes())
create and return new Order
else
return validator errors
}
}
Then, each of your modules can use this functionality inside its controllers.
In your API, you could have this:
class APIOrderController extends APIController {
protected $repository;
public function __construct(OrderRepositoryInterface $repository)
{
$this->repository = $repository;
}
public function create()
{
// Let's imagine you have an APIAuth class which
// authenticates via auth tokens:
if (APIAuth::check()) {
$params = Input::all();
return $this->repository->new($params);
}
return Response::json(['error' => 'You are not authorized to create orders'], 401);
}
}
While in your administration module, you could have:
class AdminOrderController extends AdminController {
protected $repository;
public function __construct(OrderRepositoryInterface $repository)
{
$this->repository = $repository;
}
public function create()
{
// Now, let's imagine Auth uses a different authentication
// method, and can check for specific permissions
if (Auth::check() && Auth::hasPermission('create.orders')) {
$params = Input::all();
return $this->repository->new($params);
}
return Redirect::home()->with('message', 'You are not authorized to create orders');
}
}
As you can see, this allows you to reuse your object creation logic in different contexts. In the example I've used different authentication methods and responses just to show flexibility, but this will really depend on your project requirements.

Categories