How to test cookies.permanent.signed in Rails 3 - ruby-on-rails

I have a action in some controller that set some value in a permanent signed cookie like this:
def some_action
cookies.permanent.signed[:cookie_name] = "somevalue"
end
And in some functional test, I'm trying to test if the cookie was set correctly suing this:
test "test cookies" do
assert_equal "somevalue", cookies.permanent.signed[:cookie_name]
end
However, when I run the test, I got the following error:
NoMethodError: undefined method `permanent' for #
If I try only:
test "test cookies" do
assert_equal "somevalue", cookies.signed[:cookie_name]
end
I get:
NoMethodError: undefined method `signed' for #
How to test signed cookies in Rails 3?

I came across this question while Googling for a solution to a similar issue, so I'll post here. I was hoping to set a signed cookie in Rspec before testing a controller action. The following worked:
jar = ActionDispatch::Cookies::CookieJar.build(#request)
jar.signed[:some_key] = "some value"
#request.cookies['some_key'] = jar[:some_key]
get :show ...
Note that the following didn't work:
# didn't work; the controller didn't see the signed cookie
#request.cookie_jar.signed[:some_key] = "some value"
get :show ...

In rails 3's ActionControlller::TestCase, you can set signed permanent cookies in the request object like so -
#request.cookies.permanent.signed[:foo] = "bar"
And the returned signed cookies from an action taken in a controller can be tested by doing this
test "do something" do
get :index # or whatever
jar = #request.cookie_jar
jar.signed[:foo] = "bar"
assert_equal jar[:foo], #response.cookies['foo'] #should both be some enc of 'bar'
end
Note that we need to set signed cookie jar.signed[:foo], but read unsigned cookie jar[:foo]. Only then we get the encrypted value of cookie, needed for comparison in assert_equal.

After looking at the Rails code that handles this I created a test helper for this:
def cookies_signed(name, opts={})
verifier = ActiveSupport::MessageVerifier.new(request.env["action_dispatch.secret_token".freeze])
if opts[:value]
#request.cookies[name] = verifier.generate(opts[:value])
else
verifier.verify(cookies[name])
end
end
Add this to test_help.rb, then you can set a signed cookie with:
cookies_signed(:foo, :value => 'bar')
And read it with:
cookies_signed(:foo)
A bit hackish maybe, but it does the job for me.

The problem (at least on the surface) is that in the context of a functional test (ActionController::TestCase), the "cookies" object is a Hash, whereas when you work with the controllers, it's a ActionDispatch::Cookies::CookieJar object. So we need to convert it to a CookieJar object so that we can use the "signed" method on it to convert it to a SignedCookieJar.
You can put the following into your functional tests (after a get request) to convert cookies from a Hash to a CookieJar object
#request.cookies.merge!(cookies)
cookies = ActionDispatch::Cookies::CookieJar.build(#request)

The problem also appears to be your tests.
Here is some code and tests I used to TDD the situation where you want to set a cookie's value from passing a params value into a view.
Functional Test:
test "reference get set in cookie when visiting the site" do
get :index, {:reference => "121212"}
refute_nil cookies["reference"]
end
SomeController:
before_filter :get_reference_code
ApplicationController:
def get_reference_code
cookies.signed[:reference] ||= params[:reference]
end
Notice that the refute_nil line, the cookies is a string... that is one thing that also made this test not pass, was putting a symbol in cookies[:reference] the test did not like that, so i didn't do that.

Related

Using Rails 5 and minitest, how can I access a record that was created in the test?

I have this test that was passing before I switched the Note table to use UUID:
test "SHOULD create note AND redirect to Notes#index(SINCE null folder_id) WHEN only body is provided IF logged in as account:owner" do
sign_in #user
assert_difference('#user.account.notes.count') do
post notes_url(#user.account.hash_id), params: { note: { body: #note_by_user_no_folder.body } }
end
assert_redirected_to notes_url(#user.account.hash_id, anchor: Note.last.id)
end
But after switching to UUID, I'm getting the following failure error:
Failure:
NotesControllerTest#test_SHOULD_create_note_AND_redirect_to_Notes#index(SINCE_null_folder_id)_WHEN_only_body_is_provided_IF_logged_in_as_account:owner [/Users/chris/Dropbox/Repositories/keepshelf/test/controllers/notes_controller_test.rb:117]:
Expected response to be a redirect to <http://www.example.com/11111111/notes#9dc409ff-14cc-5f64-8f5f-08e487f583ee> but was a redirect to <http://www.example.com/11111111/notes#34dac6b7-46af-4c5c-bff7-760ffa77edf6>.
Expected "http://www.example.com/11111111/notes#9dc409ff-14cc-5f64-8f5f-08e487f583ee" to be === "http://www.example.com/11111111/notes#34dac6b7-46af-4c5c-bff7-760ffa77edf6".
I take this as that since the UUID's do not follow an order, that the new Note I am creating in the "post notes_url(..." does not end up being the "last" one that Note.last.id finds.
How can I set the anchor: to record_that_was_just_created.id ?
I figured this out. apparently there is a way to access the controller's instance variables in the test so I got it to work by changing the assert_redirected_to to
assert_redirected_to notes_url(#user.account.hash_id, anchor: controller.instance_variable_get(:#note).id)
Late answer here, I'm glad you got the result you need!
... in case anyone else reads this looking for the test practices ...
The short answer is - you should always know what data is being used in the test ... because you make it. The most common 3 ways ...
In the setup method where you
Load a fixture #account = Accounts(:first) (needs matching entry in yaml file)
Call factorybot gem (see thoughtbot's page for how to)
Explicitly write #account = #user.account.build(field: value, field2: value)
So, you have the literal answer to your question - it's bad practice to test against uncertain data - Shaunak was saying that too.
The above is probably enough - but your detail oriented testing should be in unit tests - where you are not calling against db & making your tests the slowest possible version. Your integration level stuff should just test that creation was successful - you already know the data going in there works.
For the best experience you probably want unit tests something from the 3 methods above in setup & use #account in the test like so to test against the validations you are about to create ...
So the unit test would be ...
setup
#account = #user.account.build # avoid hitting db unnecessarily versus create
#account.field = value
#account.field2 = value2
end
def test_details_pass_activerecord_validations
signin #user
#account.test_specific_fields = value # stuff not all the tests use
assert #account.valid? # trigger all the activerecord validations, but not db
end
From there your integration ...
test "SHOULD create note AND redirect to Notes#index(SINCE null folder_id) WHEN only body is provided IF logged in as account:owner" do
# Your setup method above should have most of the intialization
# ... basically #account = #user.account .note would be setup, but not inserted
signin #user
assert_difference('#user.account.notes.count') do
post notes_url(#account), params: { note: { body: #account.note.body } }
end
assert_redirected_to notes_url(#account, anchor: #account.whatever_field)
end

Set header in RSpec 3 request

I'm trying to set the header for some RSpec requests that require authentication. The header is ACCESS_TOKEN. No matter how I attempt to set the header, it never gets set. I know the app works because I can manually test it, I just cant get rspec tests to work. See the full source code & tests for this problem here: https://github.com/lightswitch05/rspec-set-header-example
Since authentication is used in most of my request specs, I've created support helper module to retrieve an access token and set it in the header. Below is the summary of how I'm trying to set the header, see everything I've tried in the full source
# my_app/spec/support/session_helper.rb
module SessionHelper
def retrieve_access_token
post api_v1_session_path({email: 'test#example.com', password: 'poor_password'})
expect(response.response_code).to eq 201
expect(response.body).to match(/"access_token":".{20}"/)
parsed = JSON(response.body)
token = parsed['access_token']['access_token']
#request.headers['HTTP_ACCESS_TOKEN'] = token
end
end
an example request spec that uses this helper and should work, but always fails because the header never gets set:
# my_app/spec/requests/posts_spec.rb
# ...
context "create" do
it "creates a post" do
retrieve_access_token
post = FactoryGirl.build(:post)
post api_v1_posts_path(
post: {
title: post.title,
content: post.content
}
)
expect(response.body).to include('"id":')
expect(response.body).to include('"title":"' + post.title + '"')
expect(response.body).to include('"content":"' + post.content + '"')
expect(response.response_code).to eq 201
end
end
I know I can manually set the header in the individual get and post requests - but that is not a maintainable solution for API-wide authorization. Imagine having to change every test if the header name changed slightly.
Note: This answer is based on what you seem to be calling api_v1_session_path with post request to SessionsController for every spec you're trying to run in your requests specs.
There are two ways to solve the issue I figured you have here.
Solution #1 - Either you create another helper method in your SessionHelper or in some other helper file called support/requests_helper.rb(however you prefer). I'd create another helper in support/requests_helper.rb:
module RequestsHelper
def get_with_token(path, params={}, headers={})
headers.merge!('HTTP_ACCESS_TOKEN' => retrieve_access_token)
get path, params, headers
end
def post_with_token(path, params={}, headers={})
headers.merge!('HTTP_ACCESS_TOKEN' => retrieve_access_token)
post path, params, headers
end
# similarly for xhr..
end
then in rails_helper.rb:
# Include the sessions helper
config.include SessionHelper, type: :request
# Include the requests helper
config.include RequestsHelper, type: :request
change session_helper.rb:
# my_app/spec/support/session_helper.rb
module SessionHelper
def retrieve_access_token
post api_v1_session_path({email: 'test#example.com', password: 'poor_password'})
expect(response.response_code).to eq 201
expect(response.body).to match(/"access_token":".{20}"/)
parsed = JSON(response.body)
parsed['access_token']['access_token'] # return token here!!
end
end
Now, you can change your all requests specs like this:
describe Api::V1::PostsController do
context "index" do
it "retrieves the posts" do
get_with_token api_v1_posts_path
expect(response.body).to include('"posts":[]')
expect(response.response_code).to eq 200
end
it "requires a valid session key" do
get api_v1_posts_path
expect(response.body).to include('"error":"unauthenticated"')
expect(response.response_code).to eq 401
end
end
end
Solution #2 - Change specs/factories/access_token_factory.rb to:
FactoryGirl.define do
factory :access_token do
active true
end
# can be used when you want to test against expired access tokens:
factory :inactive_access_token do
active false
end
end
Now, change your all requests specs to use access_token:
describe Api::V1::PostsController do
context "index" do
let(:access_token){ FactoryGirl.create(:access_token) }
it "retrieves the posts" do
# You will have to send HEADERS while making request like this:
get api_v1_posts_path, nil, { 'HTTP_ACCESS_TOKEN' => access_token.access_token }
expect(response.body).to include('"posts":[]')
expect(response.response_code).to eq 200
end
it "requires a valid session key" do
get api_v1_posts_path
expect(response.body).to include('"error":"unauthenticated"')
expect(response.response_code).to eq 401
end
end
end
I'd go with "Solution #1" as it removes a burden of making you remember to send HTTP_ACCESS_TOKEN in headers every time you want to make such requests.
Common misconception is to treat controller and request tests equally.
It would be good to start from reading about controller specs and request specs. As you can see, controller specs simulate http request, while request specs perform full stack request.
You can find some good article about why you should write controller specs and what to test there here. While it is good to write them, they shouldn't be touching database in my opinion.
So while Voxdei answer is partially valid (after changing request specs to controller specs your way of setting headers will work), it misses the point in my opinion.
In request specs, you cannot just use request / controller methods, you have to pass your headers in hash as third argument of your request methods, so i.e.
post '/something', {}, {'MY-HEADER' => 'value'}
What you could do though is to stub authentication like:
before do
allow(AccessToken).to receive("authenticate").and_return(true)
end
Then you could test your authentication in one spec to be sure that it works and use such before filter in other specs. This is also probably better approach as performing additional request every time you run spec needing authentication is quite huge overhead.
I also found quite interesting pull request in grape gem which tries to add default headers behaviour so you could also try with such approach if you would really want to use default headers in request specs.
Probably because of how now Rspec treats spec files. It no longer automatically infers spec type from a file location
Try either setting this behavior back to what you used to know
RSpec.configure do |config|
config.infer_spec_type_from_file_location!
end
or set it locally for each controller spec files in your project
describe MyController, type: :controller do
# your specs accessing #request
end
Surya's answer is the best. But you can DRY it up a little bit more:
def request_with_user_session(method, path, params={}, headers={})
headers.merge!('HTTP_ACCESS_TOKEN' => retrieve_access_token)
send(method, path, params, headers)
end
Here you have only one method and call the request method by the given parameter method.
I stub the function that authenticates the request to return true or any value returned by the function.
ApplicationController.any_instance.stub(:authenticate_request) { true }

Expect to receive working for any_instance but not for specific one

I have the following rspec:
context 'when there is an incoming error' do
it 'should update status of url to error' do
url = create(:url)
error_params = {error: 'whatever', url_id : url.id}
expect(url).to receive(:set_error)
post :results, error_params
end
end
And the results action looks like this:
def results
url = Url.find(url_id: params['url_id'])
if params.key?('error') && !params['error'].blank?
url.set_error
end
end
If I do it like this, the test does not pass:
expected: 1 time with any arguments
received: 0 times with any arguments
However, if I change to:
expect_any_instance_of(Url).to receive(:set_error).
It passes. I just have one Url, so I am not sure what is going on.
When you create a to receive expectation, it is connected to a specific Ruby object.
When the results action is called, it instantiates a new url object. It represents the same database object that you called your expectation on in the Rspec example. But it isn't the same Ruby object - it's a new object with (probably) the same data. So the expectation fails.
To illustrate:
describe ".to_receive" do
it "works on Ruby objects" do
url = Url.create(:url)
same_url = Url.find(url.id)
expect(url).to_not receive(:to_s)
same_url.to_s
end
end
To (somewhat) get the desired behaviour you could use any_instance and change the controller so that it assigns the url object to an instance variable. In that way you can inspect the url object easier:
# Change your action so that it saves the url object as an instance variable
def results
#url = Url.find(url_id: params['url_id'])
if params[:error].present?
#url.set_error
end
end
# Change your spec to look at the assigned variable
context 'when there is an incoming error' do
it 'should update status of url to error' do
url = create(:url)
error_params = {error: 'whatever', url_id: url.id}
expect_any_instance_of(Url).to receive(:set_error)
post :results, error_params
expect(assigns(:url)).to eq(url)
end
end
Since assigns only lets you inspect the assigned ivar after the controller action has been executed, you can't use it to create a receive expectation.

RoR: testing an action that uses http token authentication

I'm trying to test a controller that's using an http token authentication in the before filter. My problem is that it works ok wheh I use curl to pass the token, but in my tests it always fails (I'm using rspec btw). Tried a simple test to see if the token was being passed at all, but it seems like it's not doing so. Am I missing anything to get the test to actually pass the token to the controller?
Here's my before filter:
def restrict_access
authenticate_or_request_with_http_token do |token, options|
api_key = ApiKey.find_by_access_token(token)
#user = api_key.user unless api_key.nil?
#token = token #set just for the sake of testing
!api_key.nil?
end
end
And here is my test:
it "passes the token" do
get :new, nil,
:authorization => ActionController::HttpAuthentication::Token.encode_credentials("test_access1")
assigns(:token).should be "test_access1"
end
I'm assuming ApiKey is an ActiveRecord model, correct? curl command runs against development database, and tests go against test db. I can't see anything that sets up ApiKey in your snippets. Unless you have it somewhere else, try adding something along these lines:
it "passes the token" do
# use factory or just create record with AR:
ApiKey.create!(:access_token => 'test_access1', ... rest of required attributes ...)
# this part remains unchanged
get :new, nil,
:authorization => ActionController::HttpAuthentication::Token.encode_credentials("test_access1")
assigns(:token).should be "test_access1"
end
You can later move it to before :each block or support module.
UPDATE:
After seeing your comment I had to look deeper. Here's another guess. This form of get
get '/path', nil, :authorization => 'string'
should work only in integration tests. And for controller tests auth preparation should look like this:
it "passes the token" do
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Token.encode_credentials("test_access1")
get :new
assigns(:token).should be "test_access1"
end
Reasons behind this come from method signatures for respective test modules:
# for action_controller/test_case.rb
def get(action, parameters = nil, session = nil, flash = nil)
# for action_dispatch/testing/integration.rb
def get(path, parameters = nil, headers = nil)

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