cult3

Building out a User Confirmation flow in Trailblazer

Jun 15, 2016

Table of contents:

  1. How is this going to work?
  2. Setting up the routes
  3. Adding the Controller
  4. Generating the User Mailer
  5. Adding the User Mailer to the User Create Operation
  6. Building out the Operation
  7. Creating the Cell
  8. Adding Controller Tests
  9. Conclusion

A couple of weeks ago I built out the action for creating new users in Culttt using a Trailblazer Operation (Using Inheritance for Trailblazer Operations).

In this application I need 3 different ways of creating new users. Firstly, I need to import users from the old WordPress application. Secondly, I need to create confirmed users to seed the database. And finally, I need the default Operation that will be used as part of the registration process.

When a user registers with the application using the default Operation, I will send a confirmation email that will allow the user to confirm their email address.

But I also need a way to allow imported users to confirm their email address, as well as choose a username, and a password.

In today’s tutorial we’re going to be looking at adding the functionality to request a confirmation email. This will touch upon a couple of interesting aspects of Trailblazer Operations that we haven’t covered so far, and it will hopefully reinforce how Trailblazer fits inside of a typical Rails application.

So with that being said, lets get started!

How is this going to work?

Before we jump into the code, lets have a quick review of what we’re going to build.

We’re first going to need two new routes. One for displaying a form that accepts an email to send the confirmation email to, and one to accept the POST request from the form.

We will need a Controller to accept those requests and we need to create the UserMailer class, and email view template.

We will need to update the User::Create::Operation::Default class to send a confirmation automatically after saving the new user and we will need to create a new Operation for requesting a confirmation email.

We will need to create a new Cell class and view template for displaying the form, and of course we will need a bucket load of tests to make sure everything is working as it should.

Phew! We best get started then!

Setting up the routes

The first thing we need to do is to add two new routes to the routes.rb under the config directory:

Rails
  .application
  .routes
  .draw do
    # Join
    get 'join', to: 'join#new'
    post 'join', to: 'join#create'

    # Login
    get 'login', to: 'login#new'

    # Confirmation
    get 'confirmation/request', to: 'confirmation/request#new'
    post 'confirmation/request', to: 'confirmation/request#create'
  end

If you are familiar with routing in Rails (Defining URL routes in Ruby on Rails), this should be fairly straight forward for you.

There is going to be a request action and a confirm action for this confirmation process.

As you might have noticed, I’m going to separate these two actions into two separate controllers under the Confirmation namespace.

I would rather keep this as two small Controllers, rather than one bigger Controller.

Adding the Controller

Next up I will add the Controller to handle the requests:

module Confirmation
  class RequestController < ApplicationController
    def new
      form Confirmation::Request::Operation
    end

    def create
      run Confirmation::Request::Operation do |op|
        return redirect_to login_url
      end

      render :new, status: 400
    end
  end
end

There isn’t a great deal going on in this Controller, which is a lovely side-effect of using Trailblazer. If you have been following along with these Trailblazer tutorials this is going to look very familiar.

I’ve added the Confirmation::Request::Operation even though I haven’t created it yet because I know exactly what this Controller is going to look like when all the pieces are in place.

At this point I will also add the view form the #new method:

h1 Confirm your membership = concept("confirmation/request/cell", @operation)

Due to the fact that we’re using a Cell to encapsulate this view, it is very simple.

Generating the User Mailer

Before we start creating the Operation class, first we need to generate a new UserMailer class using Rails’ generate command:

bin/rails generate mailer UserMailer

This should create a new user_mailer.rb file under the mailers directory. In this new class I will add a method for confirming a membership:

class UserMailer < ApplicationMailer
  def confirm_membership(user)
    @user = user
    @url = # generate confirmation url
      mail(to: @user.email, subject: 'Confirm your Culttt Membership!')
  end
end

If you are new to Action Mailer, take a look at Getting started with Action Mailer in Ruby on Rails.

I haven’t set the @url variable yet as we don’t have the confirmation routes. I’ll come back to this when we implement this functionality.

Adding the User Mailer to the User Create Operation

With the User Mailer class generated and ready to go we can update the User::Create::Operation::Default class to automatically send a confirmation email when a new user is saved:

class Default < Base
  contract User::Create::Contract::Default

  def process(params)
    validate(params[:user]) do |f|
      generate_digest
      generate_token
      f.save
      send_confirmtion_email
    end
  end

  def send_confirmtion_email
    UserMailer.confirm_membership(model).deliver_later
  end
end

I will also update the test to ensure that the email is sent when a new user is created with this Operation:

test 'create default user' do
  res, op = User::Create::Operation::Default.run(user: attributes_for(:user))

  mail = ActionMailer::Base.deliveries.last

  assert(res)
  assert_not(op.model.confirmed?)
  assert_not(op.model.imported?)
  assert_equal(op.model.email, mail[:to].to_s)
  assert_equal('Confirm your Culttt Membership!', mail[:subject].to_s)
end

Here I’m getting the last email that was sent via Action Mailer and then asserting that the email and subject line are correct.

Building out the Operation

Next up we need to build out the Operation that will accept the POST request from the user when a confirmation email is requested.

The POST request will contain an email address that should be a registered, but unconfirmed email address that exists in the database.

This Operation is a bit different to the Operations we’ve seen in previous tutorials because we are not creating or updating a model:

module Confirmation::Request
  class Operation < Trailblazer::Operation
    contract do
      undef persisted?
      attr_reader :user

      property :email, virtual: true

      validate :email_is_unconfirmed
      validates :email, presence: true, email: true

      def email_is_unconfirmed
        @user = User.find_by(email: email, confirmed_at: nil)

        errors.add(:email, :not_found) unless @user
      end
    end

    def process(params)
      validate(params[:user]) do |f|
        UserMailer.confirm_membership(contract.user).deliver_later
      end
    end
  end
end

As you can see from this Operation, it’s a bit different from what we’ve seen so far. In this Operation, we need to find the user by their email address, but only if the user is not confirmed.

If the user is found, we can use the Mailer class from earlier to dispatch the confirmation email.

Here are the tests for this Operation:

require 'test_helper'

module Confirmation::Request::OperationTest
  class RequestCase < ActiveSupport::TestCase
    test 'require email' do
      res, op = Confirmation::Request::Operation.run(user: {})

      assert_not(res)
      assert_includes(op.errors[:email], "can't be blank")
    end

    test 'require valid email' do
      res, op =
        Confirmation::Request::Operation.run(user: { email: 'invalid email' })

      assert_not(res)
      assert_includes(op.errors[:email], 'is invalid')
    end

    test 'require unconfirmed email' do
      res, op =
        Confirmation::Request::Operation.run(user: { email: 'name@domain.com' })

      assert_not(res)
      assert_includes(op.errors[:email], 'not found')
    end

    test 'ignore confirmed users' do
      @user =
        User::Create::Operation::Confirmed.(user: attributes_for(:user)).model

      res, op =
        Confirmation::Request::Operation.run(user: { email: @user.email })

      assert_not(res)
      assert_includes(op.errors[:email], 'not found')
    end

    test 'send confirmation email' do
      @user =
        User::Create::Operation::Default.(user: attributes_for(:user)).model

      res, op =
        Confirmation::Request::Operation.run(user: { email: @user.email })

      mail = ActionMailer::Base.deliveries.last

      assert(res)
      assert_equal(@user.email, mail[:to].to_s)
      assert_equal('Confirm your Culttt Membership!', mail[:subject].to_s)
    end
  end
end

As you can see, these tests are pretty similar to the tests we’ve written in previous weeks.

First I write a test for each business rule that I want to enforce, and then finally I write a test to confirm the happy path.

Creating the Cell

As with last week’s tutorial (Getting started with Trailblazer Cells) I’m going to be encapsulating the form for this functionality in a Cell:

module Confirmation::Request
  class Cell < Culttt::Cells::Form
    def show
      render
    end
  end
end

Once again I’m extending Culttt::Cells::Form from last week’s tutorial. This is also a really basic example of a Cell class, and so there isn’t any additional logic that is required in the class itself.

I will also need the template file:

- if form.errors.any? ul - form.errors.full_messages.each do |msg| li = msg =
form_tag confirmation_request_url, class: "confirmation-request-form" do |f| div
= label_tag :email, nil, class: "qa-email-label" = text_field_tag :email, nil,
class: "qa-email-input" div = submit_tag "Request Confirmation", class:
"qa-submit"

This is your typical Rails form using the form_tag method, rather than the usual form_for method. As we’re using a Cell to render this form, this file lives under the concept directory.

I will also include some simple assertions to ensure that all of the correct form fields are present in the Cell template:

require 'test_helper'

module Confirmation::Request::CellTest
  class CellTest < Cell::TestCase
    controller Confirmation::RequestController

    test 'has correct markup' do
      html =
        concept(
          'confirmation/request/cell',
          Confirmation::Request::Operation.present({})
        ).()

      html.must_have_selector('form.confirmation-request-form')
      html.must_have_selector('label.qa-email-label')
      html.must_have_selector('input.qa-email-input')
      html.must_have_selector('input.qa-submit')
    end
  end
end

If you missed the setup to this kind of testing from Getting started with Trailblazer Cells, I would recommend that you go back and take a look. I really love how easy it is to test Cells in isolation.

Adding Controller Tests

With all of the functionality in place, we can add some Controller tests to set through the process and ensure everything is working as it should:

require 'test_helper'

module Confirmation
  class RequestControllerTest < ActionController::TestCase
    test 'display form' do
      get :new

      assert_response(:success)
    end

    test 'fail with invalid email' do
      post :create, user: { email: 'invalid email' }

      assert_response(400)
    end

    test 'fail with not found email' do
      post :create, user: { email: 'name@domain.com' }

      assert_response(400)
    end

    test 'fail with confirmed email' do
      @user =
        User::Create::Operation::Confirmed.(user: attributes_for(:user)).model

      post :create, user: { email: @user.email }

      assert_response(400)
    end

    test 'send confirmation email' do
      @user =
        User::Create::Operation::Default.(user: attributes_for(:user)).model

      assert_difference 'ActionMailer::Base.deliveries.size' do
        post :create, user: { email: @user.email }
      end

      assert_response(302)
      assert_redirected_to(login_url)
    end
  end
end

Hopefully this reads well and you’re familiar with what we’re testing. I’m basically just walking through the business rules and checking that they fail. I don’t check the actual errors for each case because I’m already check them at the Unit level.

In the final test I’m asserting that the request was successful and that the email was sent correctly.

Conclusion

Hopefully today’s tutorial was beneficial in that it was an example of where we’re using an Operation to perform a process that is not directly creating or updating a 1-to-1 resource.

Despite this we have still used many of the same techniques that we’ve been building up over the last couple of weeks.

Building out this functionality can often be boring. This kind of boiler plate code is basically the same for just about every type of web application.

But hopefully you can see how nice and simple Trailblazer makes it so you don’t have to worry about too much complexity.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.