Laravel: Testing Policies with Spatie Permissions Package - php

I'm looking to find a way to test Policy methods in isolation. The policies work as intended when Feature testing (e.g. via http) but I am having a hard time trying to write tests for these when I just want to test the logic of the policy itself.
My code:
class ItemPolicy
{
...
public function delete(User $user, Item $item)
{
return $this->can('delete_item') && !$item->isDeletable();
}
...
}
class ItemPolicyTest extends TestCase
{
use RefreshDatabase;
private $user;
public function setUp() : void
{
parent::setUp();
$this->user = User::factory()->create();
Permission::create(['name' => 'delete_item', 'guard_name' => 'web']);
$role = Role::create(['name' => 'admin', 'guard_name' => 'web']);
$role->givePermissionTo('delete_item');
$this->user->assignRole(['admin']);
$this->user = $this->user->fresh(); //just to make sure
$this->app->make(\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();
}
/** #test */
public function it_will_not_allow_deletion()
{
$item = Item::factory()->cannotBeDeleted()->create();
$this->be($this->user, 'web');
$this->assertFalse(auth()->user()->can('delete', $item));
}
}
Setting a breakpoint in my Policy doesn't get triggered so I assume that Spatie has taken this over somewhere. So ultimately, I'm looking for a way to recreate this so I can test these in isolation. Any help appreciated!

Related

Laravel get logged in user in Observer when event is triggered from queued job

I'm not sure if this is possible or not, but if it is it will save me lots of duplicate code. I have many queued jobs that handle importing csv data. I need to track the changes that have been made in these imports and who made the changes. I implemented observers for each model and I can log the changes but I can't get the user that imported the data since the changes take place in a queued job. The job itself is aware of the user since I am passing the user to the job, but I can't figure out how to pass the user to the observer. Does anyone have any ideas on how to do this?
I'm using the Laravel Excel package to handle the imports, and this package also integrates queuing the jobs. Here's the code from the import job:
class QueuedImport implements ToCollection, WithChunkReading, ShouldQueue
{
use Importable;
private $user;
private $client;
public function __construct($user, $client)
{
$this->user = $user; // this is the logged in user
$this->client = $client;
}
public function collection(Collection $rows)
{
//each row updates a model, which triggers the update event in the observer
$this->importRows($rows, $this->client, $this->user);
}
public function chunkSize(): int
{
return 500;
}
//....
}
And here's the observer's update event:
public function updated(MyModel $model)
{
$original = $model->getOriginal();
$user = auth()->user();
foreach ($model->getChanges() as $key => $value) {
\App\History::create([
'user_id' => $user->id ?? null,
'model_id' => $model->id,
'field' => $key,
'old_value' => $original[$key],
'new_value' => $value
]);
}
}
When I update the model and it's not in a queued job, the user saves fine. I understand that queued jobs are not aware of the authenticated user, but there must be some way to get the user into the observer.
I realize I could just track the change in the Job, but I have many places this model could be updated, and I don't want to worry about finding all the different spots it may be updated. Can this be done, and how?
I am not super happy with this solution, but maybe this is gonna work.
You might want to implement the laravel excel events with the Auth facade.
use Auth;
use Maatwebsite\Excel\Concerns\WithEvents;
class QueuedImport implements ToCollection, WithChunkReading, ShouldQueue, withEvents
{
use Importable;
private $user;
private $client;
public function __construct($user, $client)
{
$this->user = $user; // this is the logged in user
$this->client = $client;
}
public function collection(Collection $rows)
{
//each row updates a model, which triggers the update event in the observer
$this->importRows($rows, $this->client, $this->user);
}
public function chunkSize(): int
{
return 500;
}
public function beforeImport()
{
Auth::loginUsingId($this->user->id);
}
public function afterImport()
{
Auth::logout();
}
//....
}
The best I could come up with was not ideal, but I couldn't think of another way to do it. I ended up adding a column to each of the tables I am observing, updated_by. So when the model is updated, I populate that field with the currently logged in user. Then I have access to that field in the observer through the model. So this is what I have now in the observer:
public function updated(MyModel $model)
{
$original = $model->getOriginal();
$user = $model->created_by; // <--- this is the new field I added
foreach ($model->getChanges() as $key => $value) {
\App\History::create([
'user_id' => $user,
'model_id' => $model->id,
'field' => $key,
'old_value' => $original[$key],
'new_value' => $value
]);
}
}

*SOLVED* PHPUnit in Laravel returns 404 for simple get request, yet works fine for others

In a right pickle with phpunit. I currently have a test class for a resource route with 9 tests in it. All but two of these tests pass, ironically what should be the two simplest; the tests for articles.index and articles.show (the last 2 tests in the code below).
<?php
namespace Tests\Feature;
use App\Article;
use Tests\TestCase;
use App\User;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use DB;
class ArticleTest extends TestCase
{
use RefreshDatabase;
// runs before any of the tests
protected function setUp(): void
{
parent::setUp();
// run tests without language(locale) middleware
$this->withoutMiddleware(\App\Http\Middleware\Language::class);
}
/** #test */
public function unauthenticated_users_can_not_access_create()
{
$this->get('/articles/create')->assertRedirect('/login');
}
/** #test */
public function admin_users_can_access_edit()
{
$article = factory(Article::class)->create();
$record = DB::table('articles')->where('id', $article->id)->first();
$user = factory(User::class)->create();
$user->isAdmin = 1;
$this->actingAs($user);
$this->get('/articles/' . $record->slug . '/edit?locale=en')->assertOK();
}
/** #test */
public function authenticated_but_not_admin_users_can_not_access_create()
{
$this->actingAs(factory(User::class)->create());
$this->get('/articles/create')->assertRedirect('home');
}
/** #test */
public function admin_can_access_create()
{
$user = factory(User::class)->create();
$user->isAdmin = 1;
$this->actingAs($user);
$this->get('/articles/create')->assertOk();
}
/** #test */
public function can_store_an_article()
{
$article = factory(Article::class)->create();
$record = DB::table('articles')->where('id', $article->id)->first();
$this->assertDatabaseHas('articles', ['slug' => $record->slug]);
}
/** #test */
public function admin_can_access_articles_admin_index()
{
$user = factory(User::class)->create();
$user->isAdmin = 1;
$this->actingAs($user);
$this->get('/admin/articles')->assertOk();
}
/** #test */
public function admin_can_delete_article_and_it_is_removed_from_the_database()
{
$user = factory(User::class)->create();
$user->isAdmin = 1;
$this->actingAs($user);
$article = factory(Article::class)->create();
$this->assertDatabaseHas('articles', ['slug' => $article->slug]);
$record = DB::table('articles')->where('id', $article->id)->delete();
$this->assertDatabaseMissing('articles', ['slug' => $article->slug]);
}
/** #test */
public function can_see_article_index_page()
{
$this->get('/articles')->assertOK();
}
/** #test */
public function can_only_access_articles_made_visible()
{
$article = factory(Article::class)->create();
$article->displayStatus = 2;
$this->assertDatabaseHas('articles', ['slug' => $article->slug]);
$this->get('/articles/' . $article->slug)->assertRedirect('/articles');
}
}
The test can_see_article_index_page should return a 200 status code yet it 404's and can_only_access_articles_made_visible should be a redirect status code yet 404's as well.
SEEN HERE
My application is multi-lingual, using the spatie/translatable package and I'm unsure if it's that that is interfering (doesn't really make sense for it to be as the withoutMiddleware line in setUp should prevent this) or something else entirely (i.e my stupidity). My app is built with Laravel 5.8, and I am running phpunit 7.5.
EDIT I found out the error was due to some specific rows not existing in my test database, where in my controller it would fail if it couldn't find them. By adding the line $this->withoutExceptionHandling() to the failing tests it told me this information.
Since all your routes are working but /articles that means that something is not ok with your routes.
You added this route lately: Every time you create a new route or make any changes in the route files of laravel you should run php artisan config:cache command.
I am pretty sure it's not a conflict in routes because of a variable {}, since the status code is a clear 404 meaning that no controller is even accessed that's why i won't elaborate further on possible routing conflicting issues.
If the error persists though get your route at the very top of your routes file and see if it works there. If that's the case that means that one of your routes, overlaps it.
A quick fix to that is to order your static naming routes above the routes using variables for example:
/articles
/{articles}
If you reverse above order your /articles route will never be accessed. This is a case of naming route conflicts.

Save variable during unit testing Laravel

Variable $this->id not visible in another function testExemple
If I pass this variable to a normal function that does not start on a “test” and not to a testing function, everything will work.
Can I fix this somehow?
class LoginTest extends TestCase
{
protected $id;
public function testLogin()
{
$response = $this->json('post', 'auth/login',
['email' => 'admin#mail.com', 'password' => '12345678'])
->assertJsonStructure(['data' => ['id', 'name', 'email']]);
$response->assertStatus(201);
$userData = $response->getContent();
$userData = json_decode($userData, true);
$this->id = $userData['data']['id'];
}
public function testExemple()
{
echo($this->id);
}
}
Each test runs independently as far as I know, if you want to pass data from one test to another you can use the #depends doc comment like below:
class LoginTest extends TestCase
{
public function testLogin()
{
$response = $this->json('post', 'auth/login',
['email' => 'admin#mail.com', 'password' => '12345678'])
->assertJsonStructure(['data' => ['id', 'name', 'email']]);
$response->assertStatus(201);
$userData = $response->getContent();
$userData = json_decode($userData, true);
return $userData['data']['id']; //Return this for the dependent tests
}
/**
* #depends testLogin
*/
public function testExample($id)
{
echo($id);
}
}
However the problem you might encounter is that while the $id has a value the user is not actually logged in during this test because everything else (e.g. session) will be wiped clean.
To ensure the user is logged in then you will need to mock user login like below:
public function testExample()
{
$this->actingAs(User::where('email', 'admin#mail.com')->first()); //User now logged in
echo(\Auth::id());
}
This ensures the user is logged in and also decouples tests.
It works like this because unit tests should be indepedent. A variable set by one test should never be accessible for the next.
If your issue is that you need to test things that requires you to be logged in, a good solution is creating a new class that extends TestCase and implementing helper functions such as loginUser() (which could return a User instance).
You should then have your tests extend this new class instead of directly extending TestCase.
Every time you run a test that requires you to log in, you can just write $this->loginUser() and go ahead with your real test.
If all tests in a class requires you to log in, you can even add a setUp() function that will run right before any test is executed (remember to also call parrent::setUp():
protected function setUp() {
parent::setUp();
$this->loginUser();
}
Lets suppose you want to pass $number from test1 to test2 :
class Test extends TestCase
{
public function test1()
{
$number = 1;
// store the variable, only available during testsuite execution,
// it does not necessarily have to exist in your .env file .
putenv('MY_NUMBER=' . $number);
//success
$this->assertEquals(getenv('MY_NUMBER'), $number);
}
public function test2()
{
//hurray ! variable retrieved
$this->assertEquals(1, getenv('MY_NUMBER'));
// after tearing down , MY_NUMBER is cleared
}
}
The solution may not be the best, but at least it works. And hey, we are doing testing, not writting production code, so who cares?

PHP: Mockery Mock variable $user = Auth::user()

So, I am trying to mock a service method.
In my service file:
/**
* Return all Api Keys for current user.
*
* #return Collection
*/
public function getApiKeys(): Collection
{
$user = Auth::user();
return ApiKey::where('org_id', $user->organizationId)->get();
}
How do I mock this?
<?php
namespace App\Services;
use PHPUnit\Framework\TestCase;
use Mockery as m;
class ApiKeysServiceTest extends TestCase
{
public function setUp()
{
parent::setUp();
/* Mock Dependencies */
}
public function tearDown()
{
m::close();
}
public function testGetApiKeys()
{
/* How to test? $user = Auth::user() */
$apiKeysService->getApiKeys();
}
}
In my TestCase class I have:
public function loginWithFakeUser()
{
$user = new GenericUser([
'id' => 1,
'organizationId' => '1234'
]);
$this->be($user);
}
What I want to do is test this method. Maybe this involves restructuring my code so that $user = Auth::user() is not called in the method. If this is the case, any thoughts as to where it should go?
Thanks for your feedback.
In your testGetApiKeys method you're not setting up the world. Make a mock user (using a factory as suggested in the comments factory('App\User')->create()), then setup an apiKey again using the factory, then call the method and assert it's what you've setup. An example with your code
public function loginWithFakeUser()
{
$user = factory('App\User')->create();
$this->be($user);
}
public function testApiSomething()
{
$this->loginWithFakeUser();
// do something to invoke the api...
// assert results
}
A good blueprint for the test structure is:
Given we have something (setup all the needed components)
If the user does some action (visits a page or whatever)
Then ensure the result of the action is what you expect (for example the status is 200)

Transactions doesn't work on some tests

I'm working with Laravel 5.2 and phpunit and i'm writing some test for my application. until now i got no problem, and today i encounter something weird and I can't find a way to handle it.
Some of my test file doesn't use transactions although the others does.
i use use DatabaseTransactions; in my TestCase class with is extended in every test file i got.
Most of my test works without any troubles but some of them does not.
Here is one which works withotut any troubles :
class V2LikeTest extends TestCase {
protected $appToken;
protected $headers;
public function setUp() {
parent::setUp();
$this->generateTopic(1);
}
/** LIKE TOPICS */
/** #test */
public function it_likes_a_topic() {
$job = new ToggleLikeJob(Topic::first());
$job->handle();
$topic = Topic::first();
$this->assertTrue($topic->present()->isLiked);
$this->assertEquals(1, $topic->nb_likes);
}
}
and this one with troubles:
class V2TopicTest extends TestCase {
private $appToken;
private $headers;
public function setUp() {
parent::setUp();
$this->generateCompany(1);
}
/** #test */
public function it_create_a_topic() {
$new_topic_request = new Request([
'content' => str_random(100),
'type' => Topic::TYPE_FEED_TEXT
]);
$job = new CreateFeedTopicJob($new_topic_request->all());
$job->handle();
$this->assertCount(1, Topic::all());
}
}
It's been a while now that i'm looking for the solution but not able to find it. Did someone already meet this troubles?
edit: GenerateTopic function use generateCompany just in case ;)

Categories