Rspec problem mocking model in controller - ruby-on-rails

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!

Related

Devise Rspec expected the response to have a success status code (2xx) but it was 302

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

Unable to stub current_or_guest_user in my helper specs

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)

Testing Devise with RSpec and Factory Girl

EDIT Read my comment to this question
I'm very new to rails, so please bear with me.
I've been trying to configure a test for Devise using factory girl and rspec. This has taken me the best part of 2 hours, and scouring half the internet to no avail. Even though there is loads of thread on what seems to be my issue, I just cant figure it out.
This is how my /spec files looks like.
GET Home Gives the correct status code
Failure/Error: sign_in user
NoMethodError:
undefined method `sign_in' for #<RSpec::Core::ExampleGroup::Nested_2:0x00000106f32558>
# ./spec/models/user_spec.rb:6:in `block (2 levels) in <top (required)>
This is the error message I get, trying to achieve the following test:
user_spec.rb:
require 'spec_helper'
describe "GET Home" do
before do
##I have tried all sorts of things here. I have also tried to define a module in devise.rb (see note below*), and then call that module here instead of the 2 lines below. But I get the same error, no local variable or undefined method for ...
user = FactoryGirl.create(:user)
sign_in user
end
describe "GET /Home"
it "Gives the correct status code" do
get root_path
response.status.should be(200)
end
end
in spec/factories/users.rb:
FactoryGirl.define do
factory :user do
name "Christoffer"
email "test#test2.com"
password "testtest"
password_confirmation "testtest"
end
end
And the folling lines is included in spec_helpers.rb
config.include FactoryGirl::Syntax::Methods
config.include Devise::TestHelpers, :type => :controller
Now, by doing this, i get the error above. Can anyone possibly explain what I'm doing wrong here? It might be something really obvious, as I'm not really that well rehearsed in the ways of Rails.
*Note (module I tried to define in devise.rb and insert in the before do):
module ValidUserRequestHelper
# Define a method which signs in as a valid user.
def sign_in_as_a_valid_user_nommels
# ASk factory girl to generate a valid user for us.
#user ||= FactoryGirl.create :user
# We action the login request using the parameters before we begin.
# The login requests will match these to the user we just created in the factory, and authenticate us.
post_via_redirect user_session_path, 'user[email]' => #user.email, 'user[password]' => #user.password
end
end
The purpose of 'spec/requests' is for integration tests. You would test features of your app from the user's perspective (ie. fill in certain info, then click button, then so and so should happen if certain inputs are valid or invalid). Spec/models and spec/controllers are usually for unit tests where you test for smaller parts of your app (ie. what happens if the password and password_confirmation params passed to your user model don't match)

Rspec controller test - should_receive on instance of model returns 0 times

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.

rspec simple example getting error on request variable in integration test

This is an adopted rails app with no tests. I am trying to test omniauth in an integration test but am getting an error (edit I have based upon this: https://github.com/intridea/omniauth/wiki/Integration-Testing). This reflects my lack of understanding of Rspec. It would seem that the request object would be available by default.
I have in my spec/spec_helper.rb:
config.include IntegrationSpecHelper, :type => :request
Capybara.default_host = 'http://localhost:3000'
OmniAuth.config.test_mode = true
OmniAuth.config.add_mock(:facebook, {
:uid => '12345'
})
and in my spec/integration/login_spec:
require 'spec_helper'
describe ServicesController, "OmniAuth" do
before do
puts OmniAuth.config.mock_auth[:facebook]
puts request # comes back blank
request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:facebook]
end
it "sets a session variable to the OmniAuth auth hash" do
request.env["omniauth.auth"][:uid].should == '12345'
end
end
and I get the following error:
{"provider"=>"facebook", "uid"=>"12345", "user_info"=>{"name"=>"Bob
Example"}}
F
Failures:
1) ServicesController OmniAuth sets a session variable to the
OmniAuth auth hash
Failure/Error: request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:facebook]
NoMethodError:
undefined method env' for nil:NilClass
# ./login_spec.rb:8:inblock (2 levels) in '
Finished in 22.06 seconds 1 example, 1 failure
Failed examples:
rspec ./login_spec.rb:11 # ServicesController OmniAuth sets a session
variable to the OmniAuth auth hash
Should the request object be available here, by default? Does this error possibly mean something else?
thx
You're getting nil because you haven't made any request yet.
To make the test work, you have to do three things:
Set up the mock
Make the request
Test whatever code is attached to the callback
Here's how I do it. First set up the mock in the before block, and then visit the URL corresponding to the provider (in this case facebook):
before do
OmniAuth.config.add_mock(:facebook, {:uid => '12345'})
visit '/auth/facebook'
end
From the wiki:
A request to /auth/provider will redirect immediately to /auth/provider/callback.
So you have to have a route which matches '/auth/:provider/callback'. Whatever action you map that do has to perform the stuff in step 3 above.
If you wanted to test that the session variable was set to the uid, you could do something like this (which works because you set the uid to '12345' in the mock above):
it "sets a session variable to the OmniAuth auth hash" do
session['uid'].should == '12345'
end
And here's a route and action that should make this pass:
routes.rb
match '/auth/:provider/callback' => 'sessions#callback'
controllers/sessions_controller.rb
def callback
session['uid'] = request.env["omniauth.auth"][:uid]
end
That's the gist of it. Hope that helps.

Resources