Laravel test not passing - laravel

So I'm learning doing tests for my application and one of the tests it doesn't want to pass, and here it is the logic: Basically, when a user request the home page, I expect that the database list count would be 0, and this passed, then I expect also that the session has an error key of NoBook and here it fails. this is the code that i have tried:
class BookDisplayManagmentTest extends TestCase
{
use RefreshDatabase;
/** #test */
public function Show_error_message_when_there_is_no_book_to_display_in_index_page()
{
//Request the home page
$response = $this->get(route('home'));
// I expect the count on the database book equal 0
$this->assertCount(0, book::all());
//Then I also expect that the session will flash an error with key NoBook
$response->assertSessionHasErrors('NoBook');
}
}
But the problem I'm getting this error:
Session is missing expected key [errors]. Failed asserting that false is true.
And the code that add the session error:
<?php
namespace App\Http\Controllers;
use App\Books;
use Illuminate\Http\Request;
class IndexController extends Controller
{
/** #show index function */
public function index()
{
$book = Books::paginate(7);
if(!$book->count())
{
session()->now('NoBook','There is no books at the moment');
}
return view('index', compact('book'));
}
}

You are using session() which is adding a key to the session which is not an error key.
Therefore, since you are not passing an error from your Controller - then your test is "successfully" failing.
If you want to pass on an error to the session, you have to use MessageBag such as using the following code:
/** #show index function */
public function index()
{
$book = Books::paginate(7);
$errors = [];
if(!$book->count())
{
$errors['NoBook'] = 'There is no books at the moment';
}
return view('index', compact('book'))->withErrors($errors);
}

Related

Laravel authorization policy not working on Show page

I have a laravel app using Policies to assign roles and permissions, i cant seem to access the show page and im not sure what im doing wrong?
If i set return true it still shows a 403 error as well, so im unsure where im going wrong here. The index page is accessable but the show page is not?
UserPolicy
public function viewAny(User $user)
{
if ($user->isSuperAdmin() || $user->hasPermissionTo(44, 'web')) {
return true;
}
return false;
}
public function view(User $user, User $model)
{
if ($user->isSuperAdmin() || $user->hasPermissionTo(44, 'web')) {
return true;
}
return false;
}
UserController
public function __construct()
{
$this->authorizeResource(User::class, 'user');
}
public function index()
{
$page_title = 'Users';
$page_description = 'User Profiles';
$users = User::all();
return view('pages.users.users.index', compact('page_title', 'page_description', 'users'));
}
public function create()
{
//
}
public function store(Request $request)
{
//
}
public function show($id)
{
$user = User::findOrFail($id);
$user_roles = $user->getRoleNames()->toArray();
return view('pages.users.users.show', compact('user', 'user_roles'));
}
Base on Authorize Resource and Resource Controller documentation.
You should run php artisan make:policy UserPolicy --model=User. This allows the policy to navigate within the model.
When you use the authorizeResource() function you should implement your condition in the middleware like:
// For Index
Route::get('/users', [UserController::class, 'index'])->middleware('can:viewAny,user');
// For View
Route::get('/users/{user}', [UserController::class, 'view'])->middleware('can:view,user');
or you can also use one policy for both view and index on your controller.
I had an issue with authorizeResource function.
I stuck on failed auth policy error:
This action is unauthorized.
The problem was that I named controller resource/request param with different name than its model class name.
F. ex. my model class name is Acknowledge , but I named param as timelineAcknowledge
Laravel writes in its documentation that
The authorizeResource method accepts the model's class name as its first argument, and the name of the route / request parameter that will contain the model's ID as its second argument
So the second argument had to be request parameter name.
// Here request param name is timelineAcknowledge
public function show(Acknowledge $timelineAcknowledge)
{
return $timelineAcknowledge->toArray();
}
// So I used this naming here also
public function __construct()
{
$this->authorizeResource(Acknowledge::class, 'timelineAcknowledge');
}
Solution was to name request param to the same name as its model class name.
Fixed code example
// I changed param name to the same as its model name
public function show(Acknowledge $acknowledge)
{
return $acknowledge->toArray();
}
// Changed here also
public function __construct()
{
$this->authorizeResource(Acknowledge::class, 'acknowledge');
}
I looked over Laravel policy auth code and I saw that the code actually expects the name to be as the model class name, but I couldn't find it anywhere mentioned in Laravel docs.
Of course in most of the cases request param name is the same as model class name, but I had a different case.
Hope it might help for someone.

Mocking class instantiation in a feature test

I'm currently working on my first Laravel project — a service endpoint that returns a resource based on a recording saved in S3. The service doesn't require a DB, but my idea was, I could keep the controller skinny, by moving the logic to a "model". I could then access the resource by mimic'ing some standard active record calls.
Functionally, the implementation works as expected, but I am having issues with mocking.
I am using a library to create signed CloudFront URLs, but it is accessed as a static method. When I first started writing my feature test, I found that I was unable to stub the static method. I tried class aliasing with Mockery, but with no luck — I was still hitting the static method. So, I tried wrapping the static method in a little class assuming mocking the class would be easier. Unfortunately, I'm experiencing the same issue. The thing that I am trying to mock is being hit as if I'm not mocking it.
This stack overflow post gives an example of how to use class aliasing, but I can't get it to work.
What is the difference between overload and alias in Mockery?
What am I doing wrong? I'd prefer to get mockery aliasing to work, but instance mocking would be fine. Please point me in the right direction.
 Thank you in advance for your help.
Controller
// app/Http/Controllers/API/V1/RecordingController.php
class RecordingController extends Controller {
public function show($id){
return json_encode(Recording::findOrFail($id));
}
}
Model
// app/Models/Recording.php
namespace App\Models;
use Mockery;
use Carbon\Carbon;
use CloudFrontUrlSigner;
use Storage;
use Illuminate\Support\Arr;
class Recording
{
public $id;
public $url;
private function __construct($array)
{
$this->id = $array['id'];
$this->url = $this->signedURL($array['filename']);
}
// imitates the behavior of the findOrFail function
public static function findOrFail($id): Recording
{
$filename = self::filenameFromId($id);
if (!Storage::disk('s3')->exists($filename)) {
abort(404, "Recording not found with id $id");
}
$array = [
'id' => $id,
'filename' => $filename,
];
return new self($array);
}
// imitate the behavior of the find function
public static function find($id): ?Recording
{
$filename = self::filenameFromId($id);
if (!Storage::disk('s3')->exists($filename)){
return null;
}
$array = [
'id' => $id,
'filename' => $filename,
];
return new self($array);
}
protected function signedURL($key) : string
{
$url = Storage::url($key);
$signedUrl = new cloudFrontSignedURL($url);
return $signedUrl->getUrl($url);
}
}
/**
* wrapper for static method for testing purposes
*/
class cloudFrontSignedURL {
protected $url;
public function __construct($url) {
$this->url = CloudFrontUrlSigner::sign($url);
}
public function getUrl($url) {
return $this->url;
}
}
Test
// tests/Feature/RecordingsTest.php
namespace Tests\Feature;
use Mockery;
use Faker;
use Tests\TestCase;
use Illuminate\Http\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Foundation\Testing\WithFaker;
/* The following is what my test looked like when I wrapped CloudFrontUrlSigner
* in a class and attempted to mock the class
*/
class RecordingsTest extends TestCase
{
/** #test */
public function if_a_recording_exists_with_provided_id_it_will_return_a_URL()
{
$recordingMock = \Mockery::mock(Recording::class);
$faker = Faker\Factory::create();
$id = $faker->numberBetween($min = 1000, $max = 9999);
$filename = "$id.mp3";
$path = '/api/v1/recordings/';
$returnValue = 'abc.1234.com';
$urlMock
->shouldReceive('getURL')
->once()
->andReturn($returnValue);
$this->app->instance(Recording::class, $urlMock);
Storage::fake('s3');
Storage::disk('s3')->put($filename, 'this is an mp3');
Storage::disk('s3')->exists($filename);
$response = $this->call('GET', "$path$id");
$response->assertStatus(200);
}
}
// The following is what my test looked like when I was trying to alias CloudFrontUrlSigner
{
/** #test */
public function if_a_recording_exists_with_provided_id_it_will_return_a_URL1()
{
$urlMock = \Mockery::mock('alias:Dreamonkey\cloudFrontSignedURL');
$faker = Faker\Factory::create();
$id = $faker->numberBetween($min = 1000, $max = 9999);
$filename = "$id.mp3";
$path = '/api/v1/recordings/';
$returnValue = 'abc.1234.com';
$urlMock
->shouldReceive('sign')
->once()
->andReturn($returnValue);
$this->app->instance('Dreamonkey\cloudFrontSignedURL', $urlMock);
Storage::fake('s3');
Storage::disk('s3')->put($filename, 'this is an mp3');
Storage::disk('s3')->exists($filename);
$response = $this->call('GET', "$path$id");
$response->assertStatus(200);
}
}
phpunit
$ phpunit tests/Feature/RecordingsTest.php --verbose
...
There was 1 failure:
1) Tests\Feature\RecordingsTest::if_a_recording_exists_with_provided_id_it_will_return_a_URL
Expected status code 200 but received 500.
Failed asserting that false is true.
/Users/stevereilly/Projects/media-service/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:133
/Users/stevereilly/Projects/media-service/tests/Feature/RecordingsTest.php:85
/Users/stevereilly/.composer/vendor/phpunit/phpunit/src/TextUI/Command.php:206
/Users/stevereilly/.composer/vendor/phpunit/phpunit/src/TextUI/Command.php:162
You're getting a 500, which means there's something wrong with the code. Just by scanning it I notice you're missing a filenameFromId method on the Recordings class, and the Test is creating a mock named $recordingMock, but you try to use $urlMock. Try to fix those issues first.
Then you're mocking the class, but you never replace it in your application (you did it in the old test apparently).
Generally you want to follow these steps when mocking:
1. Mock a class
2. Tell Laravel to replace the class with your mock whenever someone requests it
3. Make some assertions against the mock

How to test custom validation rule in Laravel 5.8

i would like to write a test for my custom rules.
my example looks like this.
I'm check if the user gives the correct current password.
my custom rule:
public function passes($attribute, $value)
{
return Hash::check($value, auth()->user()->password);
}
public function message()
{
return 'Your current password is incorrect.';
}
and the test for this rule:
class CurrentPasswordTest extends TestCase
{
use WithFaker, RefreshDatabase;
/** #test */
public function current_password_must_be_valid()
{
$rule = new CurrentPassword();
$user = factory(User::class)->create(['password' => '1234']);
$this->assertTrue($rule->passes('current_password','1234'), $user->password);
}
}
but i'm getting an error:
Tests\Unit\CurrentPasswordTest::current_password_must_be_valid
ErrorException: Trying to get property 'password' of non-object
what i'm doing wrong in this example ?
You need to log in your user before you can use your passes() method - otherwise auth()->user() will be null. You can do that using the be($user) method like this:
/** #test */
public function current_password_must_be_valid()
{
$rule = new CurrentPassword();
$user = factory(User::class)->create(['password' => '1234']);
$this->be($user);
$this->assertTrue($rule->passes('current_password','1234'), $user->password);
}
It would also be advisable to guard against null values in your passes() method to prevent errors. If there is no user logged in, it should probably just return false.

How to write unit test cases for session variables in laravel 5.4

Hello I have a test case which will call a route and it will return some data if the session will set.
Here is my test case
class TestControllerTest extends TestCase
{
// ...
public function testResponseOfJson()
{
$response = $this->call('GET', 'profile/test');
$this->assertEmpty( !$response );
}
// ...
}
and here is my controller
Class TestController{
public function sendResponse(Request $request){
$this->data['user_id'] = $this->request->session()->get('userdata.userid');
if($this->data['userid']){
return data;
}
else{
return Failed;
}
}
}
My routes.php
Route::get('profile/test',['uses'=>'TestController#sendResponse']);
how can i set the session variable userdata.userid and get while doing unit testing.
Please check this page.
Laravel gives you the capability of using withSession chain method and other functions that will help you test where a session is required or needs to be manipulated in some way.
Example:
<?php
class ExampleTest extends TestCase
{
public function testApplication()
{
$response = $this->withSession(['foo' => 'bar'])
->get('/');
}
}

Event fails when using serialization in laravel 5.4

I have different models, which describe different tables in database.
I use this trait for these classes
trait ApplicationModelTrait
{
protected static $_currentModel = null;
protected $currentModel;
public static function setCurrentModel(ApplicationModel $model) {
static::$_currentModel = $model;
}
public function __construct(array $attributes = [], ApplicationModel $model = null) {
parent::__construct($attributes);
$this->currentModel = $model !== null ? $model : static::$_currentModel;
if($this->currentModel === null) throw new \InvalidArgumentException('No model passed');
}
}
All models are similar. The only difference is what fields exist in database for every model, so I describe these fields in separate config and main model class ApplicationModel has some methods to work with different tables. For example
public function getInstanceTable() {
return 'application_model_instances_' . $this->name;
}
public function getCommentsTable() {
return 'application_model_instance_' . $this->name . '_comments';
}
Where application_model_instances_{name} contains (obviously) instances of this model and application_model_instance_{name}_comments contains comments for instances of this model.
Everything works fine except events.
When I add comment to model instance, I pass as argument current model
$comment = new ApplicationModelInstanceComment([], $this->currentModel);
$comment->text = $request->input('comment');
// etc.
After comment has been saved I want it to be instantly delivered to user browser via websocket
event(new CommentCreated($comment, $this)); // this represents model instance class
And, finally, the event
namespace App\Events;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use App\Models\Application\ApplicationModelInstanceComment;
use App\Models\Application\ApplicationModelInstance;
class CommentCreated implements ShouldBroadcast
{
use InteractsWithSockets, SerializesModels;
public $comment;
public $instance;
public function __construct(ApplicationModelInstanceComment $comment, ApplicationModelInstance $instance)
{
$this->comment = $comment;
$this->instance = $instance;
}
public function broadcastOn() {
$notifiableUsers = $this->instance->getNotifiableUsers(); // this method is used to fetch list of users who must get this comment in their browser
$channels = [];
foreach($notifiableUsers as $user) {
$channels[] = new PrivateChannel('user.' . $user->id);
}
return $channels;
}
}
But when I add comment Laravel my code throws an exception (this is what I see in network tab in Google Chrome because request is done via ajax:
Whoops, looks like something went wrong.
1/1 InvalidArgumentException in ApplicationModelTrait.php line 20: No model passed
in ApplicationModelTrait.php line 20
at ApplicationModelInstanceComment->__construct() in SerializesAndRestoresModelIdentifiers.php line 45
at CommentCreated->getRestoredPropertyValue(object(ModelIdentifier)) in SerializesModels.php line 41
at CommentCreated->__wakeup()
at unserialize('O:38:"Illuminate\\Broadcasting\\BroadcastEvent":4:{s:5:"event";O:25:"App\\Events\\CommentCreated":3:{s:7:"comment";O:45:"Illuminate\\Contracts\\Database\\ModelIdentifier":2:{s:5:"class";s:54:"App\\Models\\Application\\ApplicationModelInstanceComment";s:2:"id";i:68;}s:8:"instance";O:45:"Illuminate\\Contracts\\Database\\ModelIdentifier":2:{s:5:"class";s:47:"App\\Models\\Application\\ApplicationModelInstance";s:2:"id";i:11;}s:6:"socket";N;}s:10:"connection";N;s:5:"queue";N;s:5:"delay";N;}') in CallQueuedHandler.php line 95
at CallQueuedHandler->failed(array('commandName' => 'Illuminate\\Broadcasting\\BroadcastEvent', 'command' => 'O:38:"Illuminate\\Broadcasting\\BroadcastEvent":4:{s:5:"event";O:25:"App\\Events\\CommentCreated":3:{s:7:"comment";O:45:"Illuminate\\Contracts\\Database\\ModelIdentifier":2:{s:5:"class";s:54:"App\\Models\\Application\\ApplicationModelInstanceComment";s:2:"id";i:68;}s:8:"instance";O:45:"Illuminate\\Contracts\\Database\\ModelIdentifier":2:{s:5:"class";s:47:"App\\Models\\Application\\ApplicationModelInstance";s:2:"id";i:11;}s:6:"socket";N;}s:10:"connection";N;s:5:"queue";N;s:5:"delay";N;}'), object(InvalidArgumentException)) in Job.php line 158
at Job->failed(object(InvalidArgumentException)) in FailingJob.php line 33
I've passed model into ApplicationModelInstanceComment upon creation of comment, so everything is fine here. There is some problem with deserialization, but I do not know how to deal with this problem.

Resources