Trouble with Capybara and Admin User permissions (and Devise) - ruby-on-rails-4

Devise for User registration and login.
Admins are set using the rails console (setting the admin boolean to true).
Rspec and FactoryGirl.
Unfortunately, I wrote my app before writing tests (the best lessons are learned the hard way). I am now learning rspec and writing a test suite for the app.
I have controller permissions and view permissions set up, for admins and non-admins, which DO work in practice (I know this through thorough browser manual testing).
In this case, I have an "Admin" link in the header which is displayed when a user is logged-in and is also an admin.
My StaticPagesController also has an effective before_action setup so no-one can access the admin page unless they are logged-in and are also an admin.
I wrote some tests for this, and thought I had it sorted, until I noticed that, when making changes to the specific /features spec file which contains these tests, guard runs only those tests and passes them. However, when I run the entire test suite, those same tests fail. I totally confused by this.
I think it may have something to do with Devise, but I just don't know.
spec/features/user_and_role_spec.rb
require 'rails_helper'
def manually_create_user
visit new_user_registration_path
fill_in('user_first_name', :with => 'Test')
fill_in('user_last_name', :with => 'User')
fill_in('user_email', :with => 'testuser#email.com')
fill_in('user_password', :with => 'testuser')
click_button('Sign up')
end
def create_user_and_login_as(type)
user = FactoryGirl.create(type)
visit(new_user_session_path)
fill_in('user_email', :with => user.email)
fill_in('user_password', :with => user.password)
click_button('Log in')
end
describe 'with users and roles' do
context "if user is not an admin" do
it "does not allow any user to visit the admin page if not logged-in" do
visit(admin_path)
expect(current_path).to eq(root_path)
end
it "does not allow a new user to visit the admin page" do
manually_create_user
visit(admin_path)
expect(current_path).to eq(root_path)
end
it "does not allow a student to visit the admin page" do
create_user_and_login_as(:student)
visit admin_path
expect(current_path).to eq(root_path)
end
it "does not allow a teacher to visit the admin page" do
create_user_and_login_as(:teacher)
visit admin_path
expect(current_path).to eq(root_path)
end
end
context "if user is an admin" do
it "allows an admin user to visit the admin page" do
create_user_and_login_as(:admin_user)
click_link 'Admin'
expect(current_path).to eq(admin_path)
end
it "allows a teacher_admin to visit the admin page" do
create_user_and_login_as(:teacher_admin_user)
click_link 'Admin'
expect(current_path).to eq(admin_path)
end
end
end
The tests in the context "if user is not an admin" all FAIL when running the full test suite. They all fail with the same error:
Failure/Error: expect(current_path).to eq(root_path)
expected: "/"
got: "/admin"
(compared using ==)
Which, to me, means the admin page was accessible, when it shouldn't have been. In my browser, the admin page link cannot be seen, neither can the page be accessed by manually typing in the url, unless the user is signed in and is an admin.
The tests in the context "if user is an admin" all PASS when running the full test suite.
spec/factories/users.rb:
require 'faker'
FactoryGirl.define do
factory :user do |f|
f.first_name { Faker::Name.first_name }
f.last_name { Faker::Name.last_name }
f.email { Faker::Internet.email }
f.password { Faker::Internet.password(8) }
f.admin false
trait :student do
type "Student"
end
trait :teacher do
type "Teacher"
end
trait :admin do
admin true
end
factory :admin_user, traits: [:admin]
factory :student, traits: [:student]
factory :teacher, traits: [:teacher]
factory :teacher_admin_user, traits: [:teacher, :admin]
end
end
static_pages_controller.rb:
class StaticPagesController < ApplicationController
before_action :admin?, only: [:admin]
def home
#testimonials = Testimonial.all
end
def admin
#groups = Group.all
#users = User.all
#students = Student.all
#teachers = Teacher.all
end
private
def admin?
unless signed_in? and current_user.admin == true
redirect_to root_path, notice: "You must be a signed-in admin to view this page"
end
end
end
static_pages_helper.rb:
module StaticPagesHelper
def allowed_to_see_admin_link?
signed_in? && current_user.admin
end
end
models/user.rb:
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
validates :first_name, presence: true
validates :last_name, presence: true
validates :admin, inclusion: { in: [true, false] }
scope :newest_first, -> { order("created_at DESC") }
scope :order_by_first_name, -> { order("first_name") }
def full_name
"#{first_name} #{last_name}"
end
def unassigned?
type != "Student" and type != "Teacher"
end
def can_view_materials?
admin || type == "Teacher" || type == "Student" && groups.any? # So only current students can view the Materials page.
end
def testimonial_owner?(testimonial)
id == testimonial.student_id
end
end
Relevant part of the _header.html.erb partial:
<ul class="nav navbar-nav navbar-right">
<% if allowed_to_see_admin_link? %>
<li><%= link_to "Admin", admin_path %></li>
<% end %>
</ul>
Gemfile:
gem 'rails', '4.2.0'
gem 'bootstrap-sass', '~> 3.3.3'
gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0'
gem 'coffee-rails', '~> 4.1.0'
gem 'jquery-rails'
gem 'turbolinks'
gem 'jbuilder', '~> 2.0'
gem 'sdoc', '~> 0.4.0', group: :doc
gem 'devise'
group :development, :test do
gem 'sqlite3'
gem 'byebug'
gem 'web-console', '~> 2.0'
gem 'spring'
gem 'better_errors'
gem 'binding_of_caller'
gem 'rspec-rails'
gem 'guard-rspec', require: false
gem 'factory_girl_rails'
end
group :test do
gem 'faker'
gem 'capybara'
gem 'launchy'
gem 'database_cleaner'
end
group :production do
gem 'pg'
gem 'rails_12factor'
end
users in the schema:
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.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "admin", default: false
t.string "type"
t.string "first_name"
t.string "last_name"
end
routes.rb:
resources :materials
root 'static_pages#home'
devise_for :users, :controllers => { registrations: 'registrations' }
get 'admin' => 'static_pages#admin'
resources :groups
resources :users
resources :students
resources :teachers
resources :testimonials
post 'assign_to_group' => 'students#assign_to_group' # Could have been 'patch', but default in the controller method is 'post', so I left the method as default and changed this route to 'post'. Doesn't NEED to be patch.
post 'remove_from_group' => 'students#remove_from_group'
post 'unassign_teacher' => 'groups#unassign_teacher'
post 'assign_as_student' => 'teachers#assign_as_student'
post 'assign_as_teacher' => 'students#assign_as_teacher'
post 'add_student' => 'groups#add_student'
post 'remove_student_from_group' => 'groups#remove_student_from_group'

The reason your tests are failing are that you are checking the current_path before the redirect has finished. Basically you're calling visit(xxx), which sets the current_path to xxx, and then immediately reading back xxx, while the server returns a redirect back to / and then the browser changes current_path to /. As long as you're using Capybara 2.5+ you should be using the have_current_path matcher which will retry for a bit, thereby giving the redirect time to be processed
expect(page).to have_current_path(root_path)

Related

Rename files before uploading to S3

I've got my rails application saving to AWS S3 the upload files that a user has selected. The users fills in a couple fields on the form relating to the file that they are about to upload. I need to use that information to construct the new file name.
This is the model:
class DocAttachment < ActiveRecord::Base
belongs_to :doc_attachment_type
belongs_to :language
has_attached_file :attachment
before_save :rename_file
#Attempted Paperclips callbacks but couldn't get values for the form
#after_attachment_post_process :rename_file
validates_attachment_presence :attachment
validates_attachment_content_type :attachment,
content_type: %w(application/pdf application/vnd.ms-excel application/vnd.openxmlformats-officedocument.spreadsheetml.sheet application/msword application/vnd.openxmlformats-officedocument.wordprocessingml.document text/plain),
size: { :in => 0..10.megabytes }
validates :doc_attachment_type_id, presence: true
validates :code, presence: true
validates :title, presence: true
validates :language_id, presence: true
def rename_file
extension = File.extname(attachment_file_name).gsub(/^\.+/, '')
filename = attachment_file_name.gsub(/\.#{extension}$/, '')
new_attachment_file_name = "#{self.code}-#{self.language.name}.#{extension}"
attachment.instance_write(:attachment_file_name, new_attachment_file_name)
end
end
Gemfile:
gem 'aws-sdk', '< 2.0'
gem 'paperclip'
I tried to use paperclips before/after callbacks but they didn't appear to provide me with the submitted form fields data. I like the idea of s3_direct_upload gem but I'm not sure if it will work because it doesnt appear to be in active development
Any help greatly appreciated.
Longer term i would like to Allow the user to do multiple uploads using AJAX with some kind of progress bar.
The issues was with the last line of the rename_file method.
def rename_file
extension = File.extname(attachment_file_name).gsub(/^\.+/, '')
filename = attachment_file_name.gsub(/\.#{extension}$/, '')
new_attachment_file_name = "#{self.code}-#{self.language.name}.#{extension}"
attachment.instance_write(:file_name, new_attachment_file_name)
end

RSpec, Devise - Could not find valid mapping error

I have a Devise User model and in my application I have different roles which I am specifying through an enum in my User model. When I am running the tests for the admin role, I am receiving the following error when running RSpec tests with Devise. I have tried some of the other answers to similar issues but nothing seems to be working. I hope you can point me in the right direction. Thanks!
RuntimeError:
Could not find a valid mapping for {:email=>"collin_cain#torpdoyle.info", :password=>"12345678", :password_confirmation=>"12345678", :role=>2}
Here is the User model:
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :comments
enum role: [:member, :moderator, :admin]
before_save :set_default_role
def set_default_role
self.role ||= 0
end
end
The user factory:
FactoryGirl.define do
factory :user do
email { Faker::Internet.email }
password "12345678"
password_confirmation "12345678"
role 0
end
end
The categories controller spec
require 'rails_helper'
RSpec.describe Admin::CategoriesController, type: :controller do
it 'should redirect to sign in path for non signed users' do
get :index
expect(response).to redirect_to(new_user_session_path)
end
it 'should redirect to root path for non admin users' do
user = create(:user)
sign_in user
get :index
expect(response).to redirect_to(root_path)
end
describe 'GET #index' do
context 'when admin signed in' do
it 'renders the index template' do
admin = attributes_for(:user, role: 2)
sign_in admin
get :index
expect(response).to render_template(:index)
end
it 'assigns a list of categories' do
admin = attributes_for(:user, role: 2)
sign_in admin
category = create(:category)
expect(assigns(:categories)).to eq([category])
end
end
end
end
and the routes file
Rails.application.routes.draw do
devise_for :users
namespace :admin do
get '', to: 'dashboard#index', as: '/'
resources :categories
end
resources :topics do
resources :comments, only: :create
end
resources :categories do
resources :topics
end
root 'categories#index'
end
I am also adding the User schema
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.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.inet "current_sign_in_ip"
t.inet "last_sign_in_ip"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "role"
t.string "image"
end
UPDATE:
I have updated the admin categories controller spec, specifically Devise's sign_in method from sign_in user to sign_in(:admin, user) as shown below.
describe 'GET #index' do
context 'when admin signed in' do
it 'renders the index template' do
user = create(:user)
user.role = 2
sign_in(:admin, user)
get :index
expect(response).to render_template(:index)
end
...
Now I am getting the following error
1) Admin::CategoriesController GET #index when admin signed in renders the index template
Failure/Error: expect(response).to render_template(:index)
expecting <"index"> but was a redirect to <http://test.host/users/sign_in>
For some reason the admin is not being signed in, I have included Devise Test Helpers in rails_helper.rb file, unfortunately the error continues. Any help will be greatly appreciated.
Have you declared the role in the migration like
t.integer :role
As this would need to be in there to be included in the migration created structure.
If not
Add the line, in your database drop the table and run your rake again
I was able to troubleshoot my own question and decided to post the answer in hope that it will help someone in the future.
Instead of setting the user role to admin in the the admin_categories_controller_spec file, instead I added a nested Factory inside the Users Factory.
FactoryGirl.define do
factory :user do
email { Faker::Internet.email }
password "12345678"
password_confirmation "12345678"
role 0
factory :admin do
role 2
end
end
end
and the test ends up like this:
describe 'GET #index' do
context 'when admin signed in' do
it 'renders the index template' do
admin = create(:admin)
sign_in admin
get :index
expect(response).to render_template(:index)
end
it 'assigns a list of categories' do
admin = create(:admin)
sign_in admin
category = create(:category)
get :index
expect(assigns(:categories)).to eq([category])
end
end
end

Undefined avatar method from Carrierwave in RoR application

I get this error message:
Completed 500 Internal Server Error in 5ms (ActiveRecord: 0.0ms)
NoMethodError (undefined method `avatar=' for #<User::ActiveRecord_Relation:0x007f87e4c304d8>):
app/controllers/api/v1/user_controller.rb:10:in `upload'
Model:
class User < ActiveRecord::Base
acts_as_token_authenticatable
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :confirmable
mount_uploader :avatar, AvatarUploader
validates_presence_of :avatar
validates_integrity_of :avatar
validates_processing_of :avatar
end
Controller:
module Api
module V1
class UserController < ApplicationController
before_action :set_user, only: [:show, :update, :destroy]
#before_filter :authenticate_user_from_token!
def upload
puts "here => " + params[:user][:email].to_s
#user = User.where(email: params[:user][:email])
#user.avatar = params[:user][:file]
#user.save!
p #user.avatar.url # => '/url/to/file.png'
p #user.avatar.current_path # => 'path/to/file.png'
p #user.avatar_identifier # => 'file.png'
end
...
environment.rb:
# Load the Rails application.
require File.expand_path('../application', __FILE__)
require 'carrierwave/orm/activerecord'
# Initialize the Rails application.
Rails.application.initialize!
The AvatarUploader was generated and the avatar:string column was added to the users table through the migration execution. I am not sure what's wrong with it.
Extra info: I use Rails: 4.2.4, Ruby: 2.2.1
Many thanks !
The error is pretty informative. When you call User.where(email: params[:user][:email]) you don't get a User object, you get an ActiveRecord_Relation object, wich can contain multiple ActiveRecord objects or be empty. To get a single User you want to use find_by instead of where, then you'll be able to get access to the avatar.

Devise::InvitationsController reports Unpermitted parameters

I have a problem with devise_invitable 1.4.0 and strong parameters when I add additional custom parameters and I really hope somebody can guide me in the right direction. I am able to send invitations, but when an invited user accepts an invitation and enters a desired username, maiden name, password and confirmed password, the following error is shown:
Processing by Users::InvitationsController#update as HTML
Unpermitted parameters: username, name
The user is created as expected, but the 'username' and 'name' columns in the database are empty.
I have tried all the suggestions I could find for related issues, but none of the worked. I have noticed that if I change the app/controllers/users/invitations_controller.rb file in any way (eg inserting a blank space on an empty line) without restarting the webserver (Thin) the problem disappears - but the problem reappears when the webserver is restarted.
The various relevant files look like this:
routes.rb:
Rails.application.routes.draw do
root to: 'visitors#index'
#Tell rails to use the Devise controllers that were generated with this command:
# > rails generate devise:controllers users
#Using these generated controllers allows us to overwrite anything in the deault controllers.
devise_for :users, :path_names => {:sign_in => 'login', :sign_out => 'logout'}, controllers: {confirmations: "users/confirmations", passwords: "users/passwords", registrations: "users/registrations", sessions: "users/sessions", unlocks: "users/unlocks", :invitations => 'users/invitations'}
resources :users
end
config/initializers/devise.rb
Devise.setup do |config|
...
...
config.scoped_views = true
config.authentication_keys = [ :username ]
...
...
end
app/controllers/users/invitations_controller.rb:
class Users::InvitationsController < Devise::InvitationsController
private
# this is called when creating invitation
# should return an instance of resource class
def invite_resource
## skip sending emails on invite
resource_class.invite!(invite_params, current_inviter) do |u|
u.tenant = current_inviter.tenant
u.role = :user
end
end
def after_invite_path_for(resource)
users_path
end
def resource_params
params.permit(user: [:name, :email,:invitation_token, :username])[:user]
end
end
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
around_filter :scope_current_tenant
before_filter :configure_permitted_parameters, if: :devise_controller?
if Rails.env.development?
# https://github.com/RailsApps/rails-devise-pundit/issues/10
include Pundit
# https://github.com/elabs/pundit#ensuring-policies-are-used
# after_action :verify_authorized, except: :index
# after_action :verify_policy_scoped, only: :index
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
end
#############################################################################
private
#############################################################################
if Rails.env.development?
def user_not_authorized
flash[:alert] = "Access denied." # TODO: make sure this isn't hard coded English.
redirect_to (request.referrer || root_path) # Send them back to them page they came from, or to the root page.
end
end
def current_tenant
#current_tenant ||= current_user.tenant unless current_user.nil?
end
helper_method :current_tenant
def scope_current_tenant(&block)
if current_tenant.nil?
scope_visitor_schema
yield
else
current_tenant.scope_schema("public", &block)
end
end
def scope_visitor_schema()
original_search_path = ActiveRecord::Base.connection.schema_search_path
ActiveRecord::Base.connection.schema_search_path = 'public'
ensure
ActiveRecord::Base.connection.schema_search_path = original_search_path
end
#############################################################################
protected
#############################################################################
def configure_permitted_parameters
# Only add some parameters
devise_parameter_sanitizer.for(:account_update).concat [:name, :email]
# Override accepted parameters
devise_parameter_sanitizer.for(:accept_invitation) do |u|
u.permit(:name, :username, :password, :password_confirmation,
:invitation_token)
end
end
end
app/models/user.rb:
class User < ActiveRecord::Base
enum role: [:user, :admin]
after_initialize :create_tenant, :if => :new_record?
belongs_to :tenant
# has_many :invitations, :class_name => self.to_s, :as => :invited_by
scope :unconfirmed, -> { where(confirmed_at: nil) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
# validate :username, presence: true, uniqueness: true, format: { with: /[a-zA-Z0-9]{4,20}/ }
def displayed_username
username.nil? ? "N/A" : username
end
def displayed_name
name.nil? ? "N/A" : name.titleize
end
def create_tenant
#The create_tenant method will also be called when looking up a user,
#so the following ensures a tenant is only created if it does not already
#exist - and the user has not been invited and assigned to an existing tenant:
if self.tenant.nil?
#Set role to 'admin' if a tenant is about to be created:
self.role = :admin #if self.tenant.nil?
self.tenant = Tenant.new
end
end
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :invitable, :database_authenticatable, :registerable, :confirmable,
:recoverable, :rememberable, :trackable, :validatable
end
I finally found a fix, which was to place the parameter sanitizer directly in users/invitations_controller.rb instead of the application_controller.rb.
class Users::InvitationsController < Devise::InvitationsController
before_filter :configure_permitted_parameters, if: :devise_controller?
private
def configure_permitted_parameters
devise_parameter_sanitizer.for(:accept_invitation) do |u|
u.permit(:username, :name, :email, :password, :password_confirmation, :invitation_token)
end
end
end

devise 3.4.1 with multiple usernames and reset password forbiddenAttributesError

I have multiple possible user names in my user model i.e. a user can login with either one.
This is based on the article here: https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-sign-in-using-their-username-or-email-address
The actual login is working fine with either code, but following the example I cannot get the reset password page to fire properly.
This is a multi-tenant scenario so there is also an account_id to take into consideration.
I have found many other people with the same issue - but I don't know what the right way to resolve this is, if I permit! in the relevant method (below) then reset password functions but standard login no longer works.
I am getting a ForbiddenAttributesError as follows:
ActiveModel::ForbiddenAttributesError - ActiveModel::ForbiddenAttributesError:
activemodel (4.1.6) lib/active_model/forbidden_attributes_protection.rb:21:in `sanitize_for_mass_assignment'
activerecord (4.1.6) lib/active_record/relation/query_methods.rb:568:in `where!'
activerecord (4.1.6) lib/active_record/relation/query_methods.rb:559:in `where'
activerecord (4.1.6) lib/active_record/querying.rb:10:in `where'
app/models/user.rb:356:in `find_first_by_auth_conditions'
devise (3.4.1) lib/devise/models/authenticatable.rb:266:in `find_or_initialize_with_errors'
devise (3.4.1) lib/devise/models/recoverable.rb:115:in `send_reset_password_instructions'
devise (3.4.1) app/controllers/devise/passwords_controller.rb:13:in `create'
User.rb
devise :database_authenticatable, :recoverable, :rememberable, :trackable, authentication_keys: [:login, :account_id], reset_password_keys: [:login, :account_id]
....
def self.find_first_by_auth_conditions(warden_conditions)
# IF i switch these 2 lines then reset password works BUT normal login doesn't
#conditions = warden_conditions.dup.permit!
conditions = warden_conditions.dup
if login = conditions.delete(:login)
where(conditions).where(["lower(remote_id) = :value OR lower(other_id) = :value", { :value => login.downcase }]).first
else
where(conditions).first
end
end
application_controller.rb
before_action :configure_permitted_parameters, if: :devise_controller?
...
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:remote_id, :email, :password, :password_confirmation, :remember_me) }
devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:login, :remote_id, :email, :password, :remember_me) }
devise_parameter_sanitizer.for(:account_update) { |u| u.permit(:remote_id, :email, :password, :password_confirmation, :current_password) }
end
Using devise 3.4.1, rails 4.1.6