Halt! Who goes there!

It’s a sad commentary on society that we can’t publish a blogging platform to the internet without bad people trying to post spam and mess it up for everyone. Let’s add a sign in form.

I like the Sorcery gem because it just quietly does its thing without imposing its opinions on registration flow like Devise does.

From the Sorcery readme:

// Gemfile
gem 'sorcery'
bundle install
rails generate sorcery:install
rails db:migrate

That will have created a User model for us.

class User < ApplicationRecord
  authenticates_with_sorcery!
end

We’ll need a SessionsController for the sign in method.

rails g controller Sessions

I think it’s interesting to show the tests for this. It can be a mini-tutorial for folks who have never used Sorcery before and need help writing integration tests with it.

require "test_helper"

class SessionsControllerTest < ActionDispatch::IntegrationTest
  setup do
    @sally = User.create! email: 'sally@example.com',
                          password: 'letmein'
  end

  test 'sign in renders a form' do
    get sign_in_url
    assert_response :success

    assert_select 'form' do
      assert_select '#credentials_email'
      assert_select '#credentials_password'
      assert_select 'input.submit'
    end
  end

  test 'redirect to home page after sign in' do
    post sessions_url, params: {
        credentials: {
            email: 'sally@example.com',
            password: 'letmein',
        }
    }
    assert_redirected_to root_url
  end

  test 'rerender the form if sign in fails' do
    post sessions_url, params: {
        credentials: {
            email: 'sally@wrong.com',
            password: 'wrong',
        }
    }
    assert_response :success

    assert_select '.form' do
      assert_select '.error', 'Invalid Login'
      assert_select '#credentials_email' do |elements|
        assert_equal 'sally@wrong.com', elements[0][:value]
      end
    end
  end
end
// routes.rb

  resources :sessions
  get :sign_in,  to: 'sessions#new', as: :sign_in
class SessionsController < ApplicationController
  def new
  end

  def create
    credentials = credential_params

    login(credentials[:email], credentials[:password]) do |user, failure|
      if failure
        @email = credentials[:email]
        @error = failure
        render action: 'new'
      else
        redirect_back_or_to(:root)
      end
    end
  end

  private
  def credential_params
    params.
        require(:credentials).
        permit(:email, :password)
  end
end

The view is pretty simple but it took me a while to remember how to write ERB. I usually use HAML as it’s so much easier to read and write.

-# views/sessions/new.html.erb
<div class='session-page'>
  <div class='session-panel'>
    <h1>Sign in</h1>

    <%= form_for :credentials, url: {action: :create}, html: {class: 'form'} do |form| %>
      <div class='error'>
        <%= t @error %>
      </div>
      <div class='input email'>
        <%= form.label :email %>
        <%= form.email_field :email,
                             value: @email,
                             required: true,
                             autofocus: true,
                             placeholder: 'Enter your email address'
        %>
      </div>
      <div class='input password'>
        <%= form.label :password %>
        <%= form.password_field :password, 
                                required: true, 
                                placeholder: 'Enter your password' %>
      </div>
      <div class='actions'>
        <%= form.submit 'Sign in', class: 'button submit primary' %>
      </div>
    <% end %>
  </div>
</div>

That test passes and a little bit of CSS magic gives me this

I’d like the sign in to be in React someday but it’ll be a distraction for now and I’ll come back to it later. No one said that all my UI must be in React anyway.

If I create a user in the Rails console, I’ll be able to sign in.

User.create! email: 'sally@example.com', password: 'letmein'

Now we can protect the important controller actions. I find it safest to require login on everything and to then make exceptions for the public-facing actions.

class ApplicationController < ActionController::Base
  before_action :require_login

class PostsController < ApplicationController
  skip_before_action :require_login, only: [:index, :show]

class SessionsController < ApplicationController
  skip_before_action :require_login

If I run all the Rails tests now, I see a failure.

PostsControllerTest#test_create_a_blog_post:
Expected response to be a <2XX: success>, 
but was a <302: Found> redirect to <http://www.example.com/>
Response body: <html><body>You are being <a href="http://www.example.com/">redirected</a>.</body></html>

I can update the test to sign in first.

  test 'create a blog post requires a sign in' do
    post posts_url(format: :json), params: {
        post: {
            title: 'A new day',
            body: 'First post!'
        }
    }
    assert_redirected_to root_url
  end

  test 'create a blog post' do
    sign_in @sally
    post posts_url(format: :json), params: {
        post: {
            title: 'A new day',
            body: 'First post!'
        }
    }
    assert_response :success
    # ...
  end 

The sign_in method exists in a test helper:

module TestHelpers
  module AuthenticationTestHelper
    def sign_in user, options={}
      password = options[:password] || 'letmein'
      post sessions_url,
           params: {
               credentials: {
                   email:    user.email,
                   password: password
               },
           }
    end
  end
end

It’s a bit of faffing to set up a test helper that is accessible to all the tests, but it make them more readable if you do.

# test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require_relative "../config/environment"
require "rails/test_help"
Dir['test/test_helpers/*.rb'].each {|file| require file.sub(/^test\//, '') }

class ActiveSupport::TestCase
  parallelize(workers: :number_of_processors)
  fixtures :all
end

class ActionDispatch::IntegrationTest
  include TestHelpers::JsonTestHelper
  include TestHelpers::AuthenticationTestHelper
end

While I am here, I’ll create a test helper for JSON testing too. That’s been bothering me for a while. I just updated the test above to use it.

module TestHelpers
  module JsonTestHelper
    def json_response options={}
      assert_response options[:expected_response] || :success
      JSON.parse response.body, symbolize_names: true
    end
  end
end

The last thing left to do is to hide the post Editor if the user is not signed in. There are a bunch of ways we can send the current user to the React client. The easiest, for now, is to add a data property to the div that contains the React application. We can do something more sophisticated later.

<div id='react' data-user-id='<%= current_user&.id%>' />

We can read this property from Javascript and store it somewhere where it is accessible from any component.

I’m sure this will get more complex later but let’s keep it simple for now.

// users/current_user.js
const current_user = {
  id: null,
}

export function signIn(id=true) {
  current_user.id = id
}

export function signOut() {
  current_user.id = null
}

export function signedIn() {
  return current_user.id
}

We can read the ID and store it when the app loads.

// application/index.jsx
document.addEventListener('DOMContentLoaded', () => {
  const react = document.querySelector('#react')
  signIn(react.getAttribute('data-user-id'))
  // ...

Now we can call the signedIn() method from the editor and render nothing if the user is not signed in.

    if(!signedIn()) return null

We can also call signIn() and signOut() in our tests.

Tests are code too

I’m going to do an experiment on a branch to see if I can make the interface for testing a little easier to read. I want something a bit more like assert_select in Rails.

Let’s see now. Pretending it’s magic…

  test('shows the first blog post', async () => {
    const component = await mount(<Application />)
    
    check(component).has(
        {
          '.site-name':   'Blogging', 
          '.post .title': 'React on Rails', 
          '.post .body':  'I can use React with Rails.', 
        }
    )
  })

Or, how about this?

  test('also shows the first blog post', async () => {
    const component = await mount(<Application />)
    assert_select(component, '.site-name',   'Blogging')
    assert_select(component, '.post .title', 'React on Rails')
    assert_select(component, '.post .body',  'I can use React with Rails.')
  })

assert_select in Rails allows nesting. Maybe something like this?

 test('shows the first blog post with nesting', async () => {
    const component = await mount(<Application />)

    assert_select(component, '.site-name',   'Blogging')
    assert_select(component, '.post', match => {
      assert_select(match, '.title', 'React on Rails')
      assert_select(match, '.body', 'I can use React with Rails.')
    })
  })

Let’s try the simple (non-nested) version for a while to see how it feels. Here’s a quick implementation.

export function assert_select(component, selector, expectation=1) {
  const selected = component.find(selector)
  switch(typeof(expectation)) {
    case 'string':
      expect(selected.text()).toEqual(expectation)
      break
    case 'number':
      expect(selected.length).toEqual(expectation)
      break
    default:
      expect(expectation).toEqual('string or number')
      break
  }
}

If the expectation is a string, we’ll see if text matches. If it’s a number, we’ll check the number of matches. If you skip the expectation, we’ll just check that there is a match.

Let’s see how it goes. I’ll extract this into a ReactHelper along with a wrapper function for mount() that I will call display().

// test/javascript/helpers/ReactHelper.jsx

import {configure, mount} from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'

configure({ adapter: new Adapter() })

export function display(component) {
  return mount(component)
}

This hides away the fiddly code to initialise enzyme and (SPOILERS) it also provides a handy place to initialise global state should we need that later.

I’ve updated all the tests to use assert_select and the setup code in ReactHelper.

Tidy is good.