POSTing raw JSON data with Rails 3.2.11 and RSpec
Asked Answered
L

7

47

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?

Lindblad answered 8/2, 2013 at 15:34 Comment(1)
As of Rails 4.2.6, setting request.env["RAW_POST_DATA"] in an RSpec controller spec is working for me.Codding
L
85

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
Lindblad answered 8/2, 2013 at 22:46 Comment(10)
This is the cleanest way I've found to test controllers that expect raw json POST requests. Thanks.Maganmagana
'CONTENT_TYPE' header is enoughCoolth
this solution is not working for me in Rails 3.2.13. My workaround was writing params = { token: 0, format: :json }. Also remove .to_json and the hash following it in the example. Also you might want to have config.include Rails.application.routes.url_helpers in spec_helper.rb. Verify json response with response.header['Content-Type'].should include 'application/json'Duvall
I did like this: post "/user/reset_password", params.merge(format: 'json') with Rails 3.2.14Eleusis
@Eleusis This approach worked for me, while the one in the above answer did not.Muro
@IanVaughan HTTP_ACCEPT tells that the return will be accept as JSON, the CONTENT_TYPE is saying that you are sending JSONBloodred
@Eleusis It worked as charm! Can you add your comments as answer ?Napoleonnapoleonic
Thank you @Duvall You saved my time!Appreciable
In Rails 5, try as: :json instead of format: :json to convert the payload.Louannlouanna
Thank you so much, this ended hours of search. This solution is pretty much the only one working with rspec 2.12 and rails 3.0.6.Mencher
F
11

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.

Fredra answered 20/2, 2015 at 15:1 Comment(2)
Awesome, I've been digging for quite some time for this exact answerAshkhabad
weird things happen, this works on a file and wont work on another file. I always encounters weird problems. :)Shrivel
O
11

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
Ossein answered 18/3, 2015 at 20:46 Comment(1)
If you're building a library that needs this kind of setup, this is the only way to get this behavior with a Rails 3, 4, and 5 compliant syntax.Socha
J
10

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
Jard answered 19/2, 2017 at 1:18 Comment(0)
P
6

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).

Penis answered 17/7, 2015 at 10:58 Comment(0)
M
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' }
Mathieu answered 16/6, 2021 at 17:41 Comment(0)
M
0

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.

Mencher answered 19/12, 2021 at 19:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.