Symfony - test properly voter class - php

I have trouble writing test for my custom voter in Symfony as I am new at writing test:
This is part of the code I want to test:
protected function voteOnAttribute(
string $attribute,
$subject,
TokenInterface $token
): bool
{
$user = $token->getUser();
if (!$user instanceof UserInterface) {
return false;
}
return match ($attribute) {
'read' => $this->canRead($member, $subject),
'create' => $this->canCreate($member, $subject),
default => throw new Exception(sprintf('Unhandled attribute "%s"', $attribute))
};
}
And this is my test class:
protected $token;
public function setUp(): void
{
$this->token = $this->createMock(TokenInterface::class);
$this->token
->method('getUser')
->willReturn([
'fistName' => 'Jonh',
'lastName' => 'Doe',
'email' => 'johndoe#socialhp.com'
]);
}
/**
* #dataProvider provideCases
*/
public function testVote(
array $attributes,
string $subject,
?TokenInterface $token,
$expectedVote
) {
$voter = new RoleVoter();
$this->assertEquals($expectedVote, $voter->vote($this->token, $subject, $attributes));
}
public function provideCases(): \Generator
{
yield 'user cannot read' => [
['read'],
'customers',
$this->token,
VoterInterface::ACCESS_DENIED,
];
yield 'user can read' => [
['read'],
'customers',
$this->token,
VoterInterface::ACCESS_GRANTED,
];
}
And I always get:
testVote with data set "user can read" (array('read'), 'customers',
null, 1) Failed asserting that -1 matches expected 1.
I would really appreciate if someone would help me continue with this.. Thanks

You didn't stubbed this method $token->getUser().
As you're defining $token as a test double, a stub to be accurate, you're somehow forced to describe every interaction with it, otherwise the testing framework (should be phpunit in this case, if I'm not mistaking), will return null for every not-defined interaction (method call).
By default, all methods of the original class are replaced with a dummy implementation that returns null (without calling the original method). Using the will($this->returnValue()) method, for instance, you can configure these dummy implementations to return a value when called.
(from docs)
You need to write something like
$this->token
->method('getUser')
->willReturn($user);
where $user is another test double you need to create.
As a conclusion, I suggest to test also the case where $token->getUser() returns null (that's basically what you're already doing here, inadvertently

Related

Symfony 5.4 - how to filter/sanitize/validate request parameter in Rest API action

I am fairly new to Symfony 5.4 and recently created my first API using that version
For my specific API endpoint one of the parameters is an array of IDs.
I need to validate this array in the following way:
make sure that this IS an array;
make sure that IDs in the array actually refer to database records;
I implemented it in a straightforward way where I check the array before persisting the entity using typecasting and existing Repository:
$parentPropertyIds = (array)$request->request->get('parent_property_ids');
if ($parentPropertyIds) {
$parentCount = $doctrine->getRepository(Property::class)->countByIds($parentPropertyIds);
if ($parentCount !== count($parentPropertyIds)) {
return $this->json([
'status' => 'error',
'message' => 'parent_property_id_invalid'
], 422);
}
foreach ($parentPropertyIds as $parentPropertyId) {
$parentProperty = $doctrine->getRepository(Property::class)->find($parentPropertyId);
$property->addParent($parentProperty);
}
}
However, this makes my controller action become too "body-positive" and also feels like something that could be implemented in a more elegant way.
I was unable to find anything in Symfony 5.4 docs.
At the moment I am wondering if:
there is a way to filter/sanitize request parameter available in Symfony;
there is an elegant built-in way to apply custom validator constraint to a request param (similar to well-documented entity field validation);
Full endpoint code:
/**
* #Route("/property", name="property_new", methods={"POST"})
*/
public function create(ManagerRegistry $doctrine, Request $request, ValidatorInterface $validator): Response
{
$entityManager = $doctrine->getManager();
$property = new Property();
$property->setName($request->request->get('name'));
$property->setCanBeShared((bool)$request->request->get('can_be_shared'));
$parentPropertyIds = (array)$request->request->get('parent_property_ids');
if ($parentPropertyIds) {
$parentCount = $doctrine
->getRepository(Property::class)
->countByIds($parentPropertyIds);
if ($parentCount !== count($parentPropertyIds)) {
return $this->json([
'status' => 'error',
'message' => 'parent_property_id_invalid'
], 422);
}
foreach ($parentPropertyIds as $parentPropertyId) {
$parentProperty = $doctrine->getRepository(Property::class)->find($parentPropertyId);
$property->addParent($parentProperty);
}
}
$errors = $validator->validate($property);
if (count($errors) > 0) {
$messages = [];
foreach ($errors as $violation) {
$messages[$violation->getPropertyPath()][] = $violation->getMessage();
}
return $this->json([
'status' => 'error',
'messages' => $messages
], 422);
}
$entityManager->persist($property);
$entityManager->flush();
return $this->json([
'status' => 'ok',
'id' => $property->getId()
]);
}
You could use a combination of Data Transfer Object (DTO) with Validation service. There is a number of predefined constraints or you could create a custom one.
For expamle, how to use simple constraint as an annotation:
class PropertyDTO {
/**
* #Assert\NotBlank
*/
public string $name = "";
public bool $shared = false;
}
Then assign data to DTO:
$propertyData = new PropertyDTO();
$propertyData->name = $request->request->get('name');
...
In some cases it is a good idea to define a constructor in the DTO, then get all data from the request and pass it to DTO at once:
$data = $request->getContent(); // or $request->getArray(); depends on your content type
$propertyData = new PropertyDTO($data);
Then validate it:
$errors = $validator->validate($propertyData);
if (count($errors) > 0) {
/*
* Uses a __toString method on the $errors variable which is a
* ConstraintViolationList object. This gives us a nice string
* for debugging.
*/
$errorsString = (string) $errors;
return $this->json([
'status' => 'error',
'message' => 'parent_property_id_invalid'
], 422);
}
//...

Laravel request firing twice with custom validation class

I have a weird problem with my Laravel application, whereby this code gets called twice once the validation rules kick in. I have abstracted validation logic into a separate class, but no matter how I consume the API (tried using Postman, and with jQuery) it still appears to run twice with the output looking like this:
called{"email":["The email has already been taken."],"country":["The country must be a number."]}called{"email":["The email has already been taken."],"country":["The country must be a number."]}
I am only expecting one JSON response. I'm tearing my hair out, tried on two different connections and can't seem to work out why the custom request is called twice. This is a new Laravel app, so there isn't much code to conflict with it.
//Create User Request extends standard request. Handles Validation
public function __construct(CreateUserRequest $request){
$this->request = $request;
}
public function register()
{
try{
$array = DB::transaction(function(){
$email = $this->request->input('email');
$password = $this->request->input('password');
$companyName = $this->request->input('companyName');
$userName = $this->request->input('name');
$country = $this->request->input('country');
$company = Company::create([
'name' => $companyName,
'active'=>true,
'country_id'=>$country
]);
$user = User::create([
'company_id' => $company->id,
'name'=>'admin',
'email' => $email,
'password' => $password,
'active' =>true
]);
if( !$company || !$user )
{
throw new \Exception('User not created for account');
}
return compact('company', 'user');
});
$token = JWTAuth::fromUser($array['user']);
return Response::json(compact('token'));
}
catch( Exception $e )
{
return Response::json(['error' => $e->getMessage() ], HttpResponse::HTTP_CONFLICT );
}
}
Then the validation custom Request..
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Response;
class CreateUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
public function response(array $errors)
{
// return Response::json(['errorg' => $errors ], 200 );
echo('called');
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required|unique:users',
'password' => 'required',
'companyName' => 'required',
'name' => 'required',
'country' => 'required|numeric'
];
}
}
Interesting.
Try to remove CreateUserRequest $request parameter from __construct() and add it to your register() method like this: register(CreateUserRequest $request). And use your request by calling $request instead of $this->request.

Laravel catch Validationexception in custom request

I'm building an API in Laravel, and am using a custom request to validate the inbound data. My problem is that I'm not sure how I 'catch' the validation errors to shape the response.
Here's what I have so far.
Register method wrapped in a transaction:
//Create User Request extends standard request. Handles Validation
public function __construct(CreateUserRequest $request){
$this->request = $request;
}
public function register()
{
try{
$array = DB::transaction(function(){
$email = $this->request->input('email');
$password = $this->request->input('password');
$companyName = $this->request->input('companyName');
$userName = $this->request->input('name');
$country = $this->request->input('country');
$company = Company::create([
'name' => $companyName,
'active'=>true,
'country_id'=>$country
]);
$user = User::create([
'company_id' => $company->id,
'name'=>'admin',
'email' => $email,
'password' => $password,
'active' =>true
]);
if( !$company || !$user )
{
throw new \Exception('User not created for account');
}
return compact('company', 'user');
});
$token = JWTAuth::fromUser($array['user']);
return Response::json(compact('token'));
}
catch( Exception $e )
{
return Response::json(['error' => $e->getMessage() ], HttpResponse::HTTP_CONFLICT );
}
}
The form request looks like this:
class CreateUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required|unique:users',
'password' => 'required',
'companyName' => 'required',
'name' => 'required',
'country' => 'required|numeric'
];
}
}
My errors are coming back automatically, which appear to be the messagebag object serialised to JSON
{"email":["The email has already been taken."]}{"email":["The email has already been taken."]}
Somewhere in there I need to catch the Exception inside the main Controller, but I've used the Custom Request Class to clean up my controller a bit, how would I do that? Note the Exception already caught in this controller, which doesn't seem to pickup whatever is thrown behind the scenes in the custom request.
any pointers? do I need to move validation back to the controller? or is there a cleaner way to do this?
You can override the response method in CreateUserRequest to customize the response:
public function response(array $errors)
{
return parent::response($errors);
}

Proper structuring of a Laravel application and where responses should be handled?

So at the moment my application is structured such that Controllers will access a "Processor" class for certain objects (Business logic). This processor object then accesses all the relevant repositories necessary to perform the action (Data access logic).
For instance, let's pretend there is a UserProcessor class which we are using to attempt updating the user's email via some function: updateEmail($user_id, $new_email). The validation is then handled on the email, but lets say this validation fails. Obviously there are multiple vectors by which the updateEmail function can fail. Many of these will throw exceptions, but in the case of the validation and a few others, they will not (they aren't totally unexpected errors thus an exception is not proper?).
My problem occurs because of the potential for multiple failures. I'm unsure how to handle updateEmail's non-exception type failures. I could just have updateEmail return a response object as necessary which solves everything. But something about this doesn't feel right, isn't the generation of a response object supposed to be handled in the Controller?
I could also create an errors variable, which the controller accesses on receiving False from updateEmail. But this ends up being very generic in-terms of my api, which returns "status", "message", and "payload". In my current form I have a generic message for errors like: "validation error(s) have occurred." and then in the payload specific errors are listed. I could create a errorMessage variable in my UserProcessor, but at this point I might as well return a response object as I need to also store the HTTP error code?
Am I overthinking this? Should the processor just handle responses?
EDIT:
class UserProcessor {
private $user;
private $error_code;
private $error_message;
private $error_payload;
public function __construct(UserRepositoryContract $user){
$this->user = $user;
}
public function error(){
return array(
'code' => $this->error_code,
'message' => $this->error_message,
'payload' => $this->error_payload
);
}
public function updateEmail($user_id, $new_email, $confirmation_email){
$validator = $this->validateEmail(array(
'email' => $new_email,
'email_confirmation' => $confirmation_email
));
if( $validator->fails() ){
$this->error_code = 400;
$this->error_message = 'validation error(s) have occurred.';
$this->error_payload = $validator->errors();
return False;
}
$confirmation_code = str_random(30);
$returned = $this->user->update($user_id, array(
'email' => $new_email,
'confirmed' => 0,
'confirmation_code' => $confirmation_code
));
if( !$returned ){
$this->error_code = 500;
$this->error_message = 'an internal error occurred while ';
$this->error_message .= 'attempting to update user record.';
return False;
}
$this->sendConfirmationCodeEmail($user_id);
return True;
}
}
class UserController extends Controller{
private $user;
private $processor;
public function __construct(UserRepositoryContract $user, UserProcessor $processor){
$this->middleware('auth.api', [ 'except' => ['verifyEmail', 'updateEmail', 'changeName', 'changePassword', 'deleteAccount'] ]);
$this->user = $user;
$this->processor = $processor;
}
public function updateEmail(Request $request, $user_id){
$response = $this->processor->updateEmail($user_id, $request->email, $request->email_confirmation);
if( !$response ){
$error = $this->processor->error();
return $this->responseBuilder(
'fail',
$error['code'],
$error['message'],
$errors['payload']
);
}
return $this->responseBuilder('success', 200, 'successfully updated user\'s email.');
}
}

How to login Yii2 without database?

I need a help!
I have a working mechanism login with DB but sometimes i need get login process without DB (fake user use).
Static method in User model
public static function findByRoot()
{
$arr = [
'id' => 100,
'created_at' => 1444322024,
'updated_at' => 1444322024,
'username' => 'vasya',
'auth_key' => 'aagsdghfgukfyrtweri',
'password_hash' => 'aa2gsdg123hfgukfyrtweri',
'email' => 'some#email',
'status' => 10,
];
return new static($arr);
}
I too tried alternative variat method like:
public static function findByRoot()
{
$model = new User();
$model->id = '1000';
$model->username = 'vasya';
$model->status = 10;
return $model;
}
Yii::$app->getUser()->login() requires implements from UserIdentity
Do auth:
\Yii::$app->getUser()->login(User::findByRoot());
If I put real name from db in login method it returned TRUE and that's OK
But if put User::findByRoot() (the same object) it returned too TRUE but Yii::$app->user->identity has NULL
What's problem?
Yii::$app->user->identity returns null in case it can't find user's id. To fix that, first of all make sure, you supply the right id here:
public static function findIdentity($id)
{
// dump $id here somehow, does it belong to the static collection?
return isset(self::$users[$id]) ? new static(self::$users[$id]) : null;
}
Second option you have, is to always return the instance with filled data, since you use a fake data to test it anyway.
public static function findIdentity($id)
{
// just ignore the $id param here
return new static(array(
'updated_at' => '...',
'username' => '....',
// and the rest
));
}

Categories