Goal
The application needs a way for a user to login and interact with the application. What I would like to do is check for a user in the database and if none exist then show them the "install" screen. This screen should allow the user to register but also obtain intial settings such as rate, timezone and company. Again, this is scoped as a self install application for one user.
Tests and Development
php artisan make:test InstallTest
tests/Feature/InstallTest.php
class InstallTest extends TestCase
{
use RefreshDatabase, ValidationHelperTrait;
/**
* @test
*/
public function visitor_redirected_to_install_page_when_the_users_table_is_empty()
{
}
/**
* @test
*/
public function install_page_can_not_be_reached_if_user_table_is_not_empty()
{
}
/**
* @test
*/
public function installation_data_can_only_be_stored_if_the_users_table_is_empty()
{
}
/**
* @test
*/
public function install_page_loads_when_the_users_table_is_empty()
{
}
/**
* @test
*/
public function installation_stores_a_new_user()
{
}
/**
* @test
*/
public function installation_stores_new_invoice_settings()
{
}
/**
* @test
*/
public function successful_installation_will_redirect_user()
{
}
/**
* @test
*/
public function storing_installation_data_requires_an_email()
{
}
/**
* @test
*/
public function storing_installation_data_requires_a_password()
{
}
/**
* @test
*/
public function storing_installation_data_requires_a_timezone()
{
}
/**
* @test
*/
public function storing_installation_data_the_email_must_be_valid()
{
}
/**
* @test
*/
public function storing_installation_data_the_timezone_must_be_valid()
{
}
}
Let's start with the visitor_redirected_to_install_page_when_the_users_table_is_empty
test. Since most pages will redirect to a login page if authenication fails, a simple solution is just to check for an existing user. So basically the /login
uri should redirect to /install
.
/**
* @test
*/
public function install_page_loads_when_the_users_table_is_empty()
{
$this->call('GET', 'login')->assertRedirect('install');
}
Before running this test and getting a 404, I'll just go ahead and setup Laravel's authentication scaffolding.
php artisan make:auth
This will setup all the routes and views need for handling user authentication. Also, I don't want to allow registration and will disable it by implimenting a couple new methods to replace the defaults.
app/Http/Controller/Auth/RegisterController.php
/**
* Redirect to login form
*
* @return \Illuminate\Http\Response
*/
public function showRegistrationForm()
{
return redirect('login');
}
/**
* Redirect to login form
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function register(Request $request)
{
return redirect('login');
}
Ok, now let's run the test.
phpunit --filter install_page_loads_when_the_users_table_is_empty
Response status code [200] is not a redirect status code.
This is expected since we haven't set this up yet. I'll make the update.
app/Http/Controllers/Auth/LoginController.php
/**
* Show the application's login form.
*
* @return \Illuminate\Http\Response
*/
public function showLoginForm()
{
if(User::all()->isEmpty()) return redirect('install');
return view('auth.login');
}
I'll just overwrite the existing showLoginForm
method and check the users table.
phpunit --filter install_page_loads_when_the_users_table_is_empty
OK (1 test, 2 assertions)
On to the next test.
/**
* @test
*/
public function install_page_can_not_be_reached_if_user_table_is_not_empty()
{
$this->createUser();
$this->call('GET', 'install')->assertRedirect('login');
}
routes/web.php
Route::get('install', 'InstallationController@getInstallForm');
php artisan make:controller InstallationController
app/Http/Controllers/InstallationController.php
public function getInstallForm()
{
if(User::all()->count()) return redirect('login');
}
All good to go. The next test is basically the same but just tested against the POST uri.
/**
* @test
*/
public function installation_data_can_only_be_stored_if_the_users_table_is_empty()
{
$this->createUser();
$data = [
'name' => 'Test McTesterson',
'email' => 'test@test.com',
'password' => 'testing',
'timezone' => 'America/Chicago',
'company' => 'Test Co.'
];
$this->call('POST', 'install', $data)->assertRedirect('login');
}
routes/web.php
Route::post('install', 'InstallationController@install');
app/Http/Controllers/InstallationController.php
public function install()
{
if(User::all()->count()) return redirect('login');
}
Next, I need to make sure the view is returned when /install
is reach with an empty users table.
/**
* @test
*/
public function install_page_loads_when_the_users_table_is_empty()
{
$this->call('GET', 'install')->assertViewIs('install');
}
app/Http/Controllers/InstallController.php
public function getInstallForm()
{
if(User::all()->count()) return redirect('login');
return view('install');
}
touch resources/views/install.blade.php
Ok, next I'll work on the actually install. I have fields for both the user and invoice settings.
/**
* @test
*/
public function installation_stores_a_new_user()
{
$data = [
'name' => 'Test McTesterson',
'email' => 'test@test.com',
'password' => 'testing',
'timezone' => 'America/Chicago',
'company' => 'Test Co.'
];
$this->call('POST', 'install', $data);
$this->assertDatabaseHas('users', array_except($data, ['company', 'password']));
}
I'll setup some basic code to make this work.
app/Http/Controllers/InstallationController.php
public function install(Request $request)
{
if (User::all()->count()) return redirect('login');
$request->offsetSet('password', bcrypt($request->password));
$this->user->create($request->all());
}
After running this test I notice an error in the logs.
Integrity constraint violation: 19 NOT NULL constraint failed: users.rate
So I'll just set a default.
database/migrations/{DATE}_create_users_table.php
$table->unsignedInteger('rate')->default(100);
Looks like that did it! Ok, almost there. This next tests just makes sure the invoice settings are created.
/**
* @test
*/
public function installation_stores_new_invoice_settings()
{
$data = [
'name' => 'Test McTesterson',
'email' => 'test@test.com',
'password' => 'testing',
'timezone' => 'America/Chicago',
'company' => 'Test Co.'
];
$this->call('POST', 'install', $data);
$this->assertDatabaseHas('invoice_settings', array_only($data, ['company']));
}
After running this test and looking at the logs, it appears we need to set several fields to NOT NULL. The invoice really just needs the company name at the minimum. So I'll update the migration.
database/migrations/{DATE}_create_invoice_settings_table.php
Schema::create('invoice_settings', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('user_id');
$table->string('logo')->nullable();
$table->string('company');
$table->string('email')->nullable();
$table->string('address_1')->nullable();
$table->string('address_2')->nullable();
$table->string('city')->nullable();
$table->string('state')->nullable();
$table->string('zip')->nullable();
$table->string('phone')->nullable();
$table->text('note')->nullable();
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onUpdate('cascade')->onDelete('cascade');
});
Yep, that did it! After installation is complete, we should authenticate the user and redirect to the home page.
/**
* @test
*/
public function successful_installation_will_redirect_user()
{
$data = [
'name' => 'Test McTesterson',
'email' => 'test@test.com',
'password' => 'testing',
'timezone' => 'America/Chicago',
'company' => 'Test Co.'
];
$this->call('POST', 'install', $data)->assertRedirect('/');
}
app/Http/Controllers/InstallationController.php
public function install(Request $request)
{
if (User::all()->count()) return redirect('login');
$request->offsetSet('password', bcrypt($request->password));
$user = $this->user->create($request->all());
$user->invoiceSettings()->create($request->all());
Auth::login($user);
return redirect('/');
}
Refactor
Ok, I'm going to do a little clean up. First, I'll make a little helper to reduce the repeated install check and make it read better.
app/Http/Controllers/InstallationController.php
/**
* @return int
*/
private function needsInstallation() : bool
{
return (bool) $this->user->all()->isEmpty();
}
I don't like that my controller is doing the actually work of performing the installation. I'll make a special class that handles this process.
app/Installers/Installer.php
<?php
namespace Invoicing\Installers;
use Invoicing\User;
class Installer {
protected $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function install($settings)
{
$user = $this->user->create($settings);
$user->invoiceSettings()->create($settings);
return $user;
}
}
app/Http/Controllers/InstallationController.php
public function install(InstallationRequest $request)
{
if (! $this->needsInstallation()) return redirect('login');
$request->offsetSet('password', bcrypt($request->password));
$user = $this->installer->install($request->all());
Auth::login($user);
return redirect('/');
}
But now I am looking at this thinking, "Why can't the Installer class tell me if it can install?". I'll move this method to the Installer class and update the controller methods.
app/Installers/Installer.php
/**
* @return bool
*/
public function needsInstallation() : bool
{
return (bool) $this->user->all()->isEmpty();
}
I'll update ...
$this->needsInstallation()
to
$this->installer->needsInstallation()
and call it good.