I am trying to stub out an :authenticate_user method call in my request spec so I can test the user's association creation. I am using these blog posts as a guide on stubbing:
1) https://8thlight.com/blog/mike-knepper/2014/07/01/stubbing-authentication-and-authorization-in-controller-specs.html
2) http://johnnyji.me/rspec/2015/06/18/stubbing-controller-instance-methods-in-rspec.html
I'm not having any success with stubbing and I can't figure out what am I missing.
When I tried
it 'creates a new contract' do
allow(controller).to receive(:authenticate_user).and_return(user)
post api_v1_user_contracts_path(user), { params: contract_params}
expect(response).to have_http_status(200)
end
I got:
When I tried:
it 'creates a new contract' do
allow_any_instance_of(controller).to receive(:authenticate_user).and_return(user)
post api_v1_user_contracts_path(user), { params: contract_params}
expect(response).to have_http_status(200)
end
I got
My code:
spec/requests/contracts_api_spec.rb
require 'rails_helper'
require 'pry'
context "POST #create" do
let (:user) { User.create(full_name: "Jason Bourne", email: "jbourne#test.com", password: "123456") }
let (:contract_params) do
{
"contract[vendor]" => "Lebara",
"contract[starts_on]" => "2018-12-12",
"contract[ends_on]" => "2018-12-16",
"contract[price]" => "15"
}
end
it 'creates a new contract' do
allow(controller).to receive(:authenticate_user).and_return(user)
post api_v1_user_contracts_path(user), { params: contract_params}
expect(response).to have_http_status(200)
end
app/controllers/api/v1/contracts_controller.rb
class Api::V1::ContractsController < ApplicationController
before_action :authenticate_user
def show
if #current_user.contracts.find_by(id: params[:id])
render json: #current_user.contracts.find_by(id: params[:id])
else
render json: { error: "Contract not found"}, status: 400
end
end
def create
contract = #current_user.contracts.build(contract_params)
if contract.save
render json: contract
else
render json: { error: contract.errors }, status: 400
end
end
app/controllers/concerns/token_authenticatable.rb
class NotAuthorizedException < StandardError; end
module TokenAuthenticatable
extend ActiveSupport::Concern
included do
attr_reader :current_user
before_action :authenticate_user
rescue_from NotAuthorizedException, with: -> { render json: { error: 'Not Authorized' }, status: :unauthorized }
end
private
def authenticate_user
#current_user = DecodeAuthenticationCommand.call(request.headers).result
raise NotAuthorizedException unless #current_user
end
end
Additional questions:
1) Should I be using a real User object, or should that be a double? I'm assuming it should be a real user in order to test if the association creation is working.
2) Should I be using allow(Api::V1::ContractsController).to receive(:authenticate_user).and_return(user)? I've tried it before and didn't work but I didn't know it was because something else also was breaking it.
Thanks for any feedback you can give!
The point is that authenticate_user assigns user to the variable (and you use it later). Please try:
allow(DecodeAuthenticationCommand).to receive_message_chain(:call, :result).and_return(user)
With the test double, you will have to define all methods for the user, such as contracts. Also, you are checking if the contract was created - in my opinion, it is perfectly fine to use a real object for the user.
Related
I've got a simple message app to learn RSpec where one user can create message to another user (only logged users can write messages). I didn't used devise or FactoryBot, this app is as simple as possible just for rspec learning.
I wanted to run these tests for sessions controller, but the second one (when user has invalid params) gives me an error Expected response to be a <3XX: redirect>, but was a <200: OK> and I don't understand why since hours.
RSpec.describe SessionsController, type: :controller do
let(:create_user) { #user = User.create(username: 'John', password: 'test123') }
describe 'POST #create' do
context 'when user is logged in' do
it 'loads correct user details and redirect to the root path' do
create_user
post :create, params: { session: { username: #user.username, password: #user.password } }
expect(response).to redirect_to(root_path)
end
end
context 'when user has invalid params' do
before do
create_user
post :create, params: { session: { username: #user.username, password: 'somepass' } }
end
it 'render new action' do
expect(assigns(:user)).not_to eq create_user
expect(response).to redirect_to(action: 'new')
end
end
end
end
Sessions Controller
class SessionsController < ApplicationController
before_action :logged_in_redirect, only: %i[new create]
def new; end
def create
user = User.find_by(username: params[:session][:username])
if user && user.authenticate(params[:session][:password])
session[:user_id] = user.id
flash[:success] = 'You have successfully logged in'
redirect_to root_path
else
flash.now[:error] = 'There was something wrong with your login'
render 'new'
end
end
end
I'm not quite sure if line expect(assigns(:user)).not_to eq create_user is in line with convention but it doesn't matter for result.
In your test you expect redirect response:
expect(response).to redirect_to(action: 'new')
And in the controller you just render new template:
render 'new'
I think it's a good approach to render new, you should change your spec to expect this.
expect(response).to render_template(:new)
I can't seem to get my head over how to make post requests for testing a url in request spec tests, here's the test code
RSpec.describe "Certifications", type: :request do
describe "denies public access" do
it "for new certification form" do
get new_certification_path
expect(response).to redirect_to new_user_session_path
end
it "for creating certification" do
certification_attributes = FactoryGirl.attributes_for :certification
expect {
post "/certifications", { certification: certification_attributes }
}.to_not change(Certification, :count)
expect(response).to redirect_to new_user_session_path
end
end
end
Which gives the error
1) Certifications denies public access for creating certification
Failure/Error: post "/certifications", { certification: certification_attributes }
ArgumentError:
unknown keyword: certification
I've tried the :certifications => certification_attributes, basically can't get my head over on how to pass params.
The controller under test is, adding only relevant methods to this post.
class CertificationsController < ApplicationController
skip_before_action :authenticate_user!, if: :skip_user_authentication
before_action :set_certification, only: [:show, :edit, :update, :destroy]
# GET /certifications
# GET /certifications.json
def index
#certifications = Certification.all
end
# GET /certifications/1
# GET /certifications/1.json
def show
end
# GET /certifications/new
def new
#certification = Certification.new
end
# POST /certifications
# POST /certifications.json
def create
#certification = Certification.new(certification_params)
respond_to do |format|
if #certification.save
format.html { redirect_to #certification, notice: 'Certification was successfully created.' }
format.json { render :show, status: :created, location: #certification }
else
format.html { render :new }
format.json { render json: #certification.errors, status: :unprocessable_entity }
end
end
end
protected
def skip_user_authentication
request.format.json? && (action_name.eql?('show') || (action_name.eql?('index')))
end
end
I am trying to assert the behaviour of allowing all methods except certifications.json or certifications/1.json to not require authentication, there are other tests which access these URLs and they pass. The part of ensuring it does not allow any other request is where I am stuck. I am using Devise with Omnitauth Google OAuth2 for authentication in this application.
certification_attributes
{
:name=>"Foundation Certification",
:description=>"Foundation Certification",
:terms=>"Foundation Certification",
:seo_meta_keywords=>["laaa", "lalala certifications"],
:seo_meta_description=>"Foundation Certifications"
}
Send request parameters under :params keyword:
post "/certifications", params: { certification: certification_attributes }
^^^^^^
Looks like you have some sort of authentication set up. You need to log the user in before attempting a POST.
Passing of the params to post looks OK. Tricky to say more without seeing your controller.
so i've been trying to make a basic response test - with 200 and 403. I'm not sure i need to add anything else ..
accounts_spec.rb
RSpec.describe Api::V1::AccountsController, :type => :controller do
describe "GET index no account" do
it "has a 403 status code" do
get :index
expect(response.status).to eq(403)
end
end
describe "GET index with account" do
login_user
it "has a 200 status code" do
get :index
expect(response.status).to eq(200)
end
end
end
Accounts Controller #index
def index
#show user details
raise if not current_user
render json: { :user => current_user.as_json(:except=>[:created_at, :updated_at, :authorization_token, :provider, :uid, :id])}
rescue
render nothing: true, status: 403
end
I keep getting
1)Api::V1::AccountsController GET index with account has a 200 status code
expected: 200
got: 403
Any thoughts on where i'm doing it wrong ?
UPDATE
module ControllerMacros
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = FactoryGirl.create(:user)
sign_in :user, user
end
end
end
Much cleaner implementation
class SomeController < ApplicationController
before_action :authenticate
skip_before_action :verify_authenticity_token, if: :json_request?
def index
render json: { user: current_user...
end
protected
def json_request?
request.format.json?
end
def authenticate
head :unauthorized unless current_user
end
end
I also recommend in using ActiveModel Serializer https://github.com/rails-api/active_model_serializers. This will separate logic of render json and oy will have a separate class under serializer that defines the json output. So your render method will look like this:
render json: current_user, status: :ok
app/serializers/user.rb
class UserSerializer < ActiveModel::Serializer
attribute :id, :email #list your attributes for json output
end
If you want to test json response in your rspec what I find best is testing against json schema like this library https://github.com/sharethrough/json-schema-rspec.
Hope it helps
RSpec has an anonymous controller which comes in handy to test the "base" controller of other controllers, please see this example:
app/controllers/admin/base_controller.rb
class Admin::BaseController < ApplicationController
before_action :authenticate_user!
before_action :admin_required
layout 'admin'
private
def admin_required
render text: 'Unauthorized', status: :unauthorized unless current_user.admin?
end
end
spec/controllers/admin/base_controller_spec.rb
require 'rails_helper'
RSpec.describe Admin::BaseController, :type => :controller do
controller do
def index
head :ok
end
end
describe '#index' do
def do_request
get :index
end
context "as non-admin" do
before { sign_in create(:user) }
it 'raises error' do
do_request
expect(response).to have_http_status(:unauthorized)
expect(response).not_to be_success
end
end
context "as admin" do
before { sign_in create(:user, :with_admin) }
it 'does not raise error' do
do_request
expect(response).to be_success
end
end
end
end
I use a similar structure for my mailers.
My current implementation would need me to add a test to BaseMailer and add corresponding view for that test method.
Is there any way to achieve sort of anonymous mailer testing? something like:
app/mailers/base_mailer.rb
class BaseMailer < ActionMailer::Base
layout 'mailer'
default from: 'Support <support#example.com>',
reply_to: 'Support <support#example.com>',
end
spec/mailers/base_mailer_spec.rb
require 'rails_helper'
RSpec.describe Admin::BaseController, :type => :mailer do
mailer do # <= Anonymous mailer!
def test
mail
end
end
describe '#welcome' do
let(:email) { email_to }
def email_to
mailer.test # <= Anonymous mailer!
end
it { expect(email).to deliver_from 'Support <support#example.com>' }
it { expect(email).to reply_to 'Support <support#example.com>' }
end
end
Then I can get rid of having a test and app/views/base_mailer/test.html.erb file that I'll never used it but just use for testing.
Thanks!
P.S. This mailer testing syntax is from: https://github.com/bmabey/email-spec
Can be achieved, please see this comment:
RSpec.describe BaseMailer do
mailer = Class.new(BaseMailer) do
def a_sample_email
# We need a body to not render views
mail(body: '')
end
end
it 'has the default "from" address' do
email = mailer.a_sample_email
expect(email.from).to eq 'Support <support#example.com>'
end
end
Source: https://github.com/rspec/rspec-rails/issues/1182
I have written this controller code in Ruby on Rails
class PostsController < ApplicationController
before_filter :authenticate_user!
def index
#posts = Post.all(:order => "created_at DESC")
respond_to do |format|
format.html
end
end
def create
#post = Post.create(:message => params[:message])
respond_to do |format|
if #post.save
format.html { redirect_to posts_path }
format.js
else
flash[:notice] = "Message failed to save."
format.html { redirect_to posts_path }
end
end
end
end
and corresponding to this I have written the following test case :-
require 'spec_helper'
describe PostsController do
describe "GET 'index'" do
it "returns http success" do
get 'index'
response.should be_success
end
end
describe "#create" do
it "creates a successful mesaage post" do
#post = Post.create(message: "Message")
#post.should be_an_instance_of Post
end
end
end
I am getting failures on both. Please take a look on the code and help me figure out.
I suspect you are not logged in since you are using Devise?
Maybe you need to include the devise testhelpers:
describe PostsController do
include Devise::TestHelpers
before(:each) do
#user = User.create(...)
sign_in #user
end
#assertions go here
end
As Tigraine states, it appears as though you probably are not logged in (with Devise) when the tests get executed. However, showing the failures would help in narrowing down the problem further.
On top of that, the second test isn't really an integration test and I would probably prefer something like the following to test the same condition. There are two types of test you could do:
# inside 'describe "#create"'
let(:valid_params) { {'post' => {'title' => 'Test Post'} }
it 'creates a new Post' do
expect {
post :create, valid_params
}.to change(Post, :count).by(1)
end
# and / or
it 'assigns a new Post' do
post :create, valid_params
assigns(:post).should be_a(Post)
assigns(:post).should be_persisted
end
Don't forget to add this line into your spec_helper.rb
require "devise/test_helpers"
include Devise::TestHelpers
Nevertheless, here is link for Devise wiki - How to test Controllers where you can find more info about this approach. I recommend writing the before method without (:each), what I remember it sometimes causes problems.
before do
#user = FactoryGirl.create(:user)
sign_in #user
end
Can always use:
puts response.inspect
To see how your response looks like.