Controller: payments_controller.rb
class PaymentsController < ApplicationController
# This is needed to have Postman work
skip_before_action :verify_authenticity_token
rescue_from ActiveRecord::RecordNotFound do |exception|
render json: 'not_found', status: :not_found
def create
new_payment = Payment.new(new_params)
current_loan = Loan.find(new_params[:loan_id])
if Payment.valid?(new_payment, current_loan)
Payment.received(new_payment, current_loan)
current_loan.save
new_payment.save
redirect_to '/loans'
else
raise 'Amount entered is above the remaining balance'
end
end
end
This method works when I test it in Postman. However, I can't seem to write a test for it that passes. I currently have:
payments_controller_spec.rb
require 'rails_helper'
RSpec.describe PaymentsController, type: :controller do
describe "#create", :type => :request do
let!(:loan) {Loan.create!(id: 1, funded_amount: 500.0)}
params = '{"payment":{"amount":400, "loan_id":2}}'
it 'creates and saves a payment while saving the associated fund_amount of the loan' do
post "/payments", params.to_json, {'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json'}
expect(loan.funded_amount).to eql(600.0)
end
end
end
The error is:
Failure/Error: post "/payments", params.to_json, {'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json'}
ActionController::ParameterMissing:
param is missing or the value is empty: payment
Valid parameters (that work with Postman) are:
{"payment":{"amount":400,"loan_id":2}}
Any help would be appreciated!
*** UPDATE ****
After messing around with this for a while, I finally got it to work with this:
describe "#create", :type => :request do
let!(:loan) {Loan.create!(id: 1, funded_amount: 500.0)}
it 'creates and saves a payment while saving the associated fund_amount of the loan' do
json = { :format => 'json', :payment => { :amount => 200.0, :loan_id => 1 } }
post '/payments', json
loan.reload
expect(loan.funded_amount).to eql(300.0)
end
end
You can pass in your params like this.
it 'creates and saves a payment while saving the associated fund_amount of the loan' do
post "/payments", payment: { amount: 400, loan_id: 1 }, {'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json'}
expect(loan.funded_amount).to eql(600.0)
end
Related
I'm starting to do some testing to my app in rails, and following official tutorial, I've written this:
class UserFlowTest < ActionDispatch::IntegrationTest
def login
post ns_login_user_path, { :user => { :username => 'user', :password => 'password' } }
assert_response 200
end
test "should complete a flow" do
login
post create_participant_path(:event_id => events(:myevent).id), {
:format => :json,
:event_role => event_roles(:regular_participant).id,
:in_team => true
}
r = JSON.parse(response.body)
assert_response 200
puts "response creating participation #{r.as_json}"
participant_id = r[:participant_id]
end
end
It does the login OK, but after that, when trying to create the participant, response is a variable with no .body attribute, just the number 200 (the status), so the JSON.parse method crashes.
This is the relevant part of my routes.rb:
# Events
scope 'events', :controller => :events do
# some routes
scope ':event_id', :controller => :events do
# some routes
scope 'participants', :controller => :participants do
post '', :action => :create_participant, :as => :create_participant
# some routes
end
end
end
And the controller ParticipantsController.rb:
class ParticipantsController < ApiController
before_action :require_login, :only => [:create_participant, :update_participant]
# Creates a participation of a person in the event
# Receives the following params:
# - +event_id+
# - +in_team+::_boolean
# - +event_role+
def create_participant
# … some logic
if participant.save
render :status => :ok, :json => Hash[
:participant_id => participant.id,
:team_participant_id => participant.team_participant_id
]
else
render :status => 406, :json => Hash[
:message => t('alerts.error_saving'),
:errors => participant.errors.as_json
]
end
end
end
I've seen the full response object on controller specs, but looking at the code, it appears that the ActionDispatch::IntegrationTest code only returns the response.status, not the entire response object.
The documentation doesn't directly say that you can access the response object. You might try doing a render_views and see if that makes any difference, but based on inspection of the code, it doesn't seem like it will.
I can't stub request to Flickr API for my
controller test. I use gem 'flickraw' for getting data from Flickr API.
flickr_search_controller.rb:
module Dashboard
class FlickrSearchController < Dashboard::BaseController
respond_to :js
def search
#search_tag = params[:search]
photos_list = if #search_tag.blank?
flickr.photos.getRecent(per_page: 10)
else
flickr.photos.search(text: #search_tag, per_page: 10)
end
#photos = photos_list.map { |photo| FlickRaw.url_q(photo) }
end
end
end
flickr_search_controller_spec.rb:
require 'rails_helper'
describe Dashboard::FlickrSearchController do
let(:user) { FactoryGirl.create(:user) }
before(:each) do
stub_request(:post, "https://api.flickr.com/services/rest").to_return(status: 200)
#controller.send(:auto_login, user)
end
describe 'when user didn\'t set search tag' do
it 'returns recend photo'do
get :search, search: ' '
expect(response.status).to eq(200)
end
end
end
I get in console next error:
Failures:
1) Dashboard::FlickrSearchController when user didn't set search tag returns recend photo
Failure/Error: flickr.photos.getRecent(per_page: 10)
WebMock::NetConnectNotAllowedError:
Real HTTP connections are disabled. Unregistered request: POST https://api.flickr.com/services/rest/ with body 'method=flickr.reflection.getMethods&format=json&nojsoncallback=1' with headers {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization'=>'OAuth realm="https://api.flickr.com/services/rest/", oauth_consumer_key="32904448e7d40c7e833c7b381c86cd31", oauth_nonce="lCL%2FUM9o8go5XNVy4F7p%2FNxHJrY%2BvFNLhlzueFq8Juc%3D", oauth_signature="1b77fc6af54b2b51%26", oauth_signature_method="PLAINTEXT", oauth_timestamp="1455128674", oauth_token="", oauth_version="1.0"', 'Content-Type'=>'application/x-www-form-urlencoded', 'User-Agent'=>'FlickRaw/0.9.8'}
You can stub this request with the following snippet:
stub_request(:post, "https://api.flickr.com/services/rest/").
with(:body => {"format"=>"json", "method"=>"flickr.reflection.getMethods", "nojsoncallback"=>"1"},
:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization'=>'OAuth realm="https://api.flickr.com/services/rest/", oauth_consumer_key="32904448e7d40c7e833c7b381c86cd31", oauth_nonce="lCL%2FUM9o8go5XNVy4F7p%2FNxHJrY%2BvFNLhlzueFq8Juc%3D", oauth_signature="1b77fc6af54b2b51%26", oauth_signature_method="PLAINTEXT", oauth_timestamp="1455128674", oauth_token="", oauth_version="1.0"', 'Content-Type'=>'application/x-www-form-urlencoded', 'User-Agent'=>'FlickRaw/0.9.8'}).
to_return(:status => 200, :body => "", :headers => {})
============================================================
Does somebody have idea how can I stub this request?
Note the slash in uri, the request you're stubbing is not the one being made
I'm writing an rspec test that sends a JSON via POST to a member route end point. But I'm getting a "no route matches" error when I do so. I'm not sure if there's something I need to add because this endpoint is a member route, or I'm just missing some HTTP request headers because I'm sending a JSON. Please help.
This is what I have:
Spec:
describe "#endpoint" do
context "type 1" do
before(:each) do
post :create, #params.merge(:abc => {:first_user_id => #user1.id, :second_user_id => #user2.id})
#mashup = assigns(:mashup)
end
it "should post the results successfully" do
units = [...]
users = [...]
params = #params.merge(:mashup_outcome => {:status => "success", :assetName => "MashupAssetName", :winningUserId => #user1.id}, :mashup_id => #mashup.id, :version_number => 1, :user_id => #user1.id, :id => #mashup.id ,:users => :users).to_json
#request.env["CONTENT_TYPE"] = "application/json"
#had to have a param key for my params below in order to bypass the NoMethodError
#In actual request body, it's just a JSON
post :over, :mashup => params
#mashups.in_progress.should be_false
end
end
context "type 2" do
before(:each) do
...
end
it "should post the results correctly" do
...
end
end
Routes:
namespace :mashup do
resources :mashups do
member do
post :endpoint
end
end
Controller:
def endpoint
if #mashup.complete_mashup(params)
render :json => api_success(#mashup.dpoints)
else
render :json => api_error({})
end
end
Error:
Failure/Error: post :endpoint, :mashup => params
ActionController::RoutingError:
No route matches {:mashup => "{...<JSON>...}", :controller => "api/mashup/mashups", :action => "endpoint"}
In order to ensure that my application is not vulnerable to this exploit, I am trying to create a controller test in RSpec to cover it. In order to do so, I need to be able to post raw JSON, but I haven't seemed to find a way to do that. In doing some research, I've determined that there at least used to be a way to do so using the RAW_POST_DATA header, but this doesn't seem to work anymore:
it "should not be exploitable by using an integer token value" do
request.env["CONTENT_TYPE"] = "application/json"
request.env["RAW_POST_DATA"] = { token: 0 }.to_json
post :reset_password
end
When I look at the params hash, token is not set at all, and it just contains { "controller" => "user", "action" => "reset_password" }. I get the same results when trying to use XML, or even when trying to just use regular post data, in all cases, it seems to not set it period.
I know that with the recent Rails vulnerabilities, the way parameters are hashed was changed, but is there still a way to post raw data through RSpec? Can I somehow directly use Rack::Test::Methods?
As far as I have been able to tell, sending raw POST data is no longer possible within a controller spec. However, it can be done pretty easily in a request spec:
describe "Example", :type => :request do
params = { token: 0 }
post "/user/reset_password", params.to_json, { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
#=> params contains { "controller" => "user", "action" => "reset_password", "token" => 0 }
end
This is the way to send raw JSON to a controller action (Rails 3+):
Let's say we have a route like this:
post "/users/:username/posts" => "posts#create"
And let's say you expect the body to be a json that you read by doing:
JSON.parse(request.body.read)
Then your test will look like this:
it "should create a post from a json body" do
json_payload = '{"message": "My opinion is very important"}'
post :create, json_payload, {format: 'json', username: "larry" }
end
{format: 'json'} is the magic that makes it happen. Additionally, if we look at the source for TestCase#post http://api.rubyonrails.org/classes/ActionController/TestCase/Behavior.html#method-i-process you can see that it takes the first argument after the action (json_payload) and if it is a string it sets that as raw post body, and parses the rest of the args as normal.
It's also important to point out that rspec is simply a DSL on top of the Rails testing architecture. The post method above is the ActionController::TestCase#post and not some rspec invention.
What we've done in our controller tests is explicitly set the RAW_POST_DATA:
before do
#request.env['RAW_POST_DATA'] = payload.to_json
post :my_action
end
Rails 5 example:
RSpec.describe "Sessions responds to JSON", :type => :request do
scenario 'with correct authentication' do
params = {id: 1, format: :json}
post "/users/sign_in", params: params.to_json, headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
expect(response.header['Content-Type']).to include 'application/json'
end
end
Here is a full working example of a controller test sending raw json data:
describe UsersController, :type => :controller do
describe "#update" do
context 'when resource is found' do
before(:each) do
#user = FactoryGirl.create(:user)
end
it 'updates the resource with valid data' do
#request.headers['Content-Type'] = 'application/vnd.api+json'
old_email = #user.email
new_email = Faker::Internet.email
jsondata =
{
"data" => {
"type" => "users",
"id" => #user.id,
"attributes" => {
"email" => new_email
}
}
}
patch :update, jsondata.to_json, jsondata.merge({:id => old_id})
expect(response.status).to eq(200)
json_response = JSON.parse(response.body)
expect(json_response['data']['id']).to eq(#user.id)
expect(json_response['data']['attributes']['email']).to eq(new_email)
end
end
end
end
The important parts are:
#request.headers['Content-Type'] = 'application/vnd.api+json'
and
patch :update, jsondata.to_json, jsondata.merge({:id => old_id})
The first makes sure that the content type is correctly set for your request, this is pretty straightforward.
The second part was giving me headaches for a few hours, my initial approach was quite a bit different, but it turned out that there is a Rails bug, which prevents us from sending raw post data in functional tests (but allows us in integration tests), and this is an ugly workaround, but it works (on rails 4.1.8 and rspec-rails 3.0.0).
On Rails 4:
params = { shop: { shop_id: new_subscrip.shop.id } }
post api_v1_shop_stats_path, params.to_json, { 'CONTENT_TYPE' => 'application/json',
'ACCEPT' => 'application/json' }
A slight alternative to #daniel-vandersluis answer, on rails 3.0.6, with rspec 2.99 and rspec-rails 2.99:
describe "Example", :type => :request do
params = { token: 0 }
post "/user/reset_password", params.merge({format: 'json'}).to_json, { 'CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json' }
end
The HTTP_ACCEPT header didn't make much difference, (it can be either HTTP_ACCEPT or just ACCEPT). But in my case, for it to work, the params had to: have the .merge({format: 'json'}) and .to_json
Another variation:
describe "Example", :type => :request do
params = { token: 0 }
post "/user/reset_password", params.merge({format: 'json'}).to_json, { 'CONTENT_TYPE' => Mime::JSON.to_s, 'HTTP_ACCEPT' => Mime::JSON }
end
It uses Mime::JSON and Mime::JSON.to_s instead of application/json for the header values.
I am doing functional tests for my controllers with Rspec. I have set my default response format in my router to JSON, so every request without a suffix will return JSON.
Now in rspec, i get an error (406) when i try
get :index
I need to do
get :index, :format => :json
Now because i am primarily supporting JSON with my API, it is very redundant having to specify the JSON format for every request.
Can i somehow set it to default for all my GET requests? (or all requests)
before :each do
request.env["HTTP_ACCEPT"] = 'application/json'
end
Put this in spec/support:
require 'active_support/concern'
module DefaultParams
extend ActiveSupport::Concern
def process_with_default_params(action, parameters, session, flash, method)
process_without_default_params(action, default_params.merge(parameters || {}), session, flash, method)
end
included do
let(:default_params) { {} }
alias_method_chain :process, :default_params
end
end
RSpec.configure do |config|
config.include(DefaultParams, :type => :controller)
end
And then simply override default_params:
describe FooController do
let(:default_params) { {format: :json} }
...
end
The following works for me with rspec 3:
before :each do
request.headers["accept"] = 'application/json'
end
This sets HTTP_ACCEPT.
Here is a solution that
works for request specs,
works with Rails 5, and
does not involve private API of Rails (like process).
Here's the RSpec configuration:
module DefaultFormat
extend ActiveSupport::Concern
included do
let(:default_format) { 'application/json' }
prepend RequestHelpersCustomized
end
module RequestHelpersCustomized
l = lambda do |path, **kwarg|
kwarg[:headers] = {accept: default_format}.merge(kwarg[:headers] || {})
super(path, **kwarg)
end
%w(get post patch put delete).each do |method|
define_method(method, l)
end
end
end
RSpec.configure do |config|
config.include DefaultFormat, type: :request
end
Verified with
describe 'the response format', type: :request do
it 'can be overridden in request' do
get some_path, headers: {accept: 'text/plain'}
expect(response.content_type).to eq('text/plain')
end
context 'with default format set as HTML' do
let(:default_format) { 'text/html' }
it 'is HTML in the context' do
get some_path
expect(response.content_type).to eq('text/html')
end
end
end
FWIW, The RSpec configuration can be placed:
Directly in spec/spec_helper.rb. This is not suggested; the file will be loaded even when testing library methods in lib/.
Directly in spec/rails_helper.rb.
(my favorite) In spec/support/default_format.rb, and be loaded explicitly in spec/rails_helper.rb with
require 'support/default_format'
In spec/support, and be loaded by
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
which loads all the files in spec/support.
This solution is inspired by knoopx's answer. His solution doesn't work for request specs, and alias_method_chain has been deprecated in favor of Module#prepend.
In RSpec 3, you need make JSON tests be request specs in order to have the views render. Here is what I use:
# spec/requests/companies_spec.rb
require 'rails_helper'
RSpec.describe "Companies", :type => :request do
let(:valid_session) { {} }
describe "JSON" do
it "serves multiple companies as JSON" do
FactoryGirl.create_list(:company, 3)
get 'companies', { :format => :json }, valid_session
expect(response.status).to be(200)
expect(JSON.parse(response.body).length).to eq(3)
end
it "serves JSON with correct name field" do
company = FactoryGirl.create(:company, name: "Jane Doe")
get 'companies/' + company.to_param, { :format => :json }, valid_session
expect(response.status).to be(200)
expect(JSON.parse(response.body)['name']).to eq("Jane Doe")
end
end
end
As for setting the format on all tests, I like the approach from this other answer: https://stackoverflow.com/a/14623960/1935918
Perhaps you could add the first answer into spec/spec_helper or spec/rails_helper with this:
config.before(:each) do
request.env["HTTP_ACCEPT"] = 'application/json' if defined? request
end
if in model test (or any not exist request methods context), this code just ignore.
it worked with rspec 3.1.7 and rails 4.1.0
it should be worked with all rails 4 version generally speaking.
Running Rails 5 and Rspec 3.5 I had to set the headers to accomplish this.
post '/users', {'body' => 'params'}, {'ACCEPT' => 'application/json'}
Thi matches what the example in the docs looks like:
require "rails_helper"
RSpec.describe "Widget management", :type => :request do
it "creates a Widget" do
headers = {
"ACCEPT" => "application/json", # This is what Rails 4 accepts
"HTTP_ACCEPT" => "application/json" # This is what Rails 3 accepts
}
post "/widgets", { :widget => {:name => "My Widget"} }, headers
expect(response.content_type).to eq("application/json")
expect(response).to have_http_status(:created)
end
end
Per the Rspec docs, the supported method is through the headers:
require "rails_helper"
RSpec.describe "Widget management", :type => :request do
it "creates a Widget" do
headers = {
"ACCEPT" => "application/json", # This is what Rails 4 and 5 accepts
"HTTP_ACCEPT" => "application/json", # This is what Rails 3 accepts
}
post "/widgets", :params => { :widget => {:name => "My Widget"} }, :headers => headers
expect(response.content_type).to eq("application/json")
expect(response).to have_http_status(:created)
end
end
For those folks who work with request tests the easiest way I found is to override #process method in ActionDispatch::Integration::Session and set default as parameter to :json like this:
module DefaultAsForProcess
def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: :json)
super
end
end
ActionDispatch::Integration::Session.prepend(DefaultAsForProcess)
Not sure if this will work for this specific case. But what I needed in particular was to be able to pass a params hash to the post method. Most solutions seem to be for rspec 3 and up, and mention adding a 3rd parameter like so:
post '/post_path', params: params_hash, :format => 'json'
(or similar, the :format => 'json' bit varies)
But none of those worked. The controller would receive a hash like: {params: => { ... }}, with the unwanted params: key.
What did work (with rails 3 and rspec 2) was:
post '/post_path', params_hash.merge({:format => 'json'})
Also check this related post, where I got the solution from: Using Rspec, how do I test the JSON format of my controller in Rails 3.0.11?
Why don't RSpec's methods, "get", "post", "put", "delete" work in a controller spec in a gem (or outside Rails)?
Based off this question, you could try redefining process() in ActionController::TestCase from https://github.com/rails/rails/blob/32395899d7c97f69b508b7d7f9b7711f28586679/actionpack/lib/action_controller/test_case.rb.
Here is my workaround though.
describe FooController do
let(:defaults) { {format: :json} }
context 'GET index' do
let(:params) { defaults }
before :each do
get :index, params
end
# ...
end
context 'POST create' do
let(:params) { defaults.merge({ name: 'bar' }) }
before :each do
post :create, params
end
# ...
end
end