Factory relationship returning null laravel testing - php

I'm trying to unit testing a service that handles the registration of a user in Laravel.
This is the service:
public function completeRegistration(Collection $data)
{
$code = $data->get('code');
$registerToken = $this->fetchRegisterToken($code);
DB::beginTransaction();
$registerToken->update(['used_at' => Carbon::now()]);
$user = $this->userRepository->update($data, $registerToken->user);
$token = $user->createToken(self::DEFAULT_TOKEN_NAME);
DB::commit();
return [
'user' => $user,
'token' => $token->plainTextToken,
];
}
Where the update method has the following signature:
<?php
namespace App\Repositories\User;
use App\Models\User;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
interface UserRepositoryInterface
{
public function create(Collection $data): User;
public function update(Collection $data, User $user): User;
}
With my test being:
/**
* Test a user can register
*
* #return void
*/
public function test_user_can_complete_registration()
{
$userRepositoryMock = Mockery::mock(UserRepositoryInterface::class);
$registerTokenRepositoryMock = Mockery::mock(RegisterTokenRepository::class);
$userFactory = User::factory()->make();
$registerTokenFactory = RegisterToken::factory()
->for($userFactory)
->timestamped()
->make(['user_id' => $userFactory->id]);
dd($registerTokenFactory->user);
$userRepositoryMock
->expects($this->any())
->once()
->andReturn($userFactory);
....
}
When I run phpunit --filter=test_user_can_complete_registration I get the following error:
1) Tests\Unit\Services\Auth\AuthServiceTest::test_user_can_complete_registration
TypeError: Argument 2 passed to Mockery_0_App_Repositories_User_UserRepositoryInterface::update() must be an instance of App\Models\User, null given, called in /var/www/app/Services/Auth/AuthService.php on line 64
/var/www/app/Services/Auth/AuthService.php:64
/var/www/tests/Unit/Services/Auth/AuthServiceTest.php:88
This tells me that the user relationship on $registerTokenFactory is null. When I do:
public function test_user_can_complete_registration()
{
...
dd($registerTokenFactory->user);
}
I get the output null. I'm trying to test the service without hitting the database. How can I attach the user relationship to the $registerTokenFactory object? I have tried using for and trying to attach directly:
$registerTokenFactory = RegisterToken::factory()
->for($userFactory)
->timestamped()
->make(['user_id' => $userFactory->id, 'user' => $userFactory]);

In Laravel factories make() does only create the model and does not save it. For relationship to work, you will need your models to be saved.
$userFactory = User::factory()->create();
Since you do not want to use a Database, which is wrong in my opinion. People don't like writing tests, so when we have to do it make it simple, mocking everything to avoid databases is a pain. Instead an alternative is to you Sqlite to run in memory, fast and easy. A drawback is some functionality does not work there JSON fields and the version that are in most Ubuntu distributions does not respect foreign keys.
If you want to follow the path you are already on, assigned the user on the object would work, you have some left out bits of the code i assume.
$userRepositoryMock = Mockery::mock(UserRepositoryInterface::class);
$registerTokenRepositoryMock = Mockery::mock(RegisterTokenRepository::class);
$user = User::factory()->make();
$registerToken = RegisterToken::factory()
->for($userFactory)
->timestamped()
->make(['user_id' => $user->id]);
$registerToken->user = $user;
$registerTokenRepositoryMock
->expects('fetchRegisterToken')
->once()
->andReturn($registerToken);
$userRepositoryMock
->expects($this->any())
->once()
->andReturn($user);
// execute the call

Related

Call a Controller method from a Command in Laravel

I have a Command that is listening via Redis Pub/Sub. When a Publish is received, I want to call a controller method so that I can update the database.
However, I have not been able to find any solution on how to call a controller method with parameters from inside of the project but outside of the routes. The closest thing I have seen is something like:
return redirect()->action(
'TransactionController#assignUser', [
'transId' => $trans_id,
'userId' => $user_id
]);
My complete command that I've tried looks like this:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class RedisSubscribe extends Command
{
protected $signature = 'redis:subscribe';
protected $description = 'Subscribe to a Redis channel';
public function handle()
{
Redis::subscribe('accepted-requests', function ($request) {
$trans_array = json_decode($request);
$trans_id = $trans_array->trans_id;
$user_id = $trans_array->user_id;
$this->assignUser($trans_id, $user_id);
});
}
public function assignUser($trans_id, $user_id)
{
return redirect()->action(
'TransactionController#assignUser', [
'transId' => $trans_id,
'userId' => $user_id
]);
}
}
However, this does not seem to work. When I run this Command, I get an error that assignUser() cannot be found (even though it exists and is expecting two paramters). I am also not sure a "redirect" is really what I am after here.
Is there some other way to call a controller function in a Command, or some other way that would make this possible to do?
If your controller does not have any required parameters, you can just create the controller as a new object, and call the function.
$controller = new TransactionController();
$controller->assignUser([
'transId' => $trans_id,
'userId' => $user_id
]);

How to separate controller method in Laravel?

I start with Laravel, I write API. I have a method in TestController that checks if the student has correctly inserted data and has access to the exam solution. I do not think it's a good idea to have the whole method in the controller, but I have no idea how to separate it. I think about politics, but I have to have several models for one policy, maybe I can try to put part of the method on AuthorizeStudentRequest or try it in a different way? Of course, now I am returning 200 with the message, but I have to return 422 or another code with errors, but I have not done it because of my problem.
public function authorizeStudent(AuthorizeStudentRequest $request)
{
$hash = $request->input('hash');
$token = $request->input('token');
$exam = Exam::where([['hash', $hash], ['token', $token]])->first();
if($exam == null)
return ['message' => 'Exam does not exist.'];
$user = $exam->user_id;
$studentFirstname = $request->input('firstname');
$studentLastname = $request->input('lastname');
$student = Student::where([
['firstname', $studentFirstname],
['lastname', $studentLastname],
['user_id', $user]
])->first();
if($student == null)
return ['message' => 'Student does not exist.'];
$classroom = Classroom::where([
['name', $classroomName],
['user_id', $user]
])->first();
if($classroom == null)
return ['message' => 'Classroom does not exist.'];
if($student->classroom_id != $classroom->id)
return ['message' => 'Student is not in classroom.'];
if($exam->classrooms()->where(['classroom_id', $classroom->id], ['access', 1])->first() == null)
return ['message' => 'Class does not access to exam yet.'];
}
I would suggest you rather pass the primary keys of the selected $exam, $student and $classroom models to your controller from the form and validate whether they exist in the corresponding tables, rather than having to check their existence using a bunch of different columns.
If you pass the primary keys, you could use the 'exists' validation rule to check if they exist. For example, in your AuthorizeStudentRequest class you could have the following function:
public function rules()
{
return [
'exam_id' => 'required|exists:exams',
'student_id' => 'required|exists:students',
'classroom_id' => 'required|exists:classrooms',
];
}
Otherwise, if you really need to use the different columns to check the existence of the exam, student and classroom, you could create custom validation rules and use them in your AuthorizeStudentRequest class. For example, create a custom validation rule that checks whether the exam exists as follows:
$php artisan make:rule ExamExists
class ExamExists implements Rule
{
private $token;
private $hash;
public function __construct($token, $hash)
{
$this->token = $token;
$this->hash = $hash;
}
public function passes($attribute, $value)
{
return Exam::where([['hash', $hash], ['token', $token]])->count() > 0;
}
}
And then you can use the custom validation rule in your request as follows:
public function rules()
{
return [
'hash' => ['required', new ExamExists($this->hash, $this->token)],
... other validation rules ...
]
}
For checking whether a student has access to a classroom or a class has access to an exam, you could use policies.
API resources present a way to easily transform our models into JSON responses. It acts as a transformation layer that sits between our Eloquent models and the JSON responses that are actually returned by our API. API resources is made of two entities: a resource class and a resource collection. A resource class represents a single model that needs to be transformed into a JSON structure, while a resource collection is used for transforming collections of models into a JSON structure.
Both the resource class and the resource collection can be created using artisan commands:
// create a resource class
$ php artisan make:resource UserResource
// create a resource collection using either of the two commands
$ php artisan make:resource Users --collection
$ php artisan make:resource UserCollection
Before diving into all of the options available to you when writing resources, let's first take a high-level look at how resources are used within Laravel. A resource class represents a single model that needs to be transformed into a JSON structure. For example, here is a simple User resource class:
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
Every resource class defines a toArray method which returns the array of attributes that should be converted to JSON when sending the response. Notice that we can access model properties directly from the $this variable. More information here
https://laravel.com/docs/5.7/eloquent-resources

ZF2 Unit test of login in wierd code

I am new to ZF2 and I want to test the login method in a legacy application. Or introduce Unit tests in old code :).
The code that I have is not done according to the manual; it seems super strange if I compare it to the manual examples or even best practices.
I the login method like this:
http://pastebin.com/ZzvuBcGe
in this case the legacy is that Helper, Carts, Users, Userslogs and Usertests are models .... all of them extend DB.
In the module.config.php I have this code:
'service_manager' => array(
'factories' => array(
'Zend\Db\Adapter\Adapter' => 'Zend\Db\Adapter\AdapterServiceFactory',
'AuthService' => function ($sm) {
$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
$dbTableAuthAdapter = new DbTableAuthAdapter(
$dbAdapter,
'tbl_user',
'USER_LOGIN',
'USER_PASSWORD',
'MD5(?)'
);
$authService = new AuthenticationService();
$authService->setAdapter($dbTableAuthAdapter);
$authService->setStorage(new StorageSession('session'));
return $authService;
},
'Helper' => function ($sm) {
return new Helper($sm);
},
'Users' => function ($sm) {
return new Users($sm);
},
'Carts' => function ($sm) {
return new Carts($sm);
}
...
I know that the DbTableAuthAdapter is deprecated but I have to understand how to modify this in order to change it in the best way possible. I have the feeling if I change this all the User, Carts etc models will crash.
My Unit test is like this for the moment:
<?php namespace ApplicationTest\Controller;
use Application\Controller\LoginController;
use Zend\Stdlib\ArrayUtils;
use Zend\Test\PHPUnit\Controller\AbstractHttpControllerTestCase;
class LoginControllerTest extends AbstractHttpControllerTestCase
{
protected $traceError = true;
public function setUp()
{
parent::setUp();
// The module configuration should still be applicable for tests.
// You can override configuration here with test case specific values,
// such as sample view templates, path stacks, module_listener_options,
// etc.
$configOverrides = [];
$this->setApplicationConfig(ArrayUtils::merge(
// Grabbing the full application configuration:
include __DIR__ . '/../../../../../config/application.config.php',
$configOverrides
));
}
public function loginCredentialsProvider()
{
return [
['userDev', '12345'],
];
}
/**
* #covers LoginController::loginAction()
* #dataProvider loginCredentialsProvider
* #param $username
* #param $password
*/
public function testLogin($username, $password)
{
// prepare request
//$this->getRequest()
//->setMethod('POST')
//->setPost(new Parameters(array(
//'user_login' => $username,
//'user_password' => $password
//)));
$helperMock = $this->getMockBuilder('Application\Model\Helper')
->disableOriginalConstructor()
->getMock();
$serviceManager = $this->getApplicationServiceLocator();
$serviceManager->setAllowOverride(true);
$serviceManager->setService('Application\Model\Helper', $helperMock);
// send request
$this->dispatch('/login', 'POST', $this->loginCredentialsProvider());
$this->assertEquals('userDev12345', $username . $password);
// $this->markTestIncomplete('login incomplete');
}
/**
* #depends testLogin
*/
public function testLogout()
{
$this->markTestIncomplete('logout incomplete');
}
}
I tried different ways to test but no succes and of course that I get errors:
Zend\ServiceManager\Exception\ServiceNotCreatedException: An exception was raised while creating "Helper"; no instance returned
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:930
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:1057
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:633
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:593
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:525
/project/module/Application/src/Application/Controller/LoginController.php:38
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractActionController.php:83
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:468
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:207
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractController.php:116
/project/vendor/zendframework/zendframework/library/Zend/Mvc/DispatchListener.php:113
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:468
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:207
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Application.php:313
/project/vendor/zendframework/zendframework/library/Zend/Test/PHPUnit/Controller/AbstractControllerTestCase.php:282
/project/module/Application/test/ApplicationTest/Controller/LoginControllerTest.php:69
/project/vendor/phpunit/phpunit/phpunit:47
Caused by
Zend\ServiceManager\Exception\ServiceNotCreatedException: An exception was raised while creating "Zend\Db\Adapter\Adapter"; no instance returned
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:930
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:1055
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:633
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:593
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:525
/project/module/Application/src/Application/Model/DB.php:17
/project/module/Application/config/module.config.php:1324
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:923
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:1057
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:633
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:593
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:525
/project/module/Application/src/Application/Controller/LoginController.php:38
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractActionController.php:83
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:468
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:207
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractController.php:116
/project/vendor/zendframework/zendframework/library/Zend/Mvc/DispatchListener.php:113
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:468
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:207
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Application.php:313
/project/vendor/zendframework/zendframework/library/Zend/Test/PHPUnit/Controller/AbstractControllerTestCase.php:282
/project/module/Application/test/ApplicationTest/Controller/LoginControllerTest.php:69
/project/vendor/phpunit/phpunit/phpunit:47
Caused by
PHPUnit_Framework_Error_Notice: Undefined index: db
/project/vendor/zendframework/zendframework/library/Zend/Db/Adapter/AdapterServiceFactory.php:26
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:923
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:1055
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:633
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:593
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:525
/project/module/Application/src/Application/Model/DB.php:17
/project/module/Application/config/module.config.php:1324
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:923
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:1057
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:633
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:593
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:525
/project/module/Application/src/Application/Controller/LoginController.php:38
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractActionController.php:83
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:468
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:207
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractController.php:116
/project/vendor/zendframework/zendframework/library/Zend/Mvc/DispatchListener.php:113
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:468
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:207
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Application.php:313
/project/vendor/zendframework/zendframework/library/Zend/Test/PHPUnit/Controller/AbstractControllerTestCase.php:282
/project/module/Application/test/ApplicationTest/Controller/LoginControllerTest.php:69
/project/vendor/phpunit/phpunit/phpunit:47
The issues that I have are first how to get the test to pass with this code? I know that normally you do the test and after that the code but I need a starting point to understand the mess that I have in the application. Second, what is the easy or the best way to modify the "models" to not be a dependency for each method and then pass the test? How to modify the deprecated DbTableAuthAdapter in order not to brake all things?
Like i said I am new to ZF2 and Phpunit and I am stuck over this messy code and I have the best practices in my mind but I don't know how to put them in action in this code. Thank you for all the info that I will receive for this.
LATER EDIT
the solution is to add this line in the test, foreach model:
// access via application object..
$bla = $this->getApplication()->getServiceManager()->get('Tests');
the solution is to add this line in the test, foreach model:
$bla = $this->getApplication()->getServiceManager()->get('Tests');
Thank you i336_ :)

CakePHP 3 Filestorage Plugin Triggering the ImageProcessingListener to delete versions

https://github.com/burzum/cakephp-file-storage/blob/3.0/docs/Home.md
I have two tables
ProductStylesTable.php and ProductStyleImagesTable.php which extends ImageStorageTable which is connect to my FileStorage Table in my sql that was created with the migration tool.
Upload works fine.
// ProductStylesController.php
public function upload($product_style_id = null) {
$this->ProductStyles->ProductStyleImages->upload( $product_style_id, $entity)
//ProductStyleImagesTable.php
class ProductStyleImagesTable extends ImageStorageTable {
//initialize code...
public function upload($product_style_id, $entity) {
$entity = $this->patchEntity($entity, [
'adapter' => 'Local',
'model' => 'ProductStyles',
'foreign_key' => $product_style_id,
]);
return $this->save($entity);
}
Awesome, ProductStyleImages is listening for the upload method and places it in the appropriate folder. I was hoping this would work the same for delete.
So I called
//ProductStylesController.php
$this->ProductStyles->ProductStyleImages->delete($fileStorage_id)
//In my ProductStyleImagesTable.php
public function delete($fileStorageID = null) {
//deleting the row, hoping for ImageProcessingListener to pick it up?
$FileStorageTable = TableRegistry::get('FileStorage');
$query = $FileStorageTable->query();
$query->delete()
->where(['id' => $fileStorageID ])
->execute();
}
I get an error that delete must be compatible with the interface. So to avoid this conflict I renamed my function to 'removeImage'. It works in removing the row but the Listener isn't picking it up. I looked in ImageStorageTable.php and FileStorageTable.php. I see the afterDelete methods. But i'm unsure how to trigger them since i'm unsure how to configure my delete methods to match the interface.
I deleted wrong, it is this way
$entity = $this->get($fileStorageID);
$result = $this->delete($entity);

How to make availableIncludes work in thephpleague/fractal

I'm having trouble implementing Fractal includes. I am trying to include posts with a particular user.
All goes well when I add 'posts' to $defaultIncludes at the top of my UserItemTransformer. Posts are included as expected.
However, posts are NOT included in my json output when I change $defaultIncludes to $availableIncludes, even after calling $fractal->parseIncludes('posts');
The problem seems to lie the fact that the method that includes the posts is only called when I use $defaultIncludes. it is never called when I use $availableIncludes.
I'm probably missing something obvious here. Can you help me find out what it is?
This works:
// [...] Class UserItemTransformer
protected $defaultIncludes = [
'posts'
];
This does not work:
// [...] Class UserItemTransformer
protected $availableIncludes = [
'posts'
];
// [...] Class PostsController
// $fractal is injected in the method (Laravel 5 style)
$fractal->parseIncludes('posts');
Got it!
When I called parseIncludes('posts'), this was on a new Fractal instance, injected into the controller method. Of course I should have called parseIncludes() on the Fractal instance that that did the actual parsing (and that I injected somewhere else, into an Api class).
public function postsWithUser($user_id, Manager $fractal, UserRepositoryInterface $userRepository)
{
$api = new \App\Services\Api();
$user = $userRepository->findById($user_id);
if ( ! $user) {
return $api->errorNotFound();
}
$params = [
'offset' => $api->getOffset(),
'limit' => $api->getLimit()
];
$user->posts = $this->postRepository->findWithUser($user_id, $params);
// It used to be this, using $fractal, that I injected as method parameter
// I can now also remove the injected Manager $fractal from this method
// $fractal->parseIncludes('posts');
// I created a new getFractal() method on my Api class, that gives me the proper Fractal instance
$api->getFractal()->parseIncludes('posts');
return $api->respondWithItem($user, new UserItemTransformer());
}
I'll just go sit in a corner and be really quit for a while, now.

Categories