I have a helper for ensuring that a user has permission to view a page, and redirects if the user doesn't have permission:
module PermissionsHelper
def require_permission(permission_attribute_name)
return if current_or_guest_user.role.send(permission_attribute_name)
redirect_to current_or_guest_user.role.landing_page,
notice: 'You do not have sufficient permissions'
end
end
The method current_or_guest_user is a method I use in another helper that returns current user, or creates and returns a guest if there is no current user.
My spec looks like this:
require 'rails_helper'
RSpec.describe PermissionsHelper, type: :helper do
describe 'requiring permissions' do
let(:test_user) { create :customer }
it "redirects the user to the user's landing page if the user doesn't have permission" do
allow(helper).to receive(:current_or_guest_user) { test_user }
require_permission(:view_admins)
expect(response).to redirect_to test_user.landing_page
end
end
end
And I am getting this error:
PermissionsHelper
requiring permissions
redirects the user to the user's langing page if the user doesn't have permission (FAILED - 1)
Failures:
1) PermissionsHelper requiring permissions redirects the user to the user's langing page if the user doesn't have permission
Failure/Error: require_permission(:view_admins)
NameError:
undefined local variable or method `current_or_guest_user' for #<RSpec::ExampleGroups::PermissionsHelper::RequiringPermissions:0x007fb7b430afe8>
# /Users/user/.rvm/gems/ruby-2.2.2#project/gems/actionpack-4.2.1/lib/action_dispatch/testing/assertions/routing.rb:171:in `method_missing'
# /Users/user/.rvm/gems/ruby-2.2.2#project/gems/actionview-4.2.1/lib/action_view/test_case.rb:271:in `method_missing'
# ./app/helpers/permissions_helper.rb:3:in `require_permission'
# ./spec/helpers/permissions_helper_spec.rb:11:in `block (3 levels) in <top (required)>'
# /Users/user/.rvm/gems/ruby-2.2.2#project/gems/rspec-retry-0.4.0/lib/rspec/retry.rb:43:in `block (3 levels) in apply'
# /Users/user/.rvm/gems/ruby-2.2.2#project/gems/rspec-retry-0.4.0/lib/rspec/retry.rb:34:in `times'
# /Users/user/.rvm/gems/ruby-2.2.2#project/gems/rspec-retry-0.4.0/lib/rspec/retry.rb:34:in `block (2 levels) in apply'
Finished in 0.01188 seconds (files took 1.88 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/helpers/permissions_helper_spec.rb:9 # PermissionsHelper requiring permissions redirects the user to the user's langing page if the user doesn't have permission
EDIT: I tried changing allow to allow_any_instance_of and now I get this error:
With this spec:
it "redirects the user to the user's langing page if the user doesn't have permission" do
allow_any_instance_of(helper).to receive(:current_or_guest_user) { test_user }
require_permission(:view_admins)
expect(response).to redirect_to test_user.landing_page
end
I get:
Failure/Error: allow_any_instance_of(helper).to receive(:current_or_guest_user) { test_user }
NoMethodError:
undefined method `ancestors' for #<#<Class:0x007fe5d19a9a98>:0x007fe5d19a1668>
allow
will only work with static methods. Since the method in the helper is an instance method, you instead will want to use
allow_any_instance_of
https://www.relishapp.com/rspec/rspec-mocks/v/2-14/docs/message-expectations/allow-a-message-on-any-instance-of-a-class
EDIT:
The below snippet fails in one of my projects (using the 'helper' variable)
allow_any_instance_of(helper).to receive(:paginate).and_return('asdf')
However, using the full class name works fine (for the purpose of testing method mocking at least)
allow_any_instance_of(PaginationHelper).to receive(:paginate).and_return('asdf)
Related
I have a user which I'm creating with FactoryGirl which needs to have a company in order to login successfully to my root_url.
I'm not having any luck at all stubbing the user method to login. I've followed this tutorial for the Devise portion of the user and needed to amend it a little since my user also requires a company to be associated to it.
I've now created a new model/controller called Scans that is behind Devise's authenticate filter and my first pass at testing it failing with:
5) ScansController GET #show returns http success
Failure/Error: expect(response).to have_http_status(:success)
expected the response to have a success status code (2xx) but it was 302
# ./spec/controllers/scans_controller_spec.rb:32:in `block (3 levels) in <top (required)>'
# ./spec/spec_helper.rb:127:in `block (3 levels) in <top (required)>'
# ./spec/spec_helper.rb:126:in `block (2 levels) in <top (required)>'
The spec is currently:
require 'rails_helper'
RSpec.describe ScansController, type: :controller do
before(:all) do
#user = build(:user)
#company = build(:company)
#device = build(:device)
#scan = build(:scan)
end
describe "GET #show" do
it "returns http success" do
login_with #user
get :show, :device_id => #device.id, :id => #scan.id
expect(response).to render_template(:show)
end
end
I'm doing a puts on the response, because I want to see what's being returned:
ScansController
GET #show
302
{"X-Frame-Options"=>"SAMEORIGIN", "X-XSS-Protection"=>"1; mode=block", "X-Content-Type-Options"=>"nosniff", "Location"=>"http://test.host/login", "Content-Type"=>"text/html; charset=utf-8"}
#<Rack::BodyProxy:0x007fb52a7407c0>
So, I'm being redirected back to my login page, which tells me that my login_with method in ControllerHelpers is not working correctly:
module ControllerHelpers
def login_with(user = double('user'), scope = :user)
current_user = "current_#{scope}".to_sym
if user.nil?
allow(request.env['warden']).to receive(:authenticate!).and_throw(:warden, {:scope => scope})
allow(controller).to receive(current_user).and_return(nil)
else
allow(request.env['warden']).to receive(:authenticate!).and_return(user)
allow(controller).to receive(current_user).and_return(user)
end
end
end
Now, my login functionality does currently work (testing manually). The first controller that fires after ApplicationController is PagesController#home:
def home
if current_user && current_user.company
verify_subscription
....
else
redirect_to new_company_path
end
end
If verify_subscription fails the user is also sent to new_company_path, so that doesn't seem to be related to this issue.
Based off my rudimentary rspec capabilities, am I right to assume that I'm not even getting close to mimicking a login? If not, what am I doing wrong?
After alot of tinkering I finally got my tests to pass. I ended up creating a company within my user Factory:
after(:build) do |user|
user.company = create(:company)
end
I converted an app from Rails 3.2 to 4.2 that was using the cancan gem. Googling around and checking on their github says it's enought to just replace gem cancan with gem cancancan without changing anything. This doesn't seem to work. My test fails:
Failures:
1) Redactable fields Manual redaction Redacting selected text in body
Failure/Error:
page.execute_script <<-EOS
document.getElementById('notice_#{#name}').focus();
document.getElementById('notice_#{#name}').select();
EOS
Capybara::Webkit::InvalidResponseError:
Javascript failed to execute
# /Users/siaw23/.rvm/gems/ruby-2.1.9/gems/capybara-webkit-1.11.1/lib/capybara/webkit/browser.rb:305:in `check'
# /Users/siaw23/.rvm/gems/ruby-2.1.9/gems/capybara-webkit-1.11.1/lib/capybara/webkit/browser.rb:211:in `command'
# /Users/siaw23/.rvm/gems/ruby-2.1.9/gems/capybara-webkit-1.11.1/lib/capybara/webkit/browser.rb:232:in `execute_script'
# /Users/siaw23/.rvm/gems/ruby-2.1.9/gems/capybara-webkit-1.11.1/lib/capybara/webkit/driver.rb:80:in `execute_script'
# /Users/siaw23/.rvm/gems/ruby-2.1.9/gems/capybara-2.7.1/lib/capybara/session.rb:524:in `execute_script'
# ./spec/support/page_objects/redactable_field_on_page.rb:15:in `select'
# ./spec/support/page_objects/redactable_field_on_page.rb:22:in `select_and_redact'
# ./spec/integration/redaction_spec.rb:37:in `block (4 levels) in <top (required)>'
2) Redactable fields Manual redaction Restoring body from original
Failure/Error: page.find("#notice_#{#name}").click
Capybara::ElementNotFound:
Unable to find css "#notice_body"
# /Users/siaw23/.rvm/gems/ruby-2.1.9/gems/capybara-2.7.1/lib/capybara/node/finders.rb:44:in `block in find'
# /Users/siaw23/.rvm/gems/ruby-2.1.9/gems/capybara-2.7.1/lib/capybara/node/base.rb:85:in `synchronize'
# /Users/siaw23/.rvm/gems/ruby-2.1.9/gems/capybara-2.7.1/lib/capybara/node/finders.rb:33:in `find'
# /Users/siaw23/.rvm/gems/ruby-2.1.9/gems/capybara-2.7.1/lib/capybara/session.rb:699:in `block (2 levels) in <class:Session>'
# ./spec/support/page_objects/redactable_field_on_page.rb:9:in `unredact'
# ./spec/integration/redaction_spec.rb:46:in `block (4 levels) in <top (required)>'
The elements being searched for can be reached manually after loggin in. This means neither cancancan nor cancan successfully applies :admin permission to my user that's created in the test. I used You are not authorized to access this page. and I get "You are not authorized to access this page."
The spec itself looks like this:
require 'rails_helper'
require 'support/notice_actions'
#lots of irrelevant code
private
def visit_redact_notice
user = create(:user, :admin)
visit '/users/sign_in'
fill_in "Email", with: user.email
fill_in "Password", with: user.password
click_on "Log in"
notice = create(:dmca, :redactable)
visit "/admin/notice/#{notice.id}/redact_notice"
notice
end
end
end
end
The visit_redact_notice method in the above spec is where everything is happening. I need help :). This is my repo if it might help: https://github.com/siaw23/lumendatabase
ability.rb
class Ability
include CanCan::Ability
def initialize(user)
return unless user
if user.has_role?(Role.submitter)
can :submit, Notice
end
if user.has_role?(Role.redactor)
grant_admin_access
grant_redact
end
if user.has_role?(Role.publisher)
grant_admin_access
grant_redact
can :publish, Notice
end
if user.has_role?(Role.admin)
grant_admin_access
grant_redact
can :edit, :all
cannot :edit, [User, Role]
can :publish, Notice
can :rescind, Notice
can :pdf_requests, :all
end
if user.has_role?(Role.super_admin)
can :manage, :all
end
if user.has_role?(Role.researcher)
can :read, Notice
end
end
def grant_admin_access
can :read, :all
can :access, :rails_admin
can :dashboard
can :search, Entity
can :access, :original_files
end
def grant_redact
can :edit, Notice
can :redact_notice, Notice
can :redact_queue, Notice
end
end
A couple issues you might want to check out...
There's a new version of Devise out that allows authentication via feature / integration tests. I've always spent hours debugging actually getting step-by-step login functionality to work. With Devise 4.2, logging in via integration tests is now supported. You'll have to make some changes to the RSpec configuration for Devise, as noted in the documentation above for the sign_in method to work.
If at this point logging in as an admin still does not work, I would check to see if your RSpec configuration has use_transactional_fixtures set to true. If it does, I would strongly encourage you to let the database_cleaner gem manage your test database, and set use_transactional_fixtures to false.
One or both of those should get you around your authentication issue. My guess is that it's most likely an issue with the transactional fixtures, as Rails is likely cleaning up a transaction before you're fully authenticated (thus why I'd suspect #1 above won't work).
I'm having an issue with Rails 4.0.3 and rspec 2.14.1 in testing a controller.
The relevant portion of the controller is:
class LoginsController < ApplicationController
def sign_in
#user = User.find_by(email: params[:email])
# ... - a few other codepaths but nothing that looks for primary_phone
if params[:email]
#user.send_token
flash[:notice] = "blah blah"
end
end
User.rb is:
class User < ActiveRecord::Base
# ...
def send_token
raise 'Tried to send token to contact with no phone number' if primary_phone.nil?
SMSSender.sms(primary_phone,"Your login code is: #{generate_token}")
end
end
The spec is:
require 'spec_helper'
describe LoginsController do
it "sends a token if a valid email is provided" do
#u = create(:user, primary_phone: "abc")
User.any_instance.should receive(:send_token)
post 'sign_in', email: #u.email
end
end
And, my user factory:
FactoryGirl.define do
factory :user do
name "MyString"
email "a#b.com"
end
end
When I change the spec's #u = create line to #u = create(:user) (ie, omitting the primary_phone), I get:
Failure/Error: post 'sign_in', email: #u.email
RuntimeError:
Tried to send token to contact with no phone number
# ./app/models/user.rb:16:in `send_token'
# ./app/controllers/logins_controller.rb:19:in `sign_in'
# ./spec/controllers/logins_controller_spec.rb:14:in `block (3 levels) in <top (required)>'
This is as expected. When I change it back to include the primary_phone, I get:
1) LoginsController sign_in sends a token if a valid email is provided
Failure/Error: User.any_instance.should receive(:send_token)
(#<RSpec::Mocks::AnyInstance::Recorder:0x007ff537ed4bd8>).send_token(any args)
expected: 1 time with any arguments
received: 0 times with any arguments
# ./spec/controllers/logins_controller_spec.rb:14:in `block (3 levels) in <top (required)>'
Having trouble understanding why that change would prevent the spec from passing. I did attach a debugger right after the 'post' in the spec and looked at the flash to see if it was correct (i.e., to ensure the proper code tree in the controller was being run) and it is.
The problem is you need to say should_receive rather than should receive. This is because of the any_instance. User.any_instance.should receive means that whatever object any_instance returns (an RSpec::Mocks::AnyInstance::Recorder) should receive the call. Of course that's not what you want, because that object is also not the same instance as what the controller instantiates. (In fact it's not even a User.) So the Recorder has a special should_receive method that does what you actually want. Tricky!
The User object you've created in your spec is not the same User object that the sign_in method creates and sends send_token to, so the expectations you set on #u as reflected in your error message are not going to be met. They both are associated with the same underlying database record, but they are different Ruby objects. (Note: In the first version of your question, the code you showed for your spec didn't match the error you showed, as the code showed setting an expectation on User.any_instance whereas your error message reflected setting an expectation on #u
Further, the expectations need to be set prior to the call you are expecting (e.g. prior to the post in your case, as noted in the comment by #PaulAJungwirth.
Finally, as an alternative to the answer provided by #PaulAJungwirth, you can use:
expect_any_instance_of(User).to receive(:send_token)
to address the problem with the your stated expectation line.
I'm having trouble setting expectations for a mock used in one of my controllers:
controllers/blark_controller.rb
class BlarkController < ApplicationController
def show
user = User.first
user.inspect
render nothing: true
end
end
spec/controllers/blark_controller_spec.rb
require 'spec_helper'
describe BlarkController do
describe 'GET :show' do
let(:user) { mock_model User }
before do
User.stub(:first).and_return(user)
get :show
end
it 'blarks' do
expect(user).to receive(:inspect)
end
end
end
Results in this:
22:04:58 - INFO - Running: spec/controllers/blark_controller_spec.rb
BlarkController
GET :show
blarks (FAILED - 1)
Failures:
1) BlarkController GET :show blarks
Failure/Error: expect(user).to receive(:inspect)
(Double "User_1001").inspect(any args)
expected: 1 time with any arguments
received: 0 times with any arguments
# ./spec/controllers/blark_controller_spec.rb:14:in `block (3 levels) in <top (required)>'
Finished in 0.15579 seconds
1 example, 1 failure
Failed examples:
rspec ./spec/controllers/blark_controller_spec.rb:13 # BlarkController GET :show blarks
Can I set expectations on mocks in specs?
You can, but the way you're doing it is wrong.
You're calling an action (get :show) and then after calling it, setting a future expectation (expect(user).to receive(:inspect)). Obvious this won't work because you've already called the action, there is no future for this test.
You either need to set the expectation before calling the action (switch the order of the statements) or use rspec's recently-added spies feature to set expectations after the fact. This uses have_received rather than receive.
For more detail: https://www.relishapp.com/rspec/rspec-mocks/v/2-14/docs/spies/spy-on-a-stubbed-method-on-a-partial-mock
I set up a controller which handles omniauth authentications which are worked into a custom built authentication system. i am trying to test the logic for how authentications are handled (ex: if user already has/does not have account, if user is/isn't currently logged in, etc.). as such i have a Authorization model and a authorizations controller. The action to create a authorization has this general outline:
class AuthorizationsController < ApplicationController
def create
omniauth = request.env['omniauth.auth']
authorization = Authorization.find_by_provider_and_uid(omniauth['provider'], omniauth['uid'])
if authorization
# Authorization already established, log in user
elsif current_user
# User is logged in but wants to add another omniauth authentication
else
# Create user and associate them with omniauth authentication
end
end
end
I am trying to test this logic in Rspec but have been having issues. Heres is what I am working with in my spec:
describe AuthorizationsController do
render_views
describe "POST 'create'" do
describe "with an already existing authorization" do
it "should log the user in" do
#authmock = mock_model(Authorization)
Authorization.should_receive(:find_by_provider_and_uid).and_return(#authmock)
post :create, :provider => 'twitter'
current_user?(#authmock.user).should == true
response.should redirect_to(root_path)
end
end
end
end
I am under the impression that this should assign my mocked Authorization model (#authmock) to the local variable authorization in my controller when the assignment call is made, thus making 'if authorization' return true. However whenever I true to run this spec I get this error:
Failures:
1) AuthorizationsController POST 'create' with an already existing authorization should log the user in
Failure/Error: post :create, :provider => 'twitter'
NoMethodError:
You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.[]
# ./app/controllers/authorizations_controller.rb:5:in `create'
# ./spec/controllers/authorizations_controller_spec.rb:16:in `block (4 levels) in <top (required)>'
Can anyone enlighten me as to what I am doing wrong here?
Edit:
since the question was raised as to whether or not the assignment of omniauth was causing issues, I commented out that line to see what would happen and got the following error:
1) AuthorizationsController POST 'create' with an already existing authorization should log the user in
Failure/Error: post :create, :provider => 'twitter'
NameError:
undefined local variable or method `omniauth' for #<AuthorizationsController:0xb41809c>
# ./app/controllers/authorizations_controller.rb:5:in `create'
# ./spec/controllers/authorizations_controller_spec.rb:16:in `block (4 levels) in <top (required)>'
which tells me that the problem is with the mock or stub as the find_by_provider_and_uid function is still being evaluated and is not stubbed when the test runs
Are you specing
current_user?(#authmock.user).should == true
or
response.should redirect_to(root_path)
I think that first expectation should not be tested here, because you've mocked 'if authorization' block, so you should spec what happens then!