Rebuilding my invoicing application in Laravel, Part 4 - Endpoints

In this post, I want to build out the backend functionality for the client sections.

Goal

In this post, I want to build out the backend functionality for the client sections. Sometimes, I will refer to clients as a "resource". Usually, but not always, resources align with the primary database tables of my application. So clients, invoices, work orders, etc will all be generalized as "resources". For this rebuild, I would like to use Vue components that leverage AJAX to handle GET, POST, PUT and DELETE operations for resources as needed. So I will start by building out a basic clients endpoint.

Tests and Development

I'll start with building a suite of http tests with endpoints to handle RESTful verbs for the client "resource".

GET

php artisan make:test ClientsEndpointTests/GetClientsEndpointTest

Again, I'm keeping it very simple for now and will add tests as needed. Here are 2 basic tests for the GET endpoint.

class GetClientsEndpointTest extends TestCase
{
    use RefreshDatabase;

    protected $base = 'api/client';

    /**
     * @test
     */
    public function guests_can_not_reach_get_clients_endpoint()
    {
        $this->json('GET', $this->base)->assertStatus(401);
    }

    /**
     * @test
     */
    public function get_clients_endpoint_should_return_a_list_of_all_clients()
    {
        $clients = factory(Client::class, 2)->create();

        $this->actingAsNewUser()
            ->json('GET', $this->base)
            ->assertExactJson($resources->toArray());
    }
}

Let's run the first test.

phpunit --filter guests_can_not_reach_get_clients_endpoint

There was 1 failure:

1) Tests\Feature\ClientsEndpointTests\GetClientsEndpointTest::guests_can_not_reach_get_clients_endpoint
Expected status code 401 but received 404.
Failed asserting that false is true.

/Users/philmareu/Code/invoicing/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:77
/Users/philmareu/Code/invoicing/tests/Feature/ClientsEndpointTests/GetClientsEndpointTest.php:20

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

Yep, no route. But to move things along, I'll go and make the controller, add the method and the middleware.

routes/web.php

Route::get('api/clients', 'Endpoints\ClientsEndpointController@get');
php artisan make:controller Endpoints/ClientsEndpointController

app/Http/Controllers/Endpoints/ClientsEndpointController.php

class ClientsEndpointController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }

    public function get()
    {

    }
}
$ phpunit --filter guests_can_not_reach_get_clients_endpoint

OK (1 test, 1 assertion)

Boom! It passed. Ok, let's take a look at the other test.

/**
 * @test
 */
public function get_clients_endpoint_should_return_a_list_of_all_clients()
{
    $clients = factory(Client::class, 2)->create();

    $this->actingAsNewUser()
        ->json('GET', $this->base)
        ->assertExactJson($resources->toArray());
}
phpunit --filter get_clients_endpoint_should_return_a_list_of_all_clients

Error: Class 'Tests\Feature\ClientsEndpointTests\Client' not found

Well, we definitely need the Client model but what else? Let's make the model, setup the factory and the migration.

php artisan make:model Models/Client
php artisan make:factory ClientFactory --model=Models/Client

database/factories/ClientFactory.php

$factory->define(Invoicing\Models\Client::class, function (Faker $faker) {
    return [
        'title' => $faker->company,
        'address_1' => $faker->streetAddress,
        'address_2' => $faker->streetAddress,
        'city' => $faker->city,
        'state' => $faker->state,
        'zip' => $faker->numberBetween(10000, 99999),
        'phone' => $faker->phoneNumber,
        'email' => $faker->email 
    ];
});
php artisan make:migration create_clients_table --create=clients

database/migrations/{DATE}_create_clients_table.php

public function up()
{
    Schema::create('clients', function (Blueprint $table) {
        $table->increments('id');
        $table->string('title');
        $table->string('email');
        $table->string('address_1')->nullable();
        $table->string('address_2')->nullable();
        $table->string('city')->nullable();
        $table->char('state', 2)->nullable();
        $table->string('zip')->nullable();
        $table->string('phone')->nullable();
        $table->timestamps();
    });
}

Run the test.

phpunit --filter get_clients_endpoint_should_return_a_list_of_all_clients

Invalid JSON was returned from the route.

The get method on the controller has no code that returns a payload. Let's attempt to fix that.

app/Http/Controllers/ClientsEndpointController.php

protected $client;

public function __construct(Client $client)
{
    $this->middleware('auth');
    $this->client = $client;
}

public function get()
{
    return $this->client->all();
}

Notice I'm injecting in the Client Modal class and then just simply return the full client list. Later down the road, I might switch this out for a repository. But for now, I wanted to keep it simple. Let's run the test!

phpunit --filter get_clients_endpoint_should_return_a_list_of_all_clients

Failed asserting that two strings are equal.
--- Expected
+++ Actual

-'[{"address_1":"6577 Fannie Estate","address_2":"2924 Flatley Land","city":"East Simone","created_at":"2017-10-04 20:45:35","email":"waino.kulas@hotmail.com","id":2,"phone":"283.347.4681","state":"Utah","title":"Carter Ltd","updated_at":"2017-10-04 20:45:35","zip":36188},{"address_1":"830 Macie Islands Suite 712","address_2":"4716 Armstrong Viaduct Apt. 444","city":"West Melvinfurt","created_at":"2017-10-04 20:45:35","email":"blair.jast@osinski.biz","id":1,"phone":"+16045998797","state":"New Hampshire","title":"Will, Feeney and Windler","updated_at":"2017-10-04 20:45:35","zip":60667}]'

+'[{"address_1":"6577 Fannie Estate","address_2":"2924 Flatley Land","city":"East Simone","created_at":"2017-10-04 20:45:35","email":"waino.kulas@hotmail.com","id":2,"phone":"283.347.4681","state":"Utah","title":"Carter Ltd","updated_at":"2017-10-04 20:45:35","zip":"36188"},{"address_1":"830 Macie Islands Suite 712","address_2":"4716 Armstrong Viaduct Apt. 444","city":"West Melvinfurt","created_at":"2017-10-04 20:45:35","email":"blair.jast@osinski.biz","id":1,"phone":"+16045998797","state":"New Hampshire","title":"Will, Feeney and Windler","updated_at":"2017-10-04 20:45:35","zip":"60667"}]'

So this is a mess. But how are they different? If you look closely, you will see that the zip code is sent to the database as a number but returned as a string. I would prefer to keep it a string to handle hyphenated zips. So let's change the factory to cast the value as string.

database/factories/ClientFactory.php

'zip' => (string) $faker->numberBetween(10000, 99999),
phpunit --filter get_clients_endpoint_should_return_a_list_of_all_clients

OK (1 test, 2 assertions)

Perfect. Both tests have passed for the get client endpoint.

POST

Let's do it again for the POST endpoint. I've listed a few validation tests as well, but won't be covering them in this post (see part 1).

php artisan make:test ClientsEndpointTests/PostClientsEndpointTest

tests/Feature/ClientsEndpointTests/PostClientsEndpointTest.php

class PostClientsEndpointTest extends TestCase
{
    use RefreshDatabase;

    protected $base = 'api/clients';

    /**
     * @test
     */
    public function guests_can_not_reach_post_clients_endpoint()
    {
        $this->json('POST', $this->base)
            ->assertStatus(401);
    }

    /**
     * @test
     */
    public function user_can_store_a_new_client()
    {
        $client = factory(Client::class)->make();

        $this->actingAsNewUser()
            ->json('POST', $this->base, $client->toArray())
            ->assertJson($client->toArray());

        $this->assertDatabaseHas('clients', $client->toArray());
    }

    /**
     * @test
     */
    public function storing_a_client_requires_a_title()
    {
        $this->postValidationTest($this->base, 'title');
    }

    /**
     * @test
     */
    public function storing_a_client_requires_an_email()
    {
        $this->postValidationTest($this->base, 'email');
    }

    /**
     * @test
     */
    public function storing_a_client_requires_a_valid_email()
    {
        $this->postValidationTest($this->base, 'email', ['email' => 'not an email']);
    }
}

The first test is straight forward and I'm sure by now you know all we need to do is make the route and add store method.

routes/web.php

Route::post('api/clients', 'Endpoints\ClientsEndpointController@store');

app/Http/Controllers/Endpoints/ClientEndpointsController.php

public function store()
{

}

The second one is a bit different. I'm using the factory make method instead of create. This will fill the model with attributes but will not save it to the database. After hitting the endpoint with the client data I'm checking the database for the new entry.

phpunit --filter user_can_store_a_new_client

The table is empty.

Let's fix this in the controller.

app/Http/Controllers/Endpoints/ClientEndpointsController.php

public function store(Request $request)
{
    return $this->client->create($request->all());
}

Of course, this now returns a MassAssignmentException. I will need to define the fillable attributes in the Client Model.

app/Models/Client.php

class Client extends Model
{
    protected $fillable = [
        'title',
        'email',
        'address_1',
        'address_2',
        'city',
        'state',
        'zip',
        'phone'
    ];
}
phpunit --filter user_can_store_a_new_client

OK (1 test, 3 assertions)

PUT

This one is very similiar to POST. Instead of using the factory, I'll augment the existing information and attempt to update. This feels more reliable to me, since the factory could technically return duplicate data for some fields.

/**
 * @test
 */
public function user_can_update_a_client()
{
    $client = factory(Client::class)->create();

    $data = [
        'title' => 'test-' . $client->name,
        'email' => 'test-' . $client->email,
        'address_1' => 'test-' . $client->address_1,
        'address_2' => 'test-' . $client->address_2,
        'city' => 'test-' . $client->city,
        'state' => 'XX',
        'zip' => 'test-' . $client->zip,
        'phone' => 'test-' . $client->phone
    ];

    $this->actingAsNewUser()
        ->json('PUT', $this->base . '/' . $client->id, $data)
        ->assertJson($data);

    $client = Client::find($client->id);

    $this->assertEquals($data['title'], $client->title);
    $this->assertEquals($data['email'], $client->email);
    $this->assertEquals($data['address_1'], $client->address_1);
    $this->assertEquals($data['address_2'], $client->address_2);
    $this->assertEquals($data['city'], $client->city);
    $this->assertEquals($data['state'], $client->state);
    $this->assertEquals($data['zip'], $client->zip);
    $this->assertEquals($data['phone'], $client->phone);
}
phpunit --filter user_can_update_a_client

Expected status code 200 but received 404.

Let's add the route.

routes/web.php

Route::put('api/clients/{client}', 'Endpoints\ClientsEndpointController@update');

I plan on using implicit modal binding which will query for the id and return the actual client model to the controller's update method. To move this along, I'll go ahead and add the method and add the update functionality.

app/Http/Controller/Enpoints/ClientsEndpointController.php

public function update(Request $request, Client $client)
{
    $client->update($request->all());

    return $client;
}

Since I am Typehinting the Client model, Laravel will know to query using that model and the id from the uri segment.

phpunit --filter user_can_update_a_client

OK (1 test, 3 assertions)

It passed!

DELETE

Again, let's just focus on the main test that performs the action of deleting a client. Here is the test.

/**
 * @test
 */
public function user_can_delete_a_client()
{
    $client = factory(Client::class)->create();

    $this->actingAs(factory(User::class)->create())
        ->json('DELETE', $this->base . '/' . $client->id)
        ->assertStatus(200);

    $this->assertDatabaseMissing('clients', ['id' => $client->id]);
}

So now we need a route, method and a delete action. Let's do that.

routes/web.php

Route::delete('api/clients/{client}', 'Endpoints\ClientsEndpointController@destroy');

app/Http/Controllers/Endpoints/ClientEndpointsController.php

public function destroy(Client $client)
{
    $client->delete();
}
phpunit --filter user_can_delete_a_client

OK (1 test, 2 assertions)

Yes!

Refactor

Before I go and build the other endpoints, I is definitely worth seeing if there is anything that can be extract as a reuable helper for future tests. I have many more endpoints to build so I will look at everything possible to make it this process more efficient.

First, I think it was be worth making 2 abstract classes. One called EndpointTest, will be the global class for all the endpoints to extend and another called ClientsEndpointClass. These class could contain properties, helpers and traits specifically for these endpoint tests.

Endpoint Abstraction Class

Let's start with creating the EndpointTest class.

tests/Feature/EndpointTest.php

namespace Tests\Feature;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use Tests\ValidationHelperTrait;

abstract class EndpointTest extends TestCase
{
    use RefreshDatabase, ValidationHelperTrait;

    protected $base;

    protected $class;

    protected $table;

    /**
     * Create a new resource
     *
     * @return Model
     */
    protected function createResource()
    {
        return factory($this->class)->create();
    }

    /**
     * Create a resource and return the id
     *
     * @return int
     */
    protected function createAndGetResourceId()
    {
        return $this->createResource()->id;
    }

    /**
     * Create a resource and build the endpoint combining the base with the resource id
     *
     * @return string
     */
    protected function attachIdToBase()
    {
        return implode('/', [$this->base, $this->createAndGetResourceId()]);
    }

    /**
     * Find a resource by the id
     * 
     * @param int $id
     * @return Model
     */
    protected function findResourceById($id)
    {
        return ($this->class)::find($id);
    }
}

I've create a few super basic helpers. These are very simple but really help clean up the code and make it more readable. Let's impliment the new class and the helpers on the PutClientsEndpointTest.

So this ...

/**
 * @test
 */
public function guests_can_not_reach_put_clients_endpoint()
{
    $this->json('PUT', $this->base . '/' . factory(Client::class)->create()->id)
        ->assertStatus(401);
}

Becomes this ...

protected $class = Client::class;

/**
 * @test
 */
public function guests_can_not_reach_put_clients_endpoint()
{
    $this->json('PUT', $this->attachIdToBase())->assertStatus(401);
}

Endpoint Specific Abstraction Class

tests/Feature/Endpoints/ClientsEndpointTest.php

class ClientsEndpointTest extends EndpointTest
{
    protected $base = 'api/clients';

    protected $class = Client::class;

    protected $table = 'clients';
}

Currently, this test only holds the properties that will be needed for the verbs tests. These helper classes should make it so much faster to create all the other basis tests.

2018 Phil Mareu - Coder, Traveler & Disc Thrower