Abstracting out common setup steps in RSpec test - ruby-on-rails

I'm trying to get my head around the best way to create a user token in my RSpec tests, and write them as eloquently as I can.
Below is one example for my Project class. From the spec below, you'll see I'm using DoorKeeper to keep the API endpoints secure on all actions other than show.
The problem I'm running into is how best to create that #access_token.
This works, passing all the examples, however I'm worried I'm not adhering to DRY principles. If lots of actions/contexts require an #access_token is there a way I could abstract this out somewhere to a helper of sorts?
Thanks in advance
## projects_spec.rb
require 'spec_helper'
describe "Projects API" do
describe "#index" do
FactoryGirl.create(:project)
context 'with a valid token' do
before(:each) do
user = FactoryGirl.create(:user)
authentication = FactoryGirl.create(:authentication, user: user)
application = Doorkeeper::Application.create!(:name => "MyApp", :redirect_uri => "http://app.com")
#access_token = Doorkeeper::AccessToken.create!(:application_id => application.id, :resource_owner_id => authentication.identity.id)
end
it 'returns a list of projects' do
get '/api/v1/projects', access_token: #access_token.token
expect(response.status).to eq(200)
# check the JSON is as we expect
end
end
context 'without a token' do
it 'responds with 401' do
get '/api/v1/projects'
expect(response.status).to eq(401)
end
end
end
describe "#create" do
context 'with a valid token' do
before(:each) do
user = FactoryGirl.create(:user)
authentication = FactoryGirl.create(:authentication, user: user)
application = Doorkeeper::Application.create!(:name => "MyApp", :redirect_uri => "http://app.com")
#access_token = Doorkeeper::AccessToken.create!(:application_id => application.id, :resource_owner_id => authentication.identity.id)
end
context 'with required params' do
project_params = {
name: "Win the lottery",
strapline: "The best feeling in the world"
}
it "creates a project and responds with 201" do
post "/api/v1/projects", :project => project_params, access_token: #access_token.token
expect(response.status).to eq(201)
# check the JSON is as we expect
end
end
context 'without required params' do
project_params = {
strapline: "Stepney City Farm's pallets, woodchips and compost",
}
it "responds with 422 and no record created" do
post "/api/v1/projects", :project => project_params, access_token: #access_token.token
expect(response.status).to eq(422)
json = JSON.parse(response.body)
expect(json['project']['errors'].length).to eq(1)
end
end
end
context 'without a token' do
it 'responds with 401' do
get '/api/v1/projects'
expect(response.status).to eq(401)
end
end
end
describe "#show" do
it 'returns a projects' do
project = FactoryGirl.create(:project, name: "A new project")
get "/api/v1/projects/#{project.id}"
expect(response.status).to eq(200)
json = JSON.parse(response.body)
expect(json['project']['name']).to eq(project.name)
expect(GroupRun.last.name).to eq(project.name)
# check the JSON is as we expect
end
end
end

I have a few techniques that I use to deal with this stuff.
The first is to use plain ruby methods over let. This is just my preference, I think it adds clarity to tests. Check this for more about that: http://robots.thoughtbot.com/lets-not
And then, I have a helper method for auth stuff. I'm using Devise for authentication, so my implementation will be different than yours, but this sets the HTTP_AUTHORIZATION header for every request made to my app after calling the helper method in the tests.
module Requests
module AuthenticationHelpers
def basic_http_auth(user)
credentials = ActionController::HttpAuthentication::Basic.encode_credentials(user.email,user.password)
Rack::MockRequest::DEFAULT_ENV['HTTP_AUTHORIZATION'] = credentials
end
end
end
And then in the actual test, I'll do something like this:
describe "GET /api/v1/messages" do
it 'returns an array of messages' do
get '/api/v1/messages'
basic_http_auth(FactoryGirl.create(:user))
expect(response).to eq(200)
end
end
So, if you're going to be using this across a lot of the API tests, move it into a support helper. And, if you're calling something multiple times in the same file, write a method (or put it in a let call) to DRY your your tests.

Building on Matthew's answer, with Doorkeeper, I ended up using the following:
module TokenMacros
def generate_access_token_for(user = nil)
user ||= FactoryGirl.create(:user)
authentication = FactoryGirl.create(:authentication, user: user)
application = Doorkeeper::Application.create!(:name => "MyApp", :redirect_uri => "http://app.com")
access_token = Doorkeeper::AccessToken.create!(:application_id => application.id, :resource_owner_id => authentication.identity.id)
access_token.token
end
end
which then allows me to call...
let(:user) { FactoryGirl.create(:user) }
let(:token) { generate_access_token_for(user) }
splendid

The easiest thing to do to avoid the duplication would be to define the token in the highest level describe so that it's available for all the examples in your spec.
In addition, to avoid any performance issue for the examples that don't depend on it, you can use let rather than before to define the token since let is lazily evaluated.
What you'd have then, is the following:
let(:access_token) do
user = FactoryGirl.create(:user)
authentication = FactoryGirl.create(:authentication, user: user)
application = Doorkeeper::Application.create!(:name => "MyApp", :redirect_uri => "http://app.com")
Doorkeeper::AccessToken.create!(:application_id => application.id, :resource_owner_id => authentication.identity.id)
end
You'd need to also change your references to this token from #access_token to access_token, since you're defining a method and not an instance variable.

Related

Playing a cassete whose request is not inside the use_cassete scope using VCR for Rails

I've built an API (MyAPI) that connects to an external API for authentication (ExAPI).
For all the requests to MyAPI where the user needs to be authenticated, he sends a cookie with a token that is then sent to ExAPI, and then it gets the user information.
I've recorded a cassete with this request:
describe 'VCR-RSpec integration' do
def make_http_request
connect = Faraday.new(url: (ENV['EX_API']).to_s) do |faraday|
faraday.request :url_encoded
faraday.response :logger
faraday.adapter Faraday.default_adapter
end
connect.authorization :Bearer, ENV['USER_TOKEN']
connect.get('/auth/...')
end
skip 'without an explicit cassette name' do
it 'records an http request' do
VCR.use_cassette('user_token') do
expect(make_http_request).to be_success
end
end
end
end
So, in my code, if I do a call to user_stories it expects a cookie with the user_token sends it to the ExAPI and if valid, executes the desired action.
This is how the spec is:
describe UserStoriesController, type: :controller do
before(:each) do
set_cookies
end
context do
let!(:user) {
FactoryGirl.create(:user)
}
let!(:some_user_stories) {
FactoryGirl.create_list(:user_story, 3, user: user)
}
describe 'GET index' do
it 'returns a successful 200 response' do
VCR.use_cassette('user_token') do
get :index
expect(response).to be_success
end
end
end
end
end
The problem is that the cassete is not used (I'm assuming that it's because the code to connect to the ExAPI is not inside the VCR.use_cassete scope, but in a method inside the controller.
Is that a way to do this?
Thanks
Ok, my mistake. I forgot to initialize VCR:
VCR.configure do |config|
config.cassette_library_dir = "fixtures/vcr_cassettes"
config.hook_into :webmock
end

How to do request spec for JWT authenticate app using RSpec

I have a Rails 5 API only app and using knock to do JWT authenticate.
After complete the model and model spec, I start to do the request spec.
But I have no idea how to complete the authentication inside the request spec in the right way,
My users controller,
module V1
class UsersController < ApplicationController
before_action :authenticate_user, except: [:create]
end
end
Application controller,
class ApplicationController < ActionController::API
include Knock::Authenticable
include ActionController::Serialization
end
My stupidest solution (call the get token request to get the JWT before the rest request),
context 'when the request contains an authentication header' do
it 'should return the user info' do
user = create(:user)
post '/user_token', params: {"auth": {"email": user.email, "password": user.password }}
body = response.body
puts body # {"jwt":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0ODgxMDgxMDYsInN1YiI6MX0.GDBHPzbivclJfwSTswXhDkV0TCFCybJFDrjBnLIfN3Q"}
# use the retrieved JWT for future requests
end
end
Any advice is appreciated.
def authenticated_header(user)
token = Knock::AuthToken.new(payload: { sub: user.id }).token
{ 'Authorization': "Bearer #{token}" }
end
describe 'GET /users?me=true' do
URL = '/v1/users?me=true'
AUTH_URL = '/user_token'
context 'when the request with NO authentication header' do
it 'should return unauth for retrieve current user info before login' do
get URL
expect(response).to have_http_status(:unauthorized)
end
end
context 'when the request contains an authentication header' do
it 'should return the user info' do
user = create(:user)
get URL, headers: authenticated_header(user)
puts response.body
end
end
end
With the help of Lorem's answer, I was able to implement something similar for my request spec. Sharing it here for others to see an alternate implementation.
# spec/requests/locations_spec.rb
require 'rails_helper'
RSpec.describe 'Locations API' do
let!(:user) { create(:user) }
let!(:locations) { create_list(:location, 10, user_id: user.id) }
describe 'GET /locations' do
it 'reponds with invalid request without JWT' do
get '/locations'
expect(response).to have_http_status(401)
expect(response.body).to match(/Invalid Request/)
end
it 'responds with JSON with JWT' do
jwt = confirm_and_login_user(user)
get '/locations', headers: { "Authorization" => "Bearer #{jwt}" }
expect(response).to have_http_status(200)
expect(json.size).to eq(10)
end
end
end
confirm_and_login_user(user) is defined in a request_spec_helper which is included as a module in rails_helper.rb:
# spec/support/request_spec_helper.rb
module RequestSpecHelper
def json
JSON.parse(response.body)
end
def confirm_and_login_user(user)
get '/users/confirm', params: {token: user.confirmation_token}
post '/users/login', params: {email: user.email, password: 'password'}
return json['auth_token']
end
end
I'm using the jwt gem for generating my tokens as described in this SitePoint tutorial (https://www.sitepoint.com/introduction-to-using-jwt-in-rails/)
Lorem's answer mostly worked for me. I got unrecognized keyword setting headers: on the get. I modified the authenticated_header method and put it in support/api_helper.rb so I could reuse it. The modification is to merge the auth token into request.headers.
# spec/support/api_helper.rb
module ApiHelper
def authenticated_header(request, user)
token = Knock::AuthToken.new(payload: { sub: user.id }).token
request.headers.merge!('Authorization': "Bearer #{token}")
end
end
In each spec file testing the api, I include api_helper.rb. And I call authenticated_header just before the get statement when testing the case of valid authentication...
# spec/controllers/api/v2/search_controller_spec.rb
RSpec.describe API::V2::SearchController, type: :controller do
include ApiHelper
...
describe '#search_by_id' do
context 'with an unauthenticated user' do
it 'returns unauthorized' do
get :search_by_id, params: { "id" : "123" }
expect(response).to be_unauthorized
end
end
context 'with an authenticated user' do
let(:user) { create(:user) }
it 'renders json listing resource with id' do
expected_result = { id: 123, title: 'Resource 123' }
authenticated_header(request, user)
get :search_by_id, params: { "id" : "123" }
expect(response).to be_successful
expect(JSON.parse(response.body)).to eq expected_result
end
end
The key lines in this second test are...
authenticated_header(request, user)
get :search_by_id, params: { "id" : "123" }

RSPEC: How to test that a JSON Web Token is returned by controller action

I am using Devise and JWT's to authenticate users in a project I am writing. I am having a hard time figuring out how to write a useful test to expect a JWT response.body (since each is encrypted).
The only thing I can think of is to test that they are structured as a JWT should be (a 3 segment, '.' delimited string).
Has anyone encountered testing random/hashed returns and come up with a better solution?
describe SessionTokensController, type: :controller do
let(:current_user) { FactoryGirl.create(:user) }
before(:each) do
sign_in current_user
end
describe '#create' do
it 'responds with a JWT' do
post :create
token = JSON.parse(response.body)['token']
expect(token).to be_kind_of(String)
segments = token.split('.')
expect(segments.size).to eql(3)
end
end
end
It really depends on what exactly you want to test.
If you simply want to test if the returned token exists and is valid you can do the following:
it 'responds with a valid JWT' do
post :create
token = JSON.parse(response.body)['token']
expect { JWT.decode(token, key) }.to_not raise_error(JWT::DecodeError)
end
Although it seems much more useful to validate the claims that the token includes:
let(:claims) { JWT.decode(JSON.parse(response.body)['token'], key) }
it 'returns a JWT with valid claims' do
post :create
expect(claims['user_id']).to eq(123)
end
In the latter example you can validate the exact claims you included in the JWT.
let(:user) { create(:user, password: "123456") }
describe "POST authenticate_user" do
context "with a valid password" do
it "authenticates successfully" do
post :authenticate_user, params:{email: user.email, password: "123456"}, format: :json
parsed_body = JSON.parse(response.body)
# binding.pry
expect(parsed_body.keys).to match_array(["auth_token", "user"])
expect(parsed_body['user']['email']).to eql("joe#gmail.com")
expect(parsed_body['user']['id']).to eql(user.id)
end
it "authentication fails" do
post :authenticate_user, params:{email: user.email, password: "123456789"}, format: :json
parsed_body = JSON.parse(response.body)
expect(parsed_body['errors'][0]).to eql("Invalid Username/Password")
end
end
end

Rspec 2.7 access controller session in spec before making request

I'm testing my controllers using Rspec and I can't seem to set the session variable of the current controller under test before making the request to the path.
For example this works:
describe "GET /controller/path" do
it "if not matching CRSF should display message" do
get controller_path
request.session[:state] = "12334"
end
end
This doesn't work (i get an error saying session is not a method of Nil class):
describe "GET /controller/path" do
it "if not matching CRSF should display message" do
request.session[:state] = "12334"
get controller_path
end
end
Any ideas?
With new version of RSpec this is done pretty nice, look:
describe SessionController do
# routes are mapped as:
# match 'login' => 'session#create'
# get 'logout' => 'session#destroy'
describe "#create" do
context "with valid credentials" do
let :credentials do
{ :email => 'example#gmail.com', :password => 'secret' }
end
let :user do
FactoryGirl.create(:user, credentials)
end
before :each do
post '/login', credentials
end
it "creates a user session" do
session[:user_id].should == user.id
end
end
# ...
end
describe "#destroy" do
context "when user logged in" do
before :each do
get "/logout", {}, { :user_id => 123 } # the first hash is params, second is session
end
it "destroys user session" do
session[:user_id].should be_nil
end
# ...
end
end
end
You can also use simply request.session[:user_id] = 123 inside before(:each) block, but above looks pretty nicer.
Try this:
describe "GET /controller/path" do
it "if not matching CRSF should display message" do
session[:state] = "12334"
get controller_path
end
end

Rails/Rspec Make tests pass with http basic authentication

Here my http basic authentication in the application controller file (application_controller.rb)
before_filter :authenticate
protected
def authenticate
authenticate_or_request_with_http_basic do |username, password|
username == "username" && password == "password"
end
end
and the default test for the index action of my home controller (spec/controllers/home_controller_spec.rb)
require 'spec_helper'
describe HomeController do
describe "GET 'index'" do
it "should be successful" do
get 'index'
response.should be_success
end
end
Test doesn't run because of the authentication method. I could comment "before_filter :authenticate" to run them but I would like to know if there is way to make them worked with the method.
Thank you!
Update (2013): Matt Connolly has provided a GIST which also works for request and controller specs: http://gist.github.com/4158961
Another way of doing this if you have many tests to run and don't want to include it everytime (DRYer code):
Create a /spec/support/auth_helper.rb file:
module AuthHelper
def http_login
user = 'username'
pw = 'password'
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user,pw)
end
end
In your test spec file:
describe HomeController do
render_views
# login to http basic auth
include AuthHelper
before(:each) do
http_login
end
describe "GET 'index'" do
it "should be successful" do
get 'index'
response.should be_success
end
end
end
Credit here - Archived site
Sorry I didn't seek enough, the solution seems to be the following:
describe "GET 'index'" do
it "should be successful" do
#request.env["HTTP_AUTHORIZATION"] = "Basic " + Base64::encode64("username:password")
get 'index'
response.should be_success
end
end
Some answers suggest to set request.env which is unsafe, because request can be nil and you will end up with private method env' called for nil:NilClass, especially when run single tests with rspec -e
Correct approach will be:
def http_login
user = 'user'
password = 'passw'
{
HTTP_AUTHORIZATION: ActionController::HttpAuthentication::Basic.encode_credentials(user,password)
}
end
get 'index', nil, http_login
post 'index', {data: 'post-data'}, http_login
For me, with Rails 6, I need keyword arguments for rspec get method like .. get route, params: params, headers: headers
Auth Helper method
module AuthHelper
def headers(options = {})
user = ENV['BASIC_AUTH_USER']
pw = ENV['BASIC_AUTH_PASSWORD']
{ HTTP_AUTHORIZATION: ActionController::HttpAuthentication::Basic.encode_credentials(user,pw) }
end
def auth_get(route, params = {})
get route, params: params, headers: headers
end
end
and the rspec request test.
describe HomeController, type: :request do
include AuthHelper
describe "GET 'index'" do
it "should be successful" do
auth_get 'index'
expect(response).to be_successful
end
end
end
When using Rspec to test Grape APIs, the following syntax works
post :create, {:entry => valid_attributes}, valid_session
where valid_session is
{'HTTP_AUTHORIZATION' => credentials}
and
credentials = ActionController::HttpAuthentication::Token.encode_credentials("test_access1")
These are great solutions for controller and request specs.
For feature tests using Capybara, here is a solution to make HTTP Basic authentication work:
spec/support/when_authenticated.rb
RSpec.shared_context 'When authenticated' do
background do
authenticate
end
def authenticate
if page.driver.browser.respond_to?(:authorize)
# When headless
page.driver.browser.authorize(username, password)
else
# When javascript test
visit "http://#{username}:#{password}##{host}:#{port}/"
end
end
def username
# Your value here. Replace with string or config location
Rails.application.secrets.http_auth_username
end
def password
# Your value here. Replace with string or config location
Rails.application.secrets.http_auth_password
end
def host
Capybara.current_session.server.host
end
def port
Capybara.current_session.server.port
end
end
Then, in your spec:
feature 'User does something' do
include_context 'When authenticated'
# test examples
end
My solution:
stub_request(method, url).with(
headers: { 'Authorization' => /Basic */ }
).to_return(
status: status, body: 'stubbed response', headers: {}
)
Use gem webmock
you can tighten verification by change:
/Basic */ -> "Basic #{Base64.strict_encode64([user,pass].join(':')).chomp}"
URL - can be a regular expression

Resources