Tagrspec

Cleaning up Rails app signup with Devise and Reform

Recently I was working on a Rails app where every user who signed up also needed to be assigned to an account. Since User and Account are two different models, this can be a bit ugly to handle when Devise expects a single resource. However, by leveraging Reform we can use the Form Object Pattern (#3) to create a RegistrationForm that composes Account and User for registration but doesn’t require creating unnecessary linkages between the actual models.

In this post, I’ll walk through the code on how I made it work. I won’t delve too deeply on actually getting Devise setup since the Devise documentation does a pretty good job of covering that. Just note that you will need to copy the Devise views into your app and you will need a local RegistrationsController that inherits from Devise::RegistrationsController.

The Tests

Let’s start first with the tests. This will show you what we want the final experience to look like.

Controller

# spec/controllers/registrations_controller_spec.rb
require 'rails_helper'
RSpec.describe RegistrationsController, type: :controller do
before :each do
@request.env['devise.mapping'] = Devise.mappings[:user]
end
describe "POST #create" do
let(:post_params) { attributes_for(:user).merge(attributes_for(:account)) }
it "Creates a user" do
expect {
post :create, user: post_params
}.to change(User, :count).by(1)
end
it "Creates an account" do
expect {
post :create, user: post_params
}.to change(Account, :count).by(1)
end
it "Associates the user with the account" do
post :create, user: post_params
user = User.last
account = Account.last
expect(user.account).to eq(account)
end
end
end

This is a basic controller test that lets us validate that we can send a post request with the form parameters and the expected objects get created. Please note, even though we will be using a RegistrationForm object, Devise thinks that we will be using a User object and so we need to use :user params to keep Devise happy.

Please note, I am using factory_girl_rails to get the attributes_for methods. Also note the necessity to specify the devise.mapping; this is discussed further down in Some Final Caveats.

Feature

# spec/features/user_registrations_spec.rb
require 'rails_helper'
describe 'User registration' do
let(:user_email) { Faker::Internet.email }
let(:user_password) { Faker::Internet.password(8) }
let(:company_name) { Faker::Company.name }
before :each do
visit new_user_registration_path
fill_in 'user_company_name', with: company_name
fill_in 'user_email', with: user_email
fill_in 'user_password', with: user_password
fill_in 'user_password_confirmation', with: user_password
click_button 'Sign up'
end
it "lets the user log in" do
visit new_user_session_path
fill_in 'user_email', with: user_email
fill_in 'user_password', with: user_password
click_button 'Log in'
expect(page).to have_content("Welcome")
end
end

This is a basic feature test to verify that the process works as expected for a regular site user. The page after a successful login contains the word “Welcome”.

The Form Object

As stated above, we are using Reform to implement a Form Object so that we don’t need to insert any logic into the User or Account models to support capturing fields for both models on user signup.

First take a look at the code for the entire form, then we’ll walk through it step-by-step.

# app/forms/registration_form.rb
class RegistrationForm < Reform::Form
include Composition
property :company_name, on: :account
property :email, on: :user
property :password, on: :user
property :password_confirmation, on: :user, empty: true
model :user
validates :email, presence: true
validates :password, presence: true, confirmation: true
validates :password, length: { minimum: 8 }
validates :company_name, presence: true
def active_for_authentication?
true
end
def authenticatable_salt
end
def save
return false unless valid?
sync
user = model[:user]
user.account = model[:account]
user.save
end
end

Standard Reform Stuff

The first half of the code is standard Reform stuff. We are using Compositions for cleaner fields, setting up our properties, and configuring validation.

Satisfying Devise Processes

The #active_for_authentication? and #authenticatable_salt methods are to make Devise happy because it’s trying to treat the RegistrationForm as if it was the User model with all of the Devise authenticatable methods mixed in.

Saving the Record

The save method is the most interesting thing here (combined with #build_resource from the RegistrationsController below). The Devise::RegistrationsController#create method expects to work with a standard ActiveModel object where the model is validated at the same time that it is saved. However, Reform uses the validation process to actually populate data into the object and validate it. Saving (or syncing) is a separate step. Here is a snippet of the Devise #create method to see what Devise is doing

# POST /resource
def create
build_resource(sign_up_params)
resource_saved = resource.save
yield resource if block_given?
if resource_saved
# Do saved stuff...
else
# Re-render to fix
end
end

As you can see, Devise expects the output from resource.save to indicate if the record is valid. Since we need to check validity differently with Reform, we have our own #save method in the form which fails fast if the RegistrationForm is not valid, but actually handles the save if it is.

The Controller

This is a fairly straightforward implantation of a custom Devise controller with one twist.

# app/controllers/registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController
def create
configure_permitted_parameters
super
end
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) << :company_name
end
def build_resource(hash = nil)
self.resource = registration_form = RegistrationForm.new(user: User.new, account: Account.new)
unless hash.nil? || hash.length == 0
self.resource.validate hash
end
end
def after_sign_up_path_for(resource)
root_path
end
end

As noted above, Reform uses the validation process to populate the form with data from the params hash. Since the #build_resource method is the only time we can get access to the params hash from Devise, we need to validate there so that we have the params data for later usage. But, #build_resource gets called from both #create and #new; and when it’s called from #new the hash is empty. If we try to validate with an empty hash we’ll show the signup form form with validation errors – definitely not something that is conducive to people signing up.

The View

The view is pretty standard. Only significant thing to note is that we are treating the :company_name field as if it belongs to the resource – which in this case it does. It’s not a field on User, but it is a field on RegistrationForm which is the resource for Devise.

<%# app/views/devise/registrations/new.html.erb %>
<h2>Sign up</h2>
<%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= f.error_notification %>
<div class="form-inputs">
<%= f.input :email, required: true, autofocus: true %>
<%= f.input :company_name, required: true %>
<%= f.input :password, required: true, hint: ("#{@minimum_password_length} characters minimum" if @validatable) %>
<%= f.input :password_confirmation, required: true %>
</div>
<div class="form-actions">
<%= f.button :submit, "Sign up" %>
</div>
<% end %>
<%= render "devise/shared/links" %>

Some Final Caveats

Reform and SimpleForm

The latest version of Reform has some issues with SimpleForm. I’ve been working with the Reform maintainer on a patch but you may need to use my fork of Reform.

RSpec and Devise

In order to get RSpec to work correctly with Devise in controller tests, you will need to edit your spec file and your rails_helper.rb (or spec_helper.rb if you’re running an older RSpec). The Devise How To: Test controllers with Rails 3 and 4 (and RSpec) wiki page has the details.

RSpec Integration tests for Devise User Registration

I’m working on a Rails project and wanted to validate that the Devise User Registration path was working as I expected. A quick google search turned up Devise Integration Tests With rspec which was a good starting point, but there were a few things I wanted to change.

  • Break the test up to have only one assertion per block
  • Update to use RSpec 3 and the expect syntax
  • Use capybara-email methods for clicking email links

That led me to the code below.

# /spec/features/user_registration_spec.rb
require 'rails_helper';
describe 'User registration' do
let(:user_email) { 'registration_test_user@example.org' }
let(:user_password) { 'registration_test_password' }
before :each do
visit new_user_registration_path
fill_in 'user_email', with: user_email
fill_in 'user_password', with: user_password
fill_in 'user_password_confirmation', with: user_password
click_button 'Sign up'
end
it "shows message about confirmation email" do
expect(page).to have_content("A message with a confirmation link has been sent to your email address. Please open the link to activate your account.")
end
describe "confirmation email" do
# Include email_spec modules here, not in rails_helper because they
# conflict with the capybara-email#open_email method which lets us
# call current_email.click_link below.
# Re: https://github.com/dockyard/capybara-email/issues/34#issuecomment-49528389
include EmailSpec::Helpers
include EmailSpec::Matchers
# open the most recent email sent to user_email
subject { open_email(user_email) }
# Verify email details
it { is_expected.to deliver_to(user_email) }
it { is_expected.to have_body_text(/You can confirm your account/) }
it { is_expected.to have_body_text(/users\/confirmation\?confirmation/) }
it { is_expected.to have_subject(/Confirmation instructions/) }
end
context "when clicking confirmation link in email" do
before :each do
open_email(user_email)
current_email.click_link 'Confirm my account'
end
it "shows confirmation message" do
expect(page).to have_content('Your account was successfully confirmed')
end
it "confirms user" do
user = User.find_for_authentication(email: user_email)
expect(user).to be_confirmed
end
end
end

© 2025 Matt Vanderpol

Theme by Anders NorénUp ↑