Corbin's avatar

How do I test encrypted database fields?

I'm encrypting database fields using a custom Encryptable trait:

Encryptable.php

<?php
namespace App;

use Illuminate\Support\Facades\Crypt;
trait Encryptable
{
    public function getAttribute($key)
    {
        $value = parent::getAttribute($key);

        if (in_array($key, $this->encryptable)) {
            $value = Crypt::decrypt($value);
            return $value;
        } else {
            return $value;
        }
    }

    public function setAttribute($key, $value)
    {
        if (in_array($key, $this->encryptable)) {
            $value = Crypt::encrypt($value);
        }

        return parent::setAttribute($key, $value);
    }
} 

This allows me to specify in my model what fields I would like encrypted:

Post

use Encryptable;

protected $encryptable = ['title'];

I've learned that Crypt doesn't create a reproducible hash which makes it problematic in my assertDatabaseHas() assertions:

CreatePostTest.php

/** @test */
public function a_user_can_create_a_thought_record()
{
    //$this->withoutExceptionHandling();
    $this->actingAs($creator = factory('App\User')->create());

    $post = factory('App\Post')->make(['user_id' => $creator->id]);

    //You can't use $post->toArray() because toArray() encrypts fields.
    $decryptedPost = [
        'title' => $post->title,
    ];

    $response = $this->json('POST', '/api/posts', $decryptedPost )
        ->assertStatus(201);

    $this->assertDatabaseHas('posts', [
        'title' => $post->title,
    ]);
}

As said this doesn't work:

$this->assertDatabaseHas('posts', [
        'title' => $post->title,
 ]);

What other test can I use to assert that the posts title is the same in the database as what I'm sending through the request and how do I do this?

0 likes
6 replies
Corbin's avatar

@bugsysha no I haven’t, this is a new concept to me. After reading the docs I’m kinda confused as to how I would implement that? Like would I use object mocking? Is there any pseudo code you can share with me to grasp this?

bugsysha's avatar

@corbin

Put following at the top of the test

$this->mock(Crypt::class, function ($mock) {
    $mock->shouldReceive('encrypt')->times(1)->andReturn('encrypt-value');
    $mock->shouldReceive('decrypt')->times(1)->andReturn('decrypt-value');
});

Note that you should put your own values for both encrypt and decrypt methods and maybe change integers passed to times methods since I'm can not know the flow of your code. There are also methods named something like onceOrMore which you can use instead of times if you do not want to be that precise.

Also I would avoid dynamic assertion values for following

    $this->assertDatabaseHas('posts', [
        'title' => $post->title, // replace with a string
    ]);

Since the code might now show you errors that you have with code and it is easier to debug.

P.S. Since Crypt is a Facade, you might need to mock the object which Crypt facade points to. Have no idea what it is since I'm writing from a phone.

Corbin's avatar

@bugsysha, I'm still moderately confused. I'm trying to read up on mockery and watch videos to catch up, but shouldn't I be mocking the Post model and not the Crypt class? Then after mocking the post model I should receive the methods in the encryptable trait?

Also I would avoid dynamic assertion values for following

 $this->assertDatabaseHas('posts', [
        'title' => $post->title, // replace with a string
    ]);

Since the code might now show you errors that you have with code and it is easier to debug.

The problem is that I don't know what that string is since the title gets encrypted.

Thanks for helping me so far. I swear I'm sitting here reading and watching videos about mockery trying to grasp it. I think I'm getting closer.

Corbin's avatar

@bugsysha I think I'm starting to see what you're getting at:

Okay for the assertDatabaseHas I changed that to be assertJsonFragment:

PostController

public function store(Request $request)
{
    $this->validate($request, [
        'title' => 'required|max:255',
    ]);
    
    $post = Post::create([
        'title' => $request->input('title'),
    ]);

    return response(new PostResource($post),201);
}

PostTest

/** @test */
public function a_user_can_create_a_post_record()
{
    //$this->withoutExceptionHandling();
    $this->actingAs($creator = factory('App\User')->create());

    $post = factory('App\Post')->make(['user_id' => $creator->id]);

    //You can't use $post->toArray() because toArray() encrypts fields.
    $decryptedPost = [
        'title' => $post->title,
    ];

    $response = $this->json('POST', '/api/posts', $decryptedPost )
        ->assertStatus(201);

    $response->assertJsonFragment(['title' => $post->title]);
}

You got me thinking that I should probably be testing to see if values have been encrypted. That's where using this at the top of my code comes in:

$this->mock(Crypt::class, function ($mock) {
    $mock->shouldReceive('encrypt')->times(1)->andReturn('encrypt-value');
    $mock->shouldReceive('decrypt')->times(1)->andReturn('decrypt-value');
});

Is that true?

bugsysha's avatar

@corbin

Cause you are using eloquent resources they will handle returning correct response code so you can replace

return response(new PostResource($post),201);

with something like

return new PostResource($post);

You got me thinking that I should probably be testing to see if values have been encrypted.

You can make unit tests to just test the encrypting and decrypting part so you do not have to test it in all your feature tests.

That's where using this at the top of my code comes in

Yes and your test should look something like

/** @test */
public function a_user_can_create_a_post_record()
{
    // arrange
    $this->mock(Crypt::class, function ($mock) {
        $mock->shouldReceive('encrypt')->times(1)->andReturn('encrypt-value');
        $mock->shouldReceive('decrypt')->times(1)->andReturn('decrypt-value');
    });
    $post = factory(\App\Post::class)->make();
    
    // act
    $response = $this->actingAs($post->user)
    ->jsonPost(route('name-of-the-route'), [
        'title' => $post->title,
    ]);

    // assert
    $response
    ->assertStatus(Response::HTTP_CREATED)
    ->assertJsonFragment(['title' => $post->title]);
}

Another thing you can do to make your tests even more logical is use spies instead of mocks

/** @test */
public function a_user_can_create_a_post_record()
{
    // arrange
    $post = factory(\App\Post::class)->make();
    
    // act
    $response = $this->actingAs($post->user)
    ->jsonPost(route('name-of-the-route'), [
        'title' => $post->title,
    ]);

    // assert
    $response
    ->assertStatus(Response::HTTP_CREATED)
    ->assertStructure(['title']);

    $this->spy(Crypt::class, function ($mock) {
    $mock->shouldHaveReceived('encrypt')->times(1);
    $mock->shouldHaveReceived('decrypt')->times(1);
    });
}

Also if you've tested encoding/decoding in unit tests then you should not worry at all about that. All boils down to what you want your tests to look like. For me it is easier to test encoding/decoding in one place and forget about it then to have to mock it in who knows how many tests.

Please or to participate in this conversation.