rspec, request spec, devise, multi-controller - ruby-on-rails

I'm trying to build a request spec which tests the full gamut of creating a new user via devise API.
I currently have this sitting in RegistrationsController spec, but this won't work if I want to follow the mail link to the confirmations controller.
I haven't been able to find a good example of how people have tested the 'hand-off' from one controller to another and the intermittent 'steps' (we have custom devise methods scattered throughout the process which this test will encompass too).
it "creates a user, sends a welcome email, confirms the user, and registers the user in email campaigns" do
post :create, {user: new_user_params}
last_email = ActionMailer::Base.deliveries.last.body
ConfirmationsController.any_instance.should_receive(:after_filter_method_to_subscribe_user)
redirect_to confirmation_link(last_email) # helper method
last_email.should include("Thanks for joining!")
user = User.find_by_first_name(new_first_name)
user.confirmed?.should be_true
user.email_lists.should_not be_empty
end
Edit:
I should also add that I need http_basic_auth to run the spec which I'm including in a spec/support file and sets the request.env['HTTP_AUTHORIZATION'] to variables stored in the API::Base controller. I currently have nil as a request obect when running specs in the spec/request folder, which I'll need to run the specs.
Edit:
Thanks to people who've taken a look. I figured it out after piecing together two SO searches and the code I had. I'll post an answer for future SO'ers when I can.

I figured this out shortly after posting my question with good luck finds on more google searches. Kudos to a couple SO references ~> request spec relish: http://goo.gl/iBg7v1 && setting request headers for http basic auth in request specs: http://goo.gl/hdDBMd
My spec turned out to look something like the below Hope this helps someone not waste 4 hours like me :).
spec/requests/api/user_registration_spec.rb.
it "sends a welcome email, confirms the user, and signs the user up to email campaigns" do
email_list = FactoryGirl.create(:email_list, name: "funky-email-campaign")
user_name = Api::RegistrationsController::USER
password = Api::RegistrationsController::PASSWORD
# post to /users/registration
post api_registrations_path({user: new_user_params}), nil , {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user_name, password)}
last_email = ActionMailer::Base.deliveries.last.body
UserService.should_receive(:subscribe_to_email).and_call_original # check that after_filter is called
get confirmation_link(last_email) # follow link in email (/users/confirmation)
response.should redirect_to(custom_path) # tests after_confirmation_path_for override
last_email.should include(new_first_name)
last_email.should include("Thanks for joining!")
user = User.find_by_first_name(new_first_name)
user.confirmed?.should be_true
user.email_lists.first.name.should eq(email_list.name)
end

Related

undefined local variable or method `request'

I am new to ruby on rails. I am getting an undefined method error when I run rspec on comment_spec.rb
1) after_save calls 'Post#update_rank' after save
Failure/Error: request.env["HTTP_REFERER"] = '/'
NameError:
undefined local variable or method `request' for #<RSpec::ExampleGroups::AfterSave:0x007fa866ead8d0>
# ./spec/models/vote_spec.rb:45:in `block (2 levels) in <top (required)>'
This is my spec:
require 'rails_helper'
describe Vote do
....
describe 'after_save' do
it "calls 'Post#update_rank' after save" do
request.env["HTTP_REFERER"] = '/'
#user = create(:user)
#post = create(:post, user: #user)
sign_in #user
vote = Vote.new(value:1, post: post)
expect(post). to receive(:update_rank)
vote.save
end
end
Any help that you would have would be greatly appreciated...
I was following the apirails book tutorial chapter 3 here
http://apionrails.icalialabs.com/book/chapter_three
I was receiving the same error and DrPositron's solution worked for me, all green again. Just needed to add ":type => :controller" on my block like so:
describe Api::V1::UsersController, :type => :controller do
end
Hope this helps someone
OK here's the deal.
Vote is a model, i suppose.
You are writing a test for that model.
There's a difference between model tests ("the domain logic is doing what its supposed to") and feature/integration tests ("the application is behaving the way its supposed to").
The request variable is associated with feature or controller tests.
So what's wrong?
You are not logging in users in model tests, just check if the update_rank method is being called on save, thats it.
No user-interaction jazz in model tests.
Hope that helps!
Cheers
Jan
So Louis, just to expand on Jan's response:
You appear to be writing a model spec. The purpose of a model spec is simply to test how your model classes work, and that behavior is testable without having to pay any attention to the application logic around signing in, making "requests" to particular controllers, or visiting particular pages.
You're essentially just testing a couple related Ruby classes. For this, we don't need to think about the whole app -- just the classes we're testing.
As a consequence, RSpec doesn't make certain methods available in the spec/models directory -- you're not supposed to think about requests or authentication in these tests.
It looks like your test is simply designed to make sure that when you create a vote for a post, it updates that post's rank (or, specifically, call's that post's update_rank method). To do that, you don't need to create a user, or sign a user in, or pay any attention to the request (what request would we be referring to? We're just testing this as if in Rails console, with no HTTP request involved).
So you could basically remove the first four lines of your test -- apart from the line creating your post, and the post's user if it's necessary (if the post model validates the presence of a user). Don't sign a user in -- we're just testing a Ruby class. There's no concept of a website to sign into in this test.
Then, as a last thing to take care of to get your spec to pass, make sure to refer to the post you create by the right name. Right now, you're creating a post and assigning it to the #post variable, but then you're referring to just post later on. post doesn't exist; just #post. You'll have to pick one variable name and stick with it.
Also, if you are using RSpec 3, file type inference is now disabled by default and must be opted in as described here. If you're new to RSpec, a quick overview of the canonical directory structure is here.
For example, for a controller spec for RelationshipsController, insert , :type => :controller as such:
describe RelationshipsController, :type => :controller do
#spec
end

rspec/capybara: how to simulate incoming POST requests? (rack-test won't work)

I need to receive incoming emails as multipart-formdata via a POST request from Cloudmailin. The POST resembles the following:
Parameters: {"to"=>"<email#exmaple.comt>", "from"=>"whomever#example", "subject"=>"my awesome subject line....
Actually, receiving and parsing emails is super easy because the email is just posted as params: params[:to], params[:from], etc. However, how do I simulate this POST request in rails?
I built a dummy rails app to test out Cloudmailin, so I have an actual request. However, it's a 6k character file, so I'd like to load this file as the parameters of the POST request. I've tried using the built rails post and post_via_redirect methods to load a file, but it escapes all of the parameters( \"to\"), which is no good. Any ideas?
So, I ended up doing:
#parameters = { "x_to_header"=>"<#{ #detail.info }>",
"to"=>"<#{ #account.slug }#cloudmailin.net>",
"from"=>"#{ #member.email }",
"subject"=>"meeting on Monday",
"plain"=>"here is my message\nand this is a new line\n\n\nand two new lines\n\n\n\nand a third new line"
}
then just:
post "/where_ever", #parameters
seems to get the job done for now
A simple way would probably to execute a script in capybara. Just make sure with the #javascript tag, then load any page in your app that has jQuery installed (technically, you don't need this, but it's much easier. Then:
When /^I get a post request from Cloudmailin$/ do
visit '/some/page/with/jquery'
page.execute_script(%{$.post("/some/path?to=some_email&etc=etc");})
end
There's the simple post capybara method too, but I'm not too sure about how that works. Might be worth looking into.
I saw this answer last night when I was updating some of my own test code for Rails 3.2.8, and which uses the Mail gem, and thought I'd share what I found. The test code is for an application that needs to take a POST from Cloudmailin and then process it to create a new user with Devise, and then send a confirmation to that user, which the user can then follow to choose a password. Here is my controller spec:
require 'spec_helper'
describe ThankyouByEmailController do
message1 = Mail.new do
from "Frommy McFromerton <frommy.mcfrommerton#gmail.com>"
to "toey.receivesalot#gmail.com"
subject "cloudmailin test"
body 'something'
text_part do
body 'Here is the attachment you wanted'
end
html_part do
content_type 'text/html; charset=UTF-8'
body '<h1>Funky Title</h1><p>Here is the attachment you wanted</p>'
end
end
describe "creating new users" do
describe "unregistered FROM sender and Unregistered TO receiver" do
it "should create 2 new users" do
lambda do
post :create, :message => "#{#message1}"
end.should change(User, :count).by(2)
end
end
end
end
Hope this clean up your own tests. And for anyone else interested in testing the mail gem, mikel's documentation has come a long way for same:
https://github.com/mikel/mail

How do I test the actions that require the user to be logged in?

I am trying to test my controllers, but some of the actions in my controller expect the user to be logged in. How will I test them? Do I mess with the session variable directly? Also, what if a lot of the actions expect the user to be logged in? Should I set up a before action, and log the user in there?
Another idea I had was that I could test them in an integration test, and do a post on the login form, before I actually test the desired action. Something like:
def setup
# log the user in, this will happen before every test
end
# integration test
test "I should see my posts" do
#setup should have happened before this, and I should be logged in
get posts_path
assert ...
end
Is this the way to test these actions? Am I missing something?
Depending on your authentication framework you use there are several ways. Devise for example has some TestHelpers, that make it easy to login users without having go through the actual webpage in functional tests. If thats not an option, like soundsop said, browser testing. (look at, from high to low: cucumber, capybara, selenium/...)
The Book includes some testing examples in their depot application:
test "should login" do
dave = users(:one)
post :create, :name => dave.name, :password => 'secret'
assert_redirected_to admin_url
assert_equal dave.id, session[:user_id]
end
Full details in the "Authenticating Users" section.
You can either make fake user credentials in the setup, or you can stub out the method which checks the credentials using a mocking library. I've done both and don't have a firm preference.

How do you POST to a URL in Capybara?

Just switched from Cucumber+Webrat to Cucumber+Capybara and I am wondering how you can POST content to a URL in Capybara.
In Cucumber+Webrat I was able to have a step:
When /^I send "([^\"]*)" to "([^\"]*)"$/ do |file, project|
proj = Project.find(:first, :conditions => "name='#{project}'")
f = File.new(File.join(::Rails.root.to_s, file))
visit "project/" + proj.id.to_s + "/upload",
:post, {:upload_path => File.join(::Rails.root.to_s, file)}
end
However, the Capybara documentation mentions:
The visit method only takes a single
parameter, the request method is
always GET.always GET.
How do I modify my step so that Cucumber+Capybara does a POST to the URL?
More recently I found this great blog post. Which is great for the cases like Tony and where you really want to post something in your cuke:
For my case this became:
def send_log(file, project)
proj = Project.find(:first, :conditions => "name='#{project}'")
f = File.new(File.join(::Rails.root.to_s, file))
page.driver.post("projects/" + proj.id.to_s + "/log?upload_path=" + f.to_path)
page.driver.status_code.should eql 200
end
You could do this:
rack_test_session_wrapper = Capybara.current_session.driver
rack_test_session_wrapper.submit :post, your_path, nil
You can replace :post which whatever method you care about e.g. :put or :delete.
Replace your_path with the Rails path you want e.g. rack_test_session_wrapper.submit :delete, document_path(Document.last), nil would delete the last Document in my app.
Updated answer 2022-10-05
If your driver doesn't have post (Poltergeist doesn't, for example), you can do this:
response = nil
open_session do |session|
session.post("/mypath", params: { foo: "bar" })
response = session.response
end
We can now e.g. assert on response.body.
You can also use integration_session.post(…) directly, but I think that can cause some confusion by not separating the POST session from the test's ordinary session.
As has been stated elsewhere, in a Capybara test you typically want to do POSTs by submitting a form just like the user would. I used the above to test what happens to the user if a POST happens in another session (via WebSockets), so a form wouldn't cut it.
Docs:
https://api.rubyonrails.org/classes/ActionDispatch/Integration/Runner.html#method-i-open_session
Old answer from 2014-06-22
If your driver doesn't have post (Poltergeist doesn't, for example), you can do this:
session = ActionDispatch::Integration::Session.new(Rails.application)
response = session.post("/mypath", my_params: "go_here")
But note that this request happens in a new session, so you will have to go through the response object to assert on it.
Docs:
http://api.rubyonrails.org/classes/ActionDispatch/Integration/Session.html
http://api.rubyonrails.org/classes/ActionDispatch/Integration/RequestHelpers.html
Capybara's visit only does GET requests. This is by design.
For a user to perform a POST, he must click a button or submit a form. There is no other way of doing this with a browser.
The correct way to test this behaviour would be:
visit "project/:id/edit" # This will only GET
attach_file "photo", File.open('cute_photo.jpg')
click_button 'Upload' # This will POST
If you want to test an API, I recommend using spec/request instead of cucumber, but that's just me.
I know the answer has already been accepted, but I'd like to provide an updated answer. Here is a technique from Anthony Eden and Corey Haines which passes Rack::Test to Cucumber's World object:
Testing REST APIs with Cucumber and Rack::Test
With this technique, I was able to directly send post requests within step definitions. While writing the step definitions, it was extremely helpful to learn the Rack::Test API from it's own specs.
# feature
Scenario: create resource from one time request
Given I am an admin
When I make an authenticated request for a new resource
Then I am redirected
And I see the message "Resource successfully created"
# step definitions using Rack::Test
When /^I make an authenticated request for a new resource$/ do
post resources_path, :auth_token => #admin.authentication_token
follow_redirect!
end
Then /^I am redirected$/ do
last_response.should_not be_redirect
last_request.env["HTTP_REFERER"].should include(resources_path)
end
Then /^I see the message "([^"]*)"$/ do |msg|
last_response.body.should include(msg)
end
Although, not an exact answer to the question, the best solution for me has been to use Capybara for specs that simulate user interaction (using visit), and Rack Test for test API like requests. They can be used together within the same test suite.
Adding the following to the spec helper gives access to get, post and other Rack test methods:
RSpec.configure do |config|
config.include Rack::Test::Methods
You may need to put the Rack Test specs in a spec/requests folder.
With an application using RSpec 3+, you would not want to make an HTTP POST request with Capybara. Capybara is for emulating user behavior, and accepting the JS behavior and page content that results. An end user doesnt form HTTP POST requests for resources in your application, a user clicks buttons, clicks ajax links, drags n drops elements, submits web forms, etc.
Check out this blog post on Capybara and other HTTP methods. The author makes the following claim:
Did you see any mention of methods like get, post or response? No? That’s because those don’t exist in Capybara. Let’s be very clear about this...Capybara is not a library suited to testing APIs. There you have it. Do not test APIs with Capybara. It wasn’t designed for it.
So, developing an API or not, if you have to make an explicit HTTP POST request, and it does not involve an HTML element and some sort of event (click, drag, select, focusout, whatever), then it shouldn't be tested with Capybara. If you can test the same feature by clicking some button, then do use Capybara.
What you likely want is RSpec Request specs. Here you can make post calls, and any other HTTP method as well, and assert expectations on the response. You can also mock n stub objects and methods to assert expectations in regards to side effects and other behaviors that happen in between your request and the response.
# spec located in spec/requests/project_file_upload_spec.rb
require "rails_helper"
RSpec.describe "Project File Upload", type: :request do
let(:project) { create(:project) }
let(:file) { File.new(File.join(::Rails.root.to_s, 'path/to/file.ext')) } # can probably extract this to a helper...
it "accepts a file uploaded to a Project resource" do
post "project/#{project.id}/upload", upload_path: file
expect(response).to be_success
expect(project.file?).to eq(true)
# expect(project.file).not_to eq(nil)
expect(response).to render_template(:show)
end
end
As others have said, there’s no direct way of doing a POST with Capybara because it’s all about browser interaction. For API testing, I’d very highly recommend the rspec_api_documentation gem.

Session variables with Cucumber Stories

I am working on some Cucumber stories for a 'sign up' application which has a number of steps.
Rather then writing a Huuuuuuuge story to cover all the steps at once, which would be bad, I'd rather work through each action in the controller like a regular user. My problem here is that I am storing the account ID which is created in the first step as a session variable, so when step 2, step 3 etc are visited the existing registration data is loaded.
I'm aware of being able to access controller.session[..] within RSpec specifications however when I try to do this in Cucumber stories it fails with the following error (and, I've also read somewhere this is an anti-pattern etc...):
Using controller.session[:whatever] or session[:whatever]
You have a nil object when you didn't expect it!
The error occurred while evaluating nil.session (NoMethodError)
Using session(:whatever)
wrong number of arguments (1 for 0) (ArgumentError)
So, it seems accession the session store isn't really possible. What I'm wondering is if it might be possible to (and I guess which would be best..):
Mock out the session store etc
Have a method within the controller and stub that out (e.g. get_registration which assigns an instance variable...)
I've looked through the RSpec book (well, skimmed) and had a look through WebRat etc, but I haven't really found an answer to my problem...
To clarify a bit more, the signup process is more like a state machine - e.g. the user progresses through four steps before the registration is complete - hence 'logging in' isn't really an option (it breaks the model of how the site works)...
In my spec for the controller I was able to stub out the call to the method which loads the model based on the session var - but I'm not sure if the 'antipattern' line also applies to stubs as well as mocks?
Thanks!
I'll repeat danpickett in saying mocks should be avoided whenever possible in Cucumber. However if your app does not have a login page, or perhaps performance is a problem, then it may be necessary to simulate login directly.
This is an ugly hack, but it should get the job done.
Given /^I am logged in as "(.*)"$/ do |email|
#current_user = Factory(:user, :email => email)
cookies[:stub_user_id] = #current_user.id
end
# in application controller
class ApplicationController < ActionController::Base
if Rails.env.test?
prepend_before_filter :stub_current_user
def stub_current_user
session[:user_id] = cookies[:stub_user_id] if cookies[:stub_user_id]
end
end
end
mocks are bad in cucumber scenarios - they're almost kind of an antipattern.
My suggestion is to write a step that actually logs a user in. I do it this way
Given I am logged in as "auser#example.com"
Given /^I am logged in as "(.*)"$/ do |email|
#user = Factory(:user, :email => email)
#user.activate!
visit("/session/new")
fill_in("email", :with => #user.email)
fill_in("password", :with => #user.password)
click_button("Sign In")
end
I realize that the instance variable #user is kind of bad form—but I think in the case of logging in/out, having #user is definitely helpful.
Sometimes I call it #current_user.
Re. Ryan's solution - you can open up ActionController in you env.rb file and place it there to avoid putting in your production code base (thanks to john # pivotal labs)
# in features/support/env.rb
class ApplicationController < ActionController::Base
prepend_before_filter :stub_current_user
def stub_current_user
session[:user_id] = cookies[:stub_user_id] if cookies[:stub_user_id]
end
end
I don't know how much this relates to the original question anymore, but I decided to post anyway in the spirit of discussion...
We have a cucumber test suite that takes > 10 minutes to run so we wanted to do some optimization. In our app the login process triggers a LOT of extra functionality that is irrelevant to majority of the scenarios, so we wanted to skip that by setting the session user id directly.
Ryanb's approach above worked nicely, except that we were unable to log out using that approach. This made our multi-user stories fail.
We ended up creating a "quick login" route that is only enabled in test environment:
# in routes.rb
map.connect '/quick_login/:login', :controller => 'logins', :action => 'quick_login'
Here is the corresponding action that creates the session variable:
# in logins_controller.rb
class LoginsController < ApplicationController
# This is a utility method for selenium/webrat tests to speed up & simplify the process of logging in.
# Please never make this method usable in production/staging environments.
def quick_login
raise "quick login only works in cucumber environment! it's meant for acceptance tests only" unless Rails.env.test?
u = User.find_by_login(params[:login])
if u
session[:user_id] = u.id
render :text => "assumed identity of #{u.login}"
else
raise "failed to assume identity"
end
end
end
For us this ended up being simpler than working with the cookies array. As a bonus, this approach also works with Selenium/Watir.
Downside is that we're including test-related code in our application. Personally I don't think that adding code to make application more testable is a huge sin, even if it does add a bit of clutter. Perhaps the biggest problem is that future test authors need to figure out which type of login they should use. With unlimited hardware performance we obviously wouldn't be doing any of this.
Re: Ryan's solution:
Does not work with Capybara, unless small adaptation done:
rack_test_driver = Capybara.current_session.driver
cookie_jar = rack_test_driver.current_session.instance_variable_get(:#rack_mock_session).cookie_jar
#current_user = Factory(:user)
cookie_jar[:stub_user_id] = #current_user.id
(found here: https://gist.github.com/484787)
My understanding is that you get:
You have a nil object when you didn't expect it!
The error occurred while evaluating nil.session (NoMethodError)
when session[] is accessed before request has been instantiated. In your case, I'd imagine if you put webrats' visit some_existing_path before accessing session[] in your step defenition, the error will go away.
Now, unfortunately, session doesn't seem to persist across steps (at least, I couldn't find the way), so this bit of information doesn't help to answer your question :)
So, I suppose, Ryan's session[:user_id] = cookies[:stub_user_id]... is the way to go. Although, imo, test related code in the application itself doesn't sound right.
I use a testing-only sign-in solution like Prikka's, but I do it all in Rack instead of creating a new Controller and routes.
# in config/environments/cucumber.rb:
config.middleware.use (Class.new do
def initialize(app); #app = app; end
def call(env)
request = ::Rack::Request.new(env)
if request.params.has_key?('signed_in_user_id')
request.session[:current_user_id] = request.params['signed_in_user_id']
end
#app.call env
end
end)
# in features/step_definitions/authentication_steps.rb:
Given /^I am signed in as ([^\"]+)$/ do |name|
user = User.find_by_username(name) || Factory(:user, :username => name)
sign_in_as user
end
# in features/step_definitions/authentication_steps.rb:
Given /^I am not signed in$/ do
sign_in_as nil
end
module AuthenticationHelpers
def sign_in_as(user)
return if #current_user == user
#current_user = user
get '/', { 'signed_in_user_id' => (user ? user.to_param : '') }
end
end
World(AuthenticationHelpers)
#Ajedi32 I ran into the same issue (undefined method 'current_session' for Capybara::RackTest::Driver) and putting this in my step definition fixed the problem for me:
rack_test_browser = Capybara.current_session.driver.browser
cookie_jar = rack_test_browser.current_session.instance_variable_get(:#rack_mock_session).cookie_jar
cookie_jar[:stub_user_id] = #current_user.id
In my controller action, I referred to cookies[:stub_user_id], instead of cookie_jar[:stub_user_id]
Why don't you use FactoryGirl or (Fixjour or Fabricator) with Devise (or Authlogic) and SentientUser? Then you can simply sniff which user is already logged in!
#user = Factory(:user) # FactoryGirl
sign_in #user # Devise
User.current.should == #user # SentientUser
Another slight variation:
# In features/step_definitions/authentication_steps.rb:
class SessionsController < ApplicationController
def create_with_security_bypass
if params.has_key? :user_id
session[:user_id] = params[:user_id]
redirect_to :root
else
create_without_security_bypass
end
end
alias_method_chain :create, :security_bypass
end
Given %r/^I am logged in as "([^"]*)"$/ do |username|
user = User.find_by_username(username) || Factory(:user, :username => username)
page.driver.post "/session?user_id=#{user.id}"
end
After a lot of soul searching and web surfing, I finally opt'ed for a very simple and obvious solution.
Using cookies adds two problems. First you have code in the application specific for testing and second there is the problem that creating cookies in Cucumber is hard when using anything other than rack test. There are various solutions to the cookie problem but all of them are a bit challenging, some introduce mocks, and all of them are what I call 'tricky'. One such solution is here.
My solution is the following. This is using HTTP basic authentication but it could be generalized for most anything.
authenticate_or_request_with_http_basic "My Authentication" do |user_name, password|
if Rails.env.test? && user_name == 'testuser'
test_authenticate(user_name, password)
else
normal_authentication
end
end
test_authenticate does what ever the normal authenticate does except it bypasses any time consuming parts. In my case, the real authentication is using LDAP which I wanted to avoid.
Yes… it is a bit gross but it is clear, simple, and obvious. And… no other solution I've seen is cleaner or clearer.
Note, one feature is that if the user_name is not 'testuser', then the normal path is taken so they can be tested.
Hope this helps others...

Resources