We will be working from the source code found here.
# Clone and change into the project
$ git clone https://github.com/okeeffed/demo-rails-7-with-devise-series
$ cd demo-rails-7-with-devise-series
# Continue from part 6 - warning that Redis required
$ git checkout 6-recaptcha
# We will use rspec-rails to test our controllers and auth
$ bundler add rspec-rails --group="development,test"
# We'll use FactoryBot for testing
$ bundler add factory_bot_rails --group "development,test"
# Setup RSpec
$ bin/rails g rspec:install
# Create a document controller without a view
$ bin/rails g controller documents --skip-template-engine
# Generate our Document model
$ bin/rails g model Document body:string
# Create spec for testing the document
$ mkdir -p spec/controllers
# Create a test file for our spec
$ touch spec/controllers/documents_controller_spec.rb
# Make User factory
$ mkdir -p spec/factories
$ mkdir -p spec/support
$ touch spec/support/factory_bot.rb
# Create the user factory and document factory
$ touch spec/factories/user_factory.rb spec/factories/document_factory.rb
# Configuring Devise for RSpec
$ touch spec/support/controller_macros.rb
In the above setup, we installed the required gems and the scaffold out a lot of the files and folders that we will need for setting up our tests.
We also add a Document model with a simple body type.
Setting up our many-to-many relationship
We want to actually make the document and user models a many-to-many relationship, so let's tackle that next:
We need to update both the Document and User models.
For Documents, it should now look like this:
class Document < ApplicationRecord
has_and_belongs_to_many :users
end
For Users:
class User < ApplicationRecord
has_and_belongs_to_many :documents
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, :omniauthable, omniauth_providers: %i[github]
def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
user.email = auth.info.email
user.password = Devise.friendly_token[0, 20]
# user.name = auth.info.name # assuming the user model has a name
# user.image = auth.info.image # assuming the user model has an image
# If you are using confirmable and the provider(s) you use validate emails,
# uncomment the line below to skip the confirmation emails.
# user.skip_confirmation!
end
end
end
Configure the documents controller routes
Next, we need to update the config/routes.rb file for our new document endpoints:
Rails.application.routes.draw do
devise_for :users,
controllers: { omniauth_callbacks: 'users/omniauth_callbacks', sessions: 'users/sessions',
registrations: 'users/registrations' }
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Defines the root path route ("/")
resources :users
resources :home, only: %i[index create]
resources :session, only: [:index]
# ADD HERE
resources :documents, only: %i[index create update destroy]
root 'home#index'
end
Configuring the Rails helper
In the spec/rails_helper.rb file, uncomment the line Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } as well as include our required helpers.
It should look like the following:
# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
# Prevent database truncation if the environment is production
abort('The Rails environment is running in production mode!') if Rails.env.production?
require 'rspec/rails'
# ... omitted ...
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
# ... omitted ...
RSpec.configure do |config|
# ... omitted ...
config.include Devise::Test::ControllerHelpers, type: :controller
config.extend ControllerMacros, type: :controller
end
Note: I omitted the unchanged code above.
Setting up our factories
We will setup two factories:
A User factory.
A Document factory.
For spec/factories/document_factory.rb:
FactoryBot.define do
factory :document do
body { 'Hello, world' }
end
end
For spec/factories/user_factory.rb:
FactoryBot.define do
factory :user do
id { 2 }
email { 'hello@example.com' }
password { 'password123' }
end
end
Next is setting up our helpers for logging users in for devise.
Configuring the spec support files
The spec/support/controller_macros.rb will have a helper to login in a user login_user and looks like this:
module ControllerMacros
def login_user
# Before each test, create and login the user
before(:each) do
@request.env['devise.mapping'] = Devise.mappings[:user]
sign_in FactoryBot.create(:user)
end
end
end
Finally, we can add a support file for FactoryBot in the spec/support/factory_bot.rb file:
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
This could be added to the rails_helper.rb config file instead too if you wanted.
Filling in the DocumentController code
Inside of the app/controllers/documents_controller.rb file, fill in the following:
class DocumentsController < ApplicationController
def create
@doc = Document.new(body: params[:body])
@doc.save!
render json: @doc, status: :created
end
def index
@docs = Document.all
render json: @docs
end
def update
@doc = Document.find(params[:id])
@doc.update!(document_params)
render json: @doc
end
def destroy
@doc = Document.find(params[:id])
@doc.destroy
render status: :no_content
end
end
Writing our test
Finally, we can write a test to check that user is logged in before they can access the document.
In spec/controllers/documents_controller_spec.rb:
require 'rails_helper'
RSpec.describe DocumentsController, type: :controller do
describe 'GET #index' do
let(:subject) { create(:document) }
context 'successful responses' do
login_user
it 'returns all posts when user is authorized' do
get :index, params: { body: subject.body }
expect(response.status).to eq(200)
expect(response.parsed_body).to eq([subject.as_json])
end
end
context 'unsuccessful responses' do
it 'redirects user when they are not logged in' do
get :create, params: { body: subject.body }
expect(response.status).to eq(302)
end
end
end
end
Our first test expects us to return an array of all documents (which we create one using factory bot).
The second test will check that we are redirected to the sign-in page (as that is our current behavior in the app).
We can now run our tests to see if they run as expected:
# Run RSpec
$ bundler exec rspec
..
Finished in 0.15058 seconds (files took 2.37 seconds to load)
2 examples, 0 failures
Success!
Summary
In this part of the series, we demonstrated how we can begin writing tests that requirements around a user login when interacting with our document controller.
The next part in the series will move on from authentication to authorization and how we can use the Pundit gem to do just that.