I’m building a Ruby on Rails application from a Test Driven Development approach. I wrote a RSpec test which tests User authentication & Authorization based on their role. I used gems like Devise, Capybara, Launcy e.t.c In my case, the User when trying to Sign-up gets a default role set to “client”
Here is the RSpec test:
require 'rails_helper'
RSpec.configure do |config|
config.include EmailHelper, type: :feature
end
RSpec.feature 'UserAuths', type: :feature do
scenario 'user signs up as a client' do
visit new_user_registration_path
fill_in 'First name', with: 'John'
fill_in 'Last name', with: 'Doe'
fill_in 'Contact number', with: '1234567890'
fill_in 'Address', with: '123 Main St'
fill_in 'Username', with: 'johndoe'
fill_in 'Email', with: '[email protected]'
fill_in 'Password', with: 'password'
fill_in 'Password confirmation', with: 'password'
click_button 'Sign up'
expect(page).to have_content 'Welcome! You have signed up successfully.'
expect(User.count).to eq(1)
end
end
and here is my User Model:
class User < ApplicationRecord
ROLES = %w[admin therapist client].freeze
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
validates :username, presence: true, uniqueness: true
validates :email, presence: true, uniqueness: true
enum role: { admin: 'admin', therapist: 'therapist', client: 'client' }, _suffix: true
attribute :role, :string, default: 'client'
ROLES.each do |name|
define_method "#{name}?" do
role == name
end
end
end
attribute :role, :string, default: 'client'
sets any user trying to Sign-up with role “client” and it is saved in the database. In my schema I have the following:
ActiveRecord::Schema[7.1].define(version: 2024_04_05_195810) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "role"
t.string "first_name"
t.string "last_name"
t.string "contact_number"
t.string "address"
t.string "username"
t.string "gender"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
end
When I run the Rspec test I keep getting this error:
UserAuths user signs up as a client
Failure/Error: expect(page).to have_content 'Welcome! You have signed up successfully.'
expected to find text "Welcome! You have signed up successfully." in "Sign upn1 error prohibited this user from being saved:nUsername can't be blanknFirst namenLast namenContact numbernAddressnUsernamenEmailnPassword (6 characters minimum)nPassword confirmationnLog innLog in Other users Log in as Admin Log in as Therapist"
# ./spec/features/user_auth_spec.rb:35:in `block (2 levels) in <top (required)>'
Here is the content of my new.html.erb:
<h2>Sign up</h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="field">
<%= f.label :first_name %><br />
<%= f.text_field :first_name, autofocus: true %>
</div>
<div class="field">
<%= f.label :last_name %><br />
<%= f.text_field :last_name %>
</div>
<div class="field">
<%= f.label :contact_number %><br />
<%= f.text_field :contact_number %>
</div>
<div class="field">
<%= f.label :address %><br />
<%= f.text_field :address %>
</div>
<% if resource.client? %>
<div class="field">
<%= f.label :username %><br />
<%= f.text_field :username %>
</div>
<% end %>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autocomplete: "email" %>
</div>
<div class="field">
<%= f.label :password %>
<% if @minimum_password_length %>
<em>(<%= @minimum_password_length %> characters minimum)</em>
<% end %><br />
<%= f.password_field :password, autocomplete: "new-password" %>
</div>
<div class="field">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
</div>
<div class="actions">
<%= f.submit "Sign up" %>
</div>
<% end %>
<%= render "devise/shared/links" %>
<%= form_for @user do |f| %>
<%= link_to 'Log in', new_user_session_path(role: 'client') %>
<%= f.label :Other_Users %><br />
<%= link_to 'Log in as Admin', new_user_session_path(role: 'admin') %>
<%= link_to 'Log in as Therapist', new_user_session_path(role: 'therapist') %>
<% end %>
<%= debug(resource.client?) %>
My Factorybot is setup like this as well:
FactoryBot.define do
factory :user do
first_name { Faker::Name.first_name }
last_name { Faker::Name.last_name }
contact_number { Faker::PhoneNumber.phone_number }
address { Faker::Address.full_address }
sequence(:username) { |n| "#{Faker::Internet.username}#{n}" }
email { Faker::Internet.email }
password { 'password' }
password_confirmation { 'password' }
gender { Faker::Gender }
trait :admin do
role { :admin }
end
trait :therapist do
role { :therapist }
end
trait :client do
role { :client }
end
end
end
My User controller is this:
class UsersController < ApplicationController
load_and_authorize_resource
before_action :authorize_admin!, only: %i[edit update]
def index
@users = User.all
end
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
redirect_to users_path, notice: 'User was successfully created.'
else
render :new
end
end
def edit
@user = User.find(params[:id])
end
def update
@user = User.find(params[:id])
if @user != current_user && @user.update(user_params)
flash[:notice] = 'User role updated successfully.'
redirect_to users_path
else
flash[:alert] = 'You are not authorized to perform this action.'
redirect_to root_path
end
end
def destroy
@user = User.find(params[:id])
@user.destroy
redirect_to users_path, notice: 'User was successfully destroyed.'
end
private
def authorize_admin!
if current_user.admin? && current_user == @user
redirect_to root_path, alert: 'Admin cannot change their own role.'
elsif current_user.admin?
# Admins can only edit other users' roles
nil
else
redirect_to root_path, alert: 'You are not authorized to perform this action.'
end
end
def user_params
params.require(:user).permit(:first_name, :last_name, :contact_number, :username, :address, :gender, :email, :password, :password_confirmation, :role)
end
end
My routes.rb looks like this as well:
Rails.application.routes.draw do
devise_for :users
root 'home#index'
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
# Can be used by load balancers and uptime monitors to verify that the app is live.
# get "up" => "rails/health#show", as: :rails_health_check
# Defines the root path route ("/")
resources :users, only: [:index, :edit, :update]
get '/admin_dashboard', to: 'dashboard#admin', as: 'admin_dashboard'
get '/therapist_dashboard', to: 'dashboard#therapist', as: 'therapist_dashboard'
get '/client_dashboard', to: 'dashboard#client', as: 'client_dashboard'
end
and my Registration controller is like this as well:
class Users::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
# before_action :configure_account_update_params, only: [:update]
protected
# If you have extra params to permit, append them to the sanitizer.
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, :contact_number, :username, :address, :gender, :email, :password, :password_confirmation, :role])
end
end
Here is my gem file as well:
source 'https://rubygems.org'
ruby '3.1.0'
gem 'cancancan'
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem 'rails', '~> 7.1.3', '>= 7.1.3.2'
# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
gem 'sprockets-rails'
# Use postgresql as the database for Active Record
gem 'pg', '~> 1.1'
# Search Functionality
gem 'pg_search'
# Use the Puma web server [https://github.com/puma/puma]
gem 'puma', '>= 5.0'
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem 'importmap-rails'
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem 'turbo-rails'
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem 'stimulus-rails'
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem 'jbuilder'
# Use Redis adapter to run Action Cable in production
# gem "redis", ">= 4.0.1"
# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
# gem "kredis"
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: %i[mswin mswin64 mingw x64_mingw jruby]
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"
group :development, :test do
gem 'capybara'
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem 'debug', platforms: %i[mri mswin mswin64 mingw x64_mingw]
gem 'factory_bot_rails'
gem 'faker'
gem 'rspec-rails'
gem 'selenium-webdriver'
end
group :development do
# Use console on exceptions pages [https://github.com/rails/web-console]
gem 'web-console'
# Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
# gem "rack-mini-profiler"
# Speed up commands on slow machines / big apps [https://github.com/rails/spring]
# gem "spring"
gem 'error_highlight', '>= 0.4.0', platforms: [:ruby]
end
group :test do
gem 'database_cleaner'
gem 'shoulda-matchers', '~> 6.2'
gem 'warden', '~> 1.2'
end
gem 'hotwire-rails', '~> 0.1.3'
gem 'tailwindcss-rails', '~> 2.3'
gem 'view_component', '~> 3.11'
gem 'devise', '~> 4.9'
gem 'launchy', '~> 3.0'
I have tried debugging from the HTML page of the sign-up to no avail. I have also tried other solutions from StackOverflow speaking of this issue but none seems to be working for me.
The HTML markup shows that the username field exists but it’s not being filled after using all manner of fill_in
and this:
if user_role == 'client'
fill_in 'Username', with: 'johndoe'
end
as well as fill_in_field('Username', with: 'johndoe')
and fill_in_field_with_placeholder('Username', with: 'johndoe')
The Username field exists in the HTML markup of the Sign-up page so I don’t know why Capybara is not filling it. Any pointers as to how I can solve this? By the way, I’m using Rails 7 and Ruby version 3.1.0.