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.
Related
Currently i am catching the error not_found like this
def show
begin
#task = Task.find(params[:id])
rescue ActiveRecord::RecordNotFound => e
render json: { error: e.to_s }, status: :not_found and return
end
and the rspec test would be like this expect(response).to be_not_found
but i dont want to do that (rescue ActiveRecord::RecordNotFound => e) in every single function (update, create, destroy and so on)
there is another way?
for example this way
rescue_from ActiveRecord::RecordNotFound, with: :not_found
def not_found
respond_to do |format|
format.json { head :not_found }
end
end
but i dont know how can i test with that
i would like to test the same way
expect(response).to be_not_found
I think that your original implementation with an error node is a better response but your modified change is a better way to handle so I would suggest combining the 2 concepts via
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :not_found
private
def not_found(exception)
respond_to do |format|
format.json do
# I expanded the response a bit but handle as you see fit
render json: {
error: {
message: 'Record Not Found',
data: {
record_type: exception.model,
id: exception.id
}
}
}, status: :not_found
end
end
end
You should be able to maintain your current test in this case while avoiding the need to individually handle in each request.
You can add the below code in your application_controller.rb.
around_filter :catch_not_found #=> only:[:show, :edit]
def catch_not_found
yield
rescue ActiveRecord::RecordNotFound => e
respond_to do |format|
format.json { render json: { error: e.to_s }, status: :not_found and return }
format.html { redirect_to root_url, :flash => { :error => "Record not found." } and return }
end
end
Below is the simple example for test cases using RSpec. Modify as per your requirements.
staff_controller.rb
def show
#staff = Staff.find(params[:id])
end
RSpec
let(:staff) { FactoryBot.create(:staff) }
describe "GET #show" do
it "Renders show page for valid staff" do
get :show, {:id => staff.to_param}
expect(response).to render_template :show
end
it "redirects to root path on staff record not_found" do
get :show, id: 100
expect(response).to redirect_to(root_path)
end
end
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.
In the routes.rb I have this nested resource
# OBSERVATIVE SESSIONS
resources :observative_sessions do
# OBSERVATIONS
resources :observations
end
In observations_controller.rb
def new
#observative_session = ObservativeSession.find(params[:observative_session_id])
#observation = Observation.new
#observation.observative_session_id = #observative_session.id
end
def create
#observative_session = ObservativeSession.find(params[:observative_session_id])
#observation = #observative_session.observations.build(observation_params)
#observation.user_id = current_user.id
respond_to do |format|
if #observation.save
format.html { redirect_to [#observative_session, #observation], notice: 'Observation was successfully created.' }
format.json { render :show, status: :created, location: #observation }
else
format.html { render :new }
format.json { render json: #observation.errors, status: :unprocessable_entity }
end
end
end
And in observations_controller_test.rb I set up both observation and observative session. The test of new works just fine.
class ObservationsControllerTest < ActionDispatch::IntegrationTest
include Devise::Test::IntegrationHelpers
setup do
#observative_session = observative_sessions(:one)
#observation = observations(:two)
sign_in users(:admin_user)
end
test "should get new" do
get new_observative_session_observation_path(#observative_session)
assert_response :success
end
test "should create observation" do
assert_difference('Observation.count') do
post observative_session_observation_path(#observative_session, #observation), params: { observation: { start_time: #observation.start_time, description: #observation.description, rating: #observation.rating, notes: #observation.notes, celestial_body_name: #observation.celestial_body_name, telescope_name: #observation.telescope_name, binocular_name: #observation.binocular_name, eyepiece_name: #observation.eyepiece_name, filter_name: #observation.filter_name, user_id: #observation.user_id, observative_session_id: #observation.observative_session_id }}
end
But this is the error I get in the creation test
test_should_create_observation
ActionController::RoutingError: No route matches [POST] "/observative_sessions/980190962/observations/298486374"
I can't understand what I'm doing wrong.
Thanks for your help.
When you say POST observation_session_observation_path(#observation_session, #observation) you are telling it to post to a url the has both :observation_session_id and an :id in the params, where the id is that of #obseravtion. However, POST paths for create actions don’t take that last id param (ostensibly you are creating a new record with that action).
Try dropping #observation from your path helper (and make sure you are using the correct create path: observation_session_observations_path(#observation_session).
You can do rake routes to see your routes in your terminal, or localhost:3000/rails/info/routes to see it in the browser.
I also see in your new action you are assigning the observation_session_id manually. I recommend you either do what you do later and call #obervation_session.observations.build, or Observation.new(observation_session: #observation_session). You should avoid setting ids like that.
I'm am new to rails. I want to consume my rest API with an android application. So I want to test whether my controller handles POST request or not. I'm using Advanced REST client for chrome to make a POST request.
Errors
Bad request 400 - rest client (chrome)
Update 1:
Log output:
Started POST "/android" for 127.0.0.1 at 2015-04-17 00:51:09 +0530
Error occurred while parsing request parameters.
Contents:
{tablet_id:1,pulse:2,pulse_timestamp:'NOW()'}
ActionDispatch::ParamsParser::ParseError (795: unexpected token at '{tablet_id:1,pulse:2,pulse_timestamp:'NOW()'}'):
My rails controller:
class Android::PulseFeedbacksController < ApplicationController
before_action :set_pulse_feedback, only: [:show, :edit, :update,:destroy]
respond_to json
# GET /pulse_feedbacks
# GET /pulse_feedbacks.json
def index
#pulse_feedbacks = PulseFeedback.all
respond_to do |format|
format.json { render json: #pulse_feedbacks }
format.xml { render xml: #pulse_feedbacks }
end
end
def create
#pulse_feedback = PulseFeedback.new(pulse_feedback_params)
respond_to do |format|
if #pulse_feedback.save
format.html { redirect_to #pulse_feedback, notice: 'Pulse feedback was successfully created.' }
format.json { render :show, status: :created, location: #pulse_feedback }
else
format.html { render :new }
format.json { render json: #pulse_feedback.errors, status: :unprocessable_entity }
end
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_pulse_feedback
#pulse_feedback = PulseFeedback.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def pulse_feedback_params
params[:pulse_feedback]
end
end
Check out the rails routing guides. Specifically at the section where it defines the types of HTTP verbs created for a controller. By default the only POST is :create. But you can edit your routes to explicitly allow POSTs for your own custom APIs by changing Routes.
Maybe this would help:
def pulse_feedback_params
params.require(:pulse_feedback).permit!
end
But of course server log output will be very helpful.
Thank you Guys, after lot of head breaking got it working.
The problem was with my application_controller.rb and my json format above.
I had to comment this line
before_action :authenticate_user!
in my application_controller.rb file
class ApplicationController < ActionController::Base
respond_to :html, :json
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :null_session, if: Proc.new { |c| c.request.format == 'application/json' }
#before_action :authenticate_user!
end
It wasn't accepting POST request from unauthorized clients(as I was using a chrome-addon to make POST request)
And my json format should have been:
{
"pulse_feedback": {
"tablet_id": "1",
"pulse": "2",
"pulse_timestamp": "NOW()"
}
}
I am learning rspec and trying to test my Account controller on create. After a user creates an account (i.e. chooses a name and a subdomain), he's redirected to a login page on his new subdomain.
My test returns NoMethodError: undefined method 'subdomain' for #<Hash:0x00000107888c88>
My account Factory is setup to generate a subdomain, so I don't see a problem with my logic. Is is just a syntax issue ?
accounts_controller.rb
class AccountsController < ApplicationController
skip_before_filter :authenticate_user!, only: [:new, :create]
def create
#account = Account.new(account_params)
respond_to do |format|
if #account.save
format.html { redirect_to new_user_session_url(subdomain: #account.subdomain, mp: 'signup' ) }
else
format.html { render action: 'new' }
format.json { render json: #account.errors, status: :unprocessable_entity }
end
end
end
end
/specs/controlles/accounts_controller_spec.rb
require 'rails_helper'
RSpec.describe AccountsController, :type => :controller do
describe "POST #create" do
context "with valid attributes" do
before :each do
#account = FactoryGirl.attributes_for(:account).merge( owner_attributes: FactoryGirl.attributes_for(:owner) )
end
it "redirects to the account subdomain login page" do
expect(post :create, account: #account).to redirect_to new_user_session_url(:subdomain => #account.subdomain)
end
end
context "with invalid attributes" do
it "does not save the new account in the database"
it "re-renders the :new template"
end
end
end
In your test, #account is a hash of account attributes
#account = FactoryGirl.attributes_for(:account).merge( owner_attributes: FactoryGirl.attributes_for(:owner) )
The above line returns a hash which you are passing as parameter while making the request
you should probably be doing account[:subdomain]
expect(post :create, account: #account).to redirect_to new_user_session_url(:subdomain => #account[:subdomain])