domjanzsoo
domjanzsoo

Reputation: 141

File Upload Validation test for Livewire component

I have a user creator Livewire component which has a profile picture uploader. Trying to write some the unit tests for the File field of that form, but it doesn't want to work. I'm sure that my approach is wrong, but there is no other way documented, with which I can achieve what I want.

So I have these fields:

public array $state = [
    'full_name'             => null,
    'email'                 => null,
    'password'              => null,
    'password_confirmation' => null,
    'verified'              => false,
    'profile_picture'       => null,
    'permissions'           => [],
    'roles'                 => []
];

with these validation rules

 protected $rules = [
    'state.full_name'               => 'required',
    'state.email'                   => 'required|email|unique:users,email',
    'state.password'                => 'required|confirmed|min:6',
    'state.password_confirmation'   => 'required',
    'state.profile_picture'         => 'image|max:2048|nullable',
    'state.permissions'             => 'array',
    'state.roles'                   => 'array'
    
];

which then uses these messages

public function messages(): array
{
    return [
        'state.full_name.required' => trans('validation.required', ['attribute' => 'name']),
        'state.email.required' => trans('validation.required', ['attribute' => 'email']),
        'state.email.unique' => trans('validation.unique', ['attribute' => 'user email']),
        'state.email.email' => trans('validation.email', ['attribute' => 'user email']),
        'state.password.required' => trans('validation.required', ['attribute' => 'password']),
        'state.password.min' => trans('validation.min.string', ['attribute' => 'password', 'min' => 6]),
        'state.password.confirmed' => trans('validation.confirmed', ['attribute' => 'password']),
        'state.profile_picture.image' => trans('validation.image', ['attribute' => 'profile picture']),
        'state.profile_picture.max' => trans('validation.max.file', ['max' => '2048', 'attribute' => 'profile picture'])
    ];
}

and here is my test

 /** @test */
public function fails_with_oversized_profile_image()
{
    $this->actingAs($this->userWithAddPermissionAccess);

    $this->assertEquals(3, User::count());

    $profileImage = UploadedFile::fake()->create('picture.jpg', 432545243254325342543254325);
    $profileImageMocked = Mockery::mock($profileImage);
    $profileImageMocked
        ->shouldReceive('temporaryUrl')
        ->andReturn('http://some-signed-url.test');
    
    Livewire::test(CreateUser::class)
        ->set('state', [
            'full_name' => 'Joe Doe',
            'email' => '[email protected]',
            'password' => 'password',
            'password_confirmation' => 'password',
            'profile_picture' => $profileImageMocked,
            'permissions' => [],
            'roles' => []
        ])
        ->call('addUser')
        ->assertHasErrors(['state.profile_picture' => 'max']);

    $this->assertEquals(3, User::count());
}

but the result I'm getting is that 'Component has no errors.' .The validator doesn't find any issue with that mocked File I passed to the component. Probably I shouldn't use mocks, but the official documentation suggests the use of the UploadedFile faker, but the object returned by that doesn't have a temporaryUrl method, which is called by my component and it makes my component fail the test.

UPDATE - So I found a "solution" for the issue of this ticket, I wrapped the temporaryUrl method call in a ternary operator in my blade file, still not happy about Livewire's approach on this though. But now I'm facing a different issue. Here is the full code of my component:

<?php

namespace App\Livewire\Users;

use Livewire\Component;
use Illuminate\Support\Facades\Hash;
use App\Contract\UserRepositoryInterface;
use App\Contract\PermissionRepositoryInterface;
use App\Contract\RoleRepositoryInterface;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Livewire\WithFileUploads;

class Add extends Component
{
use WithFileUploads;

private $userRepository;
private $permissionRepository;
private $roleRepository;

public array $state = [
    'full_name'             => null,
    'email'                 => null,
    'password'              => null,
    'password_confirmation' => null,
    'verified'              => false,
    'profile_picture'       => null,
    'permissions'           => [],
    'roles'                 => []
];

protected $rules = [
    'state.full_name'               => 'required',
    'state.email'                   => 'required|email|unique:users,email',
    'state.password'                => 'required|confirmed|min:6',
    'state.password_confirmation'   => 'required',
    'state.profile_picture'         => 'image|max:2048|nullable',
    'state.permissions'             => 'array',
    'state.roles'                   => 'array'
    
];

protected $listeners = [
    'user-permissions' => 'handlePermissions'
];

public function messages(): array
{
    return [
        'state.full_name.required' => trans('validation.required', ['attribute' => 'name']),
        'state.email.required' => trans('validation.required', ['attribute' => 'email']),
        'state.email.unique' => trans('validation.unique', ['attribute' => 'user email']),
        'state.email.email' => trans('validation.email', ['attribute' => 'user email']),
        'state.password.required' => trans('validation.required', ['attribute' => 'password']),
        'state.password.min' => trans('validation.min.string', ['attribute' => 'password', 'min' => 6]),
        'state.password.confirmed' => trans('validation.confirmed', ['attribute' => 'password']),
        'state.profile_picture.image' => trans('validation.image', ['attribute' => 'profile picture']),
        'state.profile_picture.max' => trans('validation.max.file', ['max' => '2048', 'attribute' => 'profile picture'])
    ];
}

public function updatedStatePassword(): void
{
    $this->validateOnly('state.password');
}

public function updatedStatePasswordConfirmation(): void
{
    $this->validateOnly('state.password');
}

public function handlePermissions(array $selections): void
{
    $this->state['permissions'] = [];

    foreach ($selections as $id => $selection) {
        if ($selection['selected'] && !in_array($id, $this->state['permissions'])) {
            array_push($this->state['permissions'], $id);
        }
    }
}

public function handleRoles(array $selections): void
{
    $this->state['roles'] = [];

    foreach ($selections as $id => $selection) {
        if ($selection['selected'] && !in_array($id, $this->state['roles'])) {
            array_push($this->state['roles'], $id);
        }
    }
}

public function render()
{
    dump('before view');
    return view('livewire.users.add', [
        'permissions'   => $this->permissionRepository->getAll(),
        'roles'         => $this->roleRepository->getAll()
    ]);
    dump('after view');
}

public function boot(
    UserRepositoryInterface $userRepository,
    PermissionRepositoryInterface $permissionRepository,
    RoleRepositoryInterface $roleRepository
)
{
    dump('boot beginnig');
    $this->userRepository = $userRepository;
    $this->permissionRepository = $permissionRepository;
    $this->roleRepository = $roleRepository;
    dump('boot end');
}

public function addUser(): void
{     
    if (!access_control()->canAccess(auth()->user(), 'add_user')) {
        throw new AuthorizationException(trans('errors.unauthorized_action', ['action' => 'add user']));
    }

    $validatedData = $this->validate();

    try {
        $user = $this->userRepository->createUser(
            [
                'name'      => $validatedData['state']['full_name'],
                'email'     => $validatedData['state']['email'],
                'password'  => Hash::make($validatedData['state']['password'])
            ],
            $this->state['permissions'],
            $this->state['roles']
        );

        if ($validatedData['state']['profile_picture']) {
            $profilePictureFileName = md5($user->id) . '.' . $validatedData['state']['profile_picture']->extension();

            $validatedData['state']['profile_picture']->storeAs('/avatar', $profilePictureFileName, $disk = config('filesystems.default'));

            $user->profile_photo_path = 'storage/avatar/' . $profilePictureFileName;
            $user->save();
        }

    } catch(Exception $exception) {
        $this->dispatch('toastr', ['type' => 'error', 'message' => $exception->getMessage()]);
    }

    $this->state['full_name'] = null;
    $this->state['email'] = null;
    $this->state['password'] = null;
    $this->state['password_confirmation'] = null;
    $this->state['profile_picture'] = null;
    $this->state['permissions'] = [];
    $this->state['roles'] = [];

    $this->dispatch('toastr', ['type' => 'confirm', 'message' => trans('notifications.successfull_creation', ['entity' => 'User'])]);
    $this->dispatch('user-permissions-submitted');
    $this->dispatch('user-roles-submitted');

    return;
}
}

And here is the test:

/** @test */
public function fails_with_oversized_profile_image()
{
    $this->actingAs($this->userWithAddPermissionAccess);

    $this->assertEquals(3, User::count());

    Storage::fake('public');

    $profileImage = UploadedFile::fake()->create('blaa.jpg', 145412);
    
    Livewire::test(CreateUser::class)
        ->set('state', [
            'full_name' => 'Joe Doe',
            'email' => '[email protected]',
            'password' => 'password',
            'password_confirmation' => 'password',
            'profile_picture' => $profileImage,
            'permissions' => [],
            'roles' => []
        ])
        ->call('addUser')
        ->assertHasErrors(['state.profile_picture' => 'max']);

    $this->assertEquals(3, User::count());
}

Now the problem is that running the test returns the following error: enter image description here

As you see I added some debugging dump calls to the component, which indicates, that the view and boot methods are called successfully at first and the snapshot has the right data inside, but then before the addUser method is called by the test, the data in the snapshot becomes null, and the test fails.

Now my question is more Livewire lifecycle related:

When that update method used by Livewire and what is the purpose of it?

What can make that snapshot null before it's being passed to the update method?

I tried to look into the architecture of these classes, but cannot see any call of that update method.

Upvotes: 1

Views: 204

Answers (0)

Related Questions