rails request spec PUT method with json doesn't go through - ruby-on-rails

In one of my tests I have to verify that a certain offer returns 404 response if the available limit (10 seats for example with that offer) are all sold, or it has expired which ever comes first.
I just cannot get the PUT request to work in Request Spec, here's my code
RSpec.describe "Offers", type: :request do
describe "gives not found response" do
it "when available limit exhausts before date till available" do
offer = Offer.new
Timecop.freeze(Date.today - 12.days) do
offer = FactoryGirl.create(:offer)
end
payload = FactoryGirl.attributes_for(:offer, :available_limit => 0, :discount_id => offer.discount.id, :coupon_id => offer.coupon.id)
sign_in
put '/offers/'<<offer.id, params: { id: offer.id, offer: payload }, as: :json
get "/vouchers/"<<offer.coupon.voucher_code
expect(response.status).to eq 404
end
end
end
needles to say that I have tried many hacks including
put '/offers/'<<offer.id.to_s<<".json", params: { offer: payload }
or even
put '/offers/'<<offer.id.to_s<<".json", payload
What I also noticed was that in one of the combinations the request did go through but it responds with both HTML and JSON format which lead to error in the spec as I am not running them under capybara (and I do not want to either)

Don't use << to build those paths. Using << with an id will insert a character code equivalent to the integer value of the id, e.g.
'XYZ' << 123 << 'ABC'
=> "XYZ{ABC"
Just use normal string interpolation, e.g.
put "/offers/#{offer.id}", params: { id: offer.id, offer: payload }, format: :json
get "/vouchers/#{offer.coupon.voucher_code}"
Credit to #house9 for noticing the format: :json part as well.

I think you want to use format: :json not as: :json
Try:
params = { id: offer.id, offer: payload }
put :offers, params: params, format: :json

Related

In Rails and minitest, how do I name my parameters when simulating a POST create request for my model?

I'm using Rails 5 and minitest. How do I submit parameters in minitest to simulate a POST request where my model gets created? I have this test
test "do create" do
person = people(:one)
score = 10
post ratings_url, params: {rating[person_id]: person.id, rating[score]: score}
# Verify we got the proper response
assert_response :success
end
but the above results in the error
localhost:myapp davea$ rails test test/controllers/ratings_controller_test.rb
Running via Spring preloader in process 30344
/Users/davea/.rvm/gems/ruby-2.4.0/gems/activesupport-5.0.6/lib/active_support/dependencies.rb:293:in `require': /Users/davea/Documents/workspace/myapp/test/controllers/ratings_controller_test.rb:14: syntax error, unexpected ':', expecting => (SyntaxError)
url, params: {rating[person_id]: person.id, rating[score]: score
The only reason I named the POST parameters "rating[person_id]" and "rating[score]" is because that's what they are named when the form is rendered in Rails as HTML.
Edit: In response to the answer given, I tried this in my test
post ratings_url, params: { rating: { person_id: person.id, score: score} }
and the controller looks like
def save(params)
rating = Rating.new(rating_params)
rating.user = current_user
respond_to do |format|
and the error I got was
Error:
RatingsControllerTest#test_do_create:
ActionController::UnknownFormat: ActionController::UnknownFormat
Complaining about the line
respond_to do |format|
The hash-keys in your params hash are not valid.
params: {rating[person_id]: person.id, rating[score]: score }
You can't use rating[person_id] and use the newer hash-syntax/ you will need to use hash-rockets here eg:
params: {rating[person_id] => person.id, rating[score] => score }
This is assuming you have a local variable called rating with the keys in it...
If you aren't actually trying to get a value out of a local variable called rating... then you may need to be more detailed about your structure eg:
params: { rating: { person_id: person.id, score: score } }
of course... depending on which kind of test you are writing, you might need to not use ratings_url (also try it with and without the params key) eg:
post :ratings, params: { rating: { person_id: person.id, score: score } }
post :ratings, rating: { person_id: person.id, score: score }
Well, maybe your controller is giving you a clue with ActionController::UnknownFormat. You should check you logs\test.log file to see if you can debug controller response over the test request. My hunch is that your controller is lacking some type in the respond_to block, or a missing template/render for a particular response type. Maybe it is posting as JS and your controller doesn't respond to that, or it is some situation where you should put a response to any type like this:
respond_to do |format|
format.any {render :text => "I respond to any request format"}
end

I want to write rspec test using factory girl for a particular usecase

My usecase is to build a http request with content type as json. for example i hit an endpoint with post call with basic authentication(authentication is Basic Auth could be like id: bla , key: blabla) and the body is for example:
{
"age":"Name",
"properties":{
"age":21}
}
and have to check response code and response message.
Sometimes i dont want to include age(mandatory field internally) to see if the proper status code is returned.
I want to write my tests in such a way that it should look like:
it 'some test' do
response = create(:event)
expect(response.code).to eq(200)
end
the above test should make an http call and return response to assert.
I am totally confused how to create a factory for the above scenario. I am an amateur with Rspec .
You need to pass parameters to perform POST action, based on response status code you can assert whether api call is success or not?
You cannot use factorygirl here.
for example:
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
refer: https://www.relishapp.com/rspec/
for rspec

Randomly failing spec

In my Rails tests I have this one failing randomly:
require 'spec_helper'
describe Api::V2::ClientsController, type: :controller do
context 'happy path' do
let!(:clients) {
[create(:client), create(:client)]
}
it 'return authorized user resource in JSON format' do
get :codes, format: :json
expect(response).to be_success
expect(json_response['clients'].size).to eql(2)
expect(json_response['clients'][0]).to eql('code' => clients[0].code)
expect(json_response['clients'][1]).to eql('code' => clients[1].code)
end
end
end
I'm not sure about using this:
let!(:clients) {
[create(:client), create(:client)]
}
I assume that your controller fetches the records in no specific order (e.g. Client.all) and that your database doesn't guarantee any default order, either.
In that case, you can use contains_exactly:
expect(json_response['clients'].size).to eql(2)
expect(json_response['clients']).to contain_exactly(
{ 'code' => clients[0].code },
{ 'code' => clients[1].code }
)
You could try swapping out the index numbers (json_response['clients'][0]) for something which doesn't rely on the json returned being in the right order.
For instance:
clients.each do |client|
expect(json_response['clients'].collect{|x|x['code']}).to include(client.code)
end
If this fixes the problem, you may want to order the client records being returned from your controller.

How to check for a JSON response using RSpec?

I have the following code in my controller:
format.json { render :json => {
:flashcard => #flashcard,
:lesson => #lesson,
:success => true
}
In my RSpec controller test I want to verify that a certain scenario does receive a success json response so I had the following line:
controller.should_receive(:render).with(hash_including(:success => true))
Although when I run my tests I get the following error:
Failure/Error: controller.should_receive(:render).with(hash_including(:success => false))
(#<AnnoController:0x00000002de0560>).render(hash_including(:success=>false))
expected: 1 time
received: 0 times
Am I checking the response incorrectly?
You could parse the response body like this:
parsed_body = JSON.parse(response.body)
Then you can make your assertions against that parsed content.
parsed_body["foo"].should == "bar"
You can examine the response object and verify that it contains the expected value:
#expected = {
:flashcard => #flashcard,
:lesson => #lesson,
:success => true
}.to_json
get :action # replace with action name / params as necessary
response.body.should == #expected
EDIT
Changing this to a post makes it a bit trickier. Here's a way to handle it:
it "responds with JSON" do
my_model = stub_model(MyModel,:save=>true)
MyModel.stub(:new).with({'these' => 'params'}) { my_model }
post :create, :my_model => {'these' => 'params'}, :format => :json
response.body.should == my_model.to_json
end
Note that mock_model will not respond to to_json, so either stub_model or a real model instance is needed.
Building off of Kevin Trowbridge's answer
response.header['Content-Type'].should include 'application/json'
There's also the json_spec gem, which is worth a look
https://github.com/collectiveidea/json_spec
Simple and easy to way to do this.
# set some variable on success like :success => true in your controller
controller.rb
render :json => {:success => true, :data => data} # on success
spec_controller.rb
parse_json = JSON(response.body)
parse_json["success"].should == true
You can also define a helper function inside spec/support/
module ApiHelpers
def json_body
JSON.parse(response.body)
end
end
RSpec.configure do |config|
config.include ApiHelpers, type: :request
end
and use json_body whenever you need to access the JSON response.
For example, inside your request spec you can use it directly
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)
expect(response).to have_http_status(:ok)
expect(response.content_type).to eq('application/vnd.api+json')
expect(json_body["data"]["attributes"]["email"]).to eq(user.email)
expect(json_body["data"]["attributes"]["name"]).to eq(user.name)
end
end
Another approach to test just for a JSON response (not that the content within contains an expected value), is to parse the response using ActiveSupport:
ActiveSupport::JSON.decode(response.body).should_not be_nil
If the response is not parsable JSON an exception will be thrown and the test will fail.
You could look into the 'Content-Type' header to see that it is correct?
response.header['Content-Type'].should include 'text/javascript'
When using Rails 5 (currently still in beta), there's a new method, parsed_body on the test response, which will return the response parsed as what the last request was encoded at.
The commit on GitHub: https://github.com/rails/rails/commit/eee3534b
A lot of the above answers are a bit out of date, so this is a quick summary for a more recent version of RSpec (3.8+). This solution raises no warnings from rubocop-rspec and is inline with rspec best practices:
A successful JSON response is identified by two things:
The content type of the response is application/json
The body of the response can be parsed without errors
Assuming that the response object is the anonymous subject of the test, both of the above conditions can be validate using Rspec's built in matchers:
context 'when response is received' do
subject { response }
# check for a successful JSON response
it { is_expected.to have_attributes(content_type: include('application/json')) }
it { is_expected.to have_attributes(body: satisfy { |v| JSON.parse(v) }) }
# validates OP's condition
it { is_expected.to satisfy { |v| JSON.parse(v.body).key?('success') }
it { is_expected.to satisfy { |v| JSON.parse(v.body)['success'] == true }
end
If you're prepared to name your subject then the above tests can be simplified further:
context 'when response is received' do
subject(:response) { response }
it 'responds with a valid content type' do
expect(response.content_type).to include('application/json')
end
it 'responds with a valid json object' do
expect { JSON.parse(response.body) }.not_to raise_error
end
it 'validates OPs condition' do
expect(JSON.parse(response.body, symoblize_names: true))
.to include(success: true)
end
end
JSON comparison solution
Yields a clean but potentially large Diff:
actual = JSON.parse(response.body, symbolize_names: true)
expected = { foo: "bar" }
expect(actual).to eq expected
Example of console output from real data:
expected: {:story=>{:id=>1, :name=>"The Shire"}}
got: {:story=>{:id=>1, :name=>"The Shire", :description=>nil, :body=>nil, :number=>1}}
(compared using ==)
Diff:
## -1,2 +1,2 ##
-:story => {:id=>1, :name=>"The Shire"},
+:story => {:id=>1, :name=>"The Shire", :description=>nil, ...}
(Thanks to comment by #floatingrock)
String comparison solution
If you want an iron-clad solution, you should avoid using parsers which could introduce false positive equality; compare the response body against a string. e.g:
actual = response.body
expected = ({ foo: "bar" }).to_json
expect(actual).to eq expected
But this second solution is less visually friendly as it uses serialized JSON which would include lots of escaped quotation marks.
Custom matcher solution
I tend to write myself a custom matcher that does a much better job of pinpointing at exactly which recursive slot the JSON paths differ. Add the following to your rspec macros:
def expect_response(actual, expected_status, expected_body = nil)
expect(response).to have_http_status(expected_status)
if expected_body
body = JSON.parse(actual.body, symbolize_names: true)
expect_json_eq(body, expected_body)
end
end
def expect_json_eq(actual, expected, path = "")
expect(actual.class).to eq(expected.class), "Type mismatch at path: #{path}"
if expected.class == Hash
expect(actual.keys).to match_array(expected.keys), "Keys mismatch at path: #{path}"
expected.keys.each do |key|
expect_json_eq(actual[key], expected[key], "#{path}/:#{key}")
end
elsif expected.class == Array
expected.each_with_index do |e, index|
expect_json_eq(actual[index], expected[index], "#{path}[#{index}]")
end
else
expect(actual).to eq(expected), "Type #{expected.class} expected #{expected.inspect} but got #{actual.inspect} at path: #{path}"
end
end
Example of usage 1:
expect_response(response, :no_content)
Example of usage 2:
expect_response(response, :ok, {
story: {
id: 1,
name: "Shire Burning",
revisions: [ ... ],
}
})
Example output:
Type String expected "Shire Burning" but got "Shire Burnin" at path: /:story/:name
Another example output to demonstrate a mismatch deep in a nested array:
Type Integer expected 2 but got 1 at path: /:story/:revisions[0]/:version
As you can see, the output tells you EXACTLY where to fix your expected JSON.
If you want to take advantage of the hash diff Rspec provides, it is better to parse the body and compare against a hash. Simplest way I've found:
it 'asserts json body' do
expected_body = {
my: 'json',
hash: 'ok'
}.stringify_keys
expect(JSON.parse(response.body)).to eql(expected_body)
end
I found a customer matcher here: https://raw.github.com/gist/917903/92d7101f643e07896659f84609c117c4c279dfad/have_content_type.rb
Put it in spec/support/matchers/have_content_type.rb and make sure to load stuff from support with something like this in you spec/spec_helper.rb
Dir[Rails.root.join('spec/support/**/*.rb')].each {|f| require f}
Here is the code itself, just in case it disappeared from the given link.
RSpec::Matchers.define :have_content_type do |content_type|
CONTENT_HEADER_MATCHER = /^(.*?)(?:; charset=(.*))?$/
chain :with_charset do |charset|
#charset = charset
end
match do |response|
_, content, charset = *content_type_header.match(CONTENT_HEADER_MATCHER).to_a
if #charset
#charset == charset && content == content_type
else
content == content_type
end
end
failure_message_for_should do |response|
if #charset
"Content type #{content_type_header.inspect} should match #{content_type.inspect} with charset #{#charset}"
else
"Content type #{content_type_header.inspect} should match #{content_type.inspect}"
end
end
failure_message_for_should_not do |model|
if #charset
"Content type #{content_type_header.inspect} should not match #{content_type.inspect} with charset #{#charset}"
else
"Content type #{content_type_header.inspect} should not match #{content_type.inspect}"
end
end
def content_type_header
response.headers['Content-Type']
end
end
For Your JSON response you should parse that response for expected results
For Instance: parsed_response = JSON.parse(response.body)
You can check other variables which is included in response like
expect(parsed_response["success"]).to eq(true)
expect(parsed_response["flashcard"]).to eq("flashcard expected value")
expect(parsed_response["lesson"]).to eq("lesson expected value")
expect(subject["status_code"]).to eq(201)
I prefer also check keys of JSON response, For Example:
expect(body_as_json.keys).to match_array(["success", "lesson","status_code", "flashcard"])
Here, We can use should matchers For expected results in Rspec

How to send raw post data in a Rails functional test?

I'm looking to send raw post data (e.g. unparamaterized JSON) to one of my controllers for testing:
class LegacyOrderUpdateControllerTest < ActionController::TestCase
test "sending json" do
post :index, '{"foo":"bar", "bool":true}'
end
end
but this gives me a NoMethodError: undefined method `symbolize_keys' for #<String:0x00000102cb6080> error.
What is the correct way to send raw post data in ActionController::TestCase?
Here is some controller code:
def index
post_data = request.body.read
req = JSON.parse(post_data)
end
I ran across the same issue today and found a solution.
In your test_helper.rb define the following method inside of ActiveSupport::TestCase:
def raw_post(action, params, body)
#request.env['RAW_POST_DATA'] = body
response = post(action, params)
#request.env.delete('RAW_POST_DATA')
response
end
In your functional test, use it just like the post method but pass the raw post body as the third argument.
class LegacyOrderUpdateControllerTest < ActionController::TestCase
test "sending json" do
raw_post :index, {}, {:foo => "bar", :bool => true}.to_json
end
end
I tested this on Rails 2.3.4 when reading the raw post body using
request.raw_post
instead of
request.body.read
If you look at the source code you'll see that raw_post just wraps request.body.read with a check for this RAW_POST_DATA in the request env hash.
Version for Rails 5:
post :create, body: '{"foo": "bar", "bool": true}'
See here - body string parameter is treated as raw request body.
I actually solved the same issues just adding one line
before simulating the rspec post request. What you do
is to populate the "RAW_POST_DATA". I tried to remove
the attributes var on the post :create, but if I do so,
it do not find the action.
Here my solution.
def do_create(attributes)
request.env['RAW_POST_DATA'] = attributes.to_json
post :create, attributes
end
In the controller the code you need to read the JSON is
something similar to this
#property = Property.new(JSON.parse(request.body.read))
Looking at stack trace running a test you can acquire more control on request preparation:
ActionDispatch::Integration::RequestHelpers.post => ActionDispatch::Integration::Session.process =>
Rack::Test::Session.env_for
You can pass json string as :params AND specify a content type "application/json". In other case content type will be set to "application/x-www-form-urlencoded" and your json will be parsed properly.
So all you need is to specify "CONTENT_TYPE":
post :index, '{"foo":"bar", "bool":true}', "CONTENT_TYPE" => 'application/json'
For those using Rails5+ integration tests, the (undocumented) way to do this is to pass a string in the params argument, so:
post '/path', params: raw_body, headers: { 'Content-Type' => 'application/json' }
I was searching very long for how to post raw JSON content in a integration test (Rails 5.1). I guess my solution could also help in this case.
I looked up the documentation and source code for the post method: https://api.rubyonrails.org/v5.1/classes/ActionDispatch/Integration/RequestHelpers.html#method-i-post
This directed me to the process method for more details: https://api.rubyonrails.org/v5.1/classes/ActionDispatch/Integration/Session.html#method-i-process
Thanks to this, I finally found out what parameters are accepted by the process and thus post method.
Here's what my final solution looked like:
post my_url, params: nil, headers: nil, env: {'RAW_POST_DATA' => my_body_content}, as: :json
If you are using RSpec (>= 2.12.0) and writing Request specs, the module that is included is ActionDispatch::Integration::Runner. If you take a look at the source code you can notice that the post method calls process which accepts a rack_env parameter.
All this means that you can simply do the following in your spec:
#spec/requests/articles_spec.rb
post '/articles', {}, {'RAW_POST_DATA' => 'something'}
And in the controller:
#app/controllers/articles_controller.rb
def create
puts request.body.read
end
Using Rails 4, I was looking to do this to test the processing of raw xml that was being posted to the controller. I was able to do it by just providing the string to the post:
raw_xml = File.read("my_raw.xml")
post :message, raw_xml, format: :xml
I believe if the parameter provided is a string, it just gets passed along to the controller as the body.
In rails, 5.1 the following work for me when doing a delete request that needed data in the body:
delete your_app_url, as: :json, env: {
"RAW_POST_DATA" => {"a_key" => "a_value"}.to_json
}
NOTE: This only works when doing an Integration test.
The post method expects a hash of name-value pairs, so you'll need to do something like this:
post :index, :data => '{"foo":"bar", "bool":true}'
Then, in your controller, get the data to be parsed like this:
post_data = params[:data]
As of Rails 4.1.5, this was the only thing that worked for me:
class LegacyOrderUpdateControllerTest < ActionController::TestCase
def setup
#request.headers["Content-Type"] = 'application/json'
end
test "sending json" do
post :index, '{"foo":"bar", "bool":true}'.to_json, { account_id: 5, order_id: 10 }
end
end
for a url at /accounts/5/orders/10/items. This gets the url params conveyed as well as the JSON body. Of course, if orders is not embedded then you can leave off the params hash.
class LegacyOrderUpdateControllerTest < ActionController::TestCase
def setup
#request.headers["Content-Type"] = 'application/json'
end
test "sending json" do
post :index, '{"foo":"bar", "bool":true}'.to_json
end
end
In Rails 4 (at least in 4.2.11.3) there's no easy way to test your controllers that consume json (functional tests). For parsing json in a running server the ActionDispatch::ParamsParser middleware is responsible. Controller tests though rely on Rack, which can't parse json to this day (not that it should).
You can do:
post :create, body_params.to_json
or:
post :update, body_parmas.to_json, url_params
But body_params won't be accessible in the controller via params. You've got to do JSON.parse(request.body.read). So the only thing that comes to mind is:
post :update, url_params.merge(body_params)
That is, in tests pass everything via parameters (application/x-www-form-urlencoded). In production the body will be parsed by ActionDispatch::ParamsParser to the same effect. Except that your numbers become strings (and possibly more):
# test/controllers/post_controller_test.rb
post :update, {id: 1, n: 2}
# app/controller/posts_controller.rb
def update
p params # tests:
# {"id"=>"1", "n" => "2", "controller"=>"posts", "action"=>"update"}
# production
# {"id"=>"1", "n" => 2, "controller"=>"posts", "action"=>"update"}
end
If you're willing to parse json in controllers yourself though you can do:
# test/controllers/post_controller_test.rb
post_json :update, {n: 2}.to_json, {id: 1}
# app/controller/posts_controller.rb
def update
p JSON.parse(request.body.read) # {"id"=>"1", "n" => 2, "controller"=>"posts", "action"=>"update"}
end
post :index, {:foo=> 'bar', :bool => 'true'}

Resources